Prepare merge.

This commit is contained in:
Martin Fietz 2015-04-18 12:16:45 +02:00
commit 1bb1f0990f
63 changed files with 2429 additions and 905 deletions

View File

@ -17,6 +17,7 @@ dependencies {
compile 'com.squareup.okhttp:okhttp:2.2.0'
compile 'com.squareup.okhttp:okhttp-urlconnection:2.2.0'
compile 'com.squareup.okio:okio:1.2.0'
compile 'de.greenrobot:eventbus:2.4.0'
compile project(':core')
compile project(':library:drag-sort-listview')
}

View File

@ -78,4 +78,9 @@
public static *** w(...);
public static *** d(...);
public static *** e(...);
}
}
# greenrobot EventBus
-keepclassmembers class ** {
public void onEvent*(**);
}

View File

@ -2,12 +2,6 @@ package de.test.antennapod.service.playback;
import android.content.Context;
import android.test.InstrumentationTestCase;
import de.danoeh.antennapod.core.feed.EventDistributor;
import de.danoeh.antennapod.core.feed.Feed;
import de.danoeh.antennapod.core.feed.FeedItem;
import de.danoeh.antennapod.core.service.playback.PlaybackServiceTaskManager;
import de.danoeh.antennapod.core.storage.PodDBAdapter;
import de.danoeh.antennapod.core.util.playback.Playable;
import java.util.ArrayList;
import java.util.Date;
@ -15,6 +9,15 @@ import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import de.danoeh.antennapod.core.feed.EventDistributor;
import de.danoeh.antennapod.core.feed.Feed;
import de.danoeh.antennapod.core.feed.FeedItem;
import de.danoeh.antennapod.core.feed.QueueEvent;
import de.danoeh.antennapod.core.service.playback.PlaybackServiceTaskManager;
import de.danoeh.antennapod.core.storage.PodDBAdapter;
import de.danoeh.antennapod.core.util.playback.Playable;
import de.greenrobot.event.EventBus;
/**
* Test class for PlaybackServiceTaskManager
*/
@ -94,7 +97,7 @@ public class PlaybackServiceTaskManagerTest extends InstrumentationTestCase {
};
EventDistributor.getInstance().register(queueListener);
List<FeedItem> queue = writeTestQueue("a");
EventDistributor.getInstance().sendQueueUpdateBroadcast();
EventBus.getDefault().post(new QueueEvent(QueueEvent.Action.ADDED_ITEMS, queue));
countDownLatch.await(5000, TimeUnit.MILLISECONDS);
assertNotNull(queue);

View File

@ -14,6 +14,7 @@ import de.danoeh.antennapod.core.feed.FeedMedia;
import de.danoeh.antennapod.core.storage.DBReader;
import de.danoeh.antennapod.core.storage.FeedItemStatistics;
import de.danoeh.antennapod.core.storage.PodDBAdapter;
import de.danoeh.antennapod.core.util.LongList;
import de.danoeh.antennapod.core.util.flattr.FlattrStatus;
import static de.test.antennapod.storage.DBTestUtils.saveFeedlist;
@ -194,7 +195,7 @@ public class DBReaderTest extends InstrumentationTestCase {
final Context context = getInstrumentation().getTargetContext();
final int numItems = 10;
List<FeedItem> queue = saveQueue(numItems);
List<Long> ids = DBReader.getQueueIDList(context);
LongList ids = DBReader.getQueueIDList(context);
assertNotNull(ids);
assertTrue(queue.size() == ids.size());
for (int i = 0; i < queue.size(); i++) {

View File

@ -5,16 +5,6 @@ import android.database.Cursor;
import android.test.InstrumentationTestCase;
import android.util.Log;
import de.danoeh.antennapod.core.feed.Chapter;
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.SimpleChapter;
import de.danoeh.antennapod.core.storage.DBReader;
import de.danoeh.antennapod.core.storage.DBWriter;
import de.danoeh.antennapod.core.storage.PodDBAdapter;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
@ -25,6 +15,16 @@ import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import de.danoeh.antennapod.core.feed.Chapter;
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.SimpleChapter;
import de.danoeh.antennapod.core.storage.DBReader;
import de.danoeh.antennapod.core.storage.DBWriter;
import de.danoeh.antennapod.core.storage.PodDBAdapter;
/**
* Test class for DBWriter
*/
@ -674,13 +674,13 @@ public class DBWriterTest extends InstrumentationTestCase {
assertTrue(item.getId() != 0);
}
for (int removeIndex = 0; removeIndex < NUM_ITEMS; removeIndex++) {
final long id = feed.getItems().get(removeIndex).getId();
final FeedItem item = feed.getItems().get(removeIndex);
adapter = new PodDBAdapter(context);
adapter.open();
adapter.setQueue(feed.getItems());
adapter.close();
DBWriter.removeQueueItem(context, id, false).get(TIMEOUT, TimeUnit.SECONDS);
DBWriter.removeQueueItem(context, item, false).get(TIMEOUT, TimeUnit.SECONDS);
adapter = new PodDBAdapter(context);
adapter.open();
Cursor queue = adapter.getQueueIDCursor();
@ -688,10 +688,10 @@ public class DBWriterTest extends InstrumentationTestCase {
for (int i = 0; i < queue.getCount(); i++) {
assertTrue(queue.moveToPosition(i));
final long queueID = queue.getLong(0);
assertTrue(queueID != id); // removed item is no longer in queue
assertTrue(queueID != item.getId()); // removed item is no longer in queue
boolean idFound = false;
for (FeedItem item : feed.getItems()) { // items that were not removed are still in the queue
idFound = idFound | (item.getId() == queueID);
for (FeedItem other : feed.getItems()) { // items that were not removed are still in the queue
idFound = idFound | (other.getId() == queueID);
}
assertTrue(idFound);
}

View File

@ -25,7 +25,9 @@ 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.QueueEvent;
import de.danoeh.antennapod.core.storage.PodDBAdapter;
import de.greenrobot.event.EventBus;
import de.test.antennapod.util.service.download.HTTPBin;
import de.test.antennapod.util.syndication.feedgenerator.RSS2Generator;
@ -202,6 +204,6 @@ public class UITestUtils {
adapter.setQueue(queue);
adapter.close();
EventDistributor.getInstance().sendFeedUpdateBroadcast();
EventDistributor.getInstance().sendQueueUpdateBroadcast();
EventBus.getDefault().post(new QueueEvent(QueueEvent.Action.ADDED_ITEMS, queue));
}
}

View File

@ -1,15 +1,17 @@
package de.test.antennapod.util.syndication.feedgenerator;
import android.util.Xml;
import de.danoeh.antennapod.core.feed.Feed;
import de.danoeh.antennapod.core.feed.FeedItem;
import de.danoeh.antennapod.core.feed.FeedMedia;
import de.danoeh.antennapod.core.syndication.util.SyndDateUtils;
import org.xmlpull.v1.XmlSerializer;
import java.io.IOException;
import java.io.OutputStream;
import de.danoeh.antennapod.core.feed.Feed;
import de.danoeh.antennapod.core.feed.FeedItem;
import de.danoeh.antennapod.core.feed.FeedMedia;
import de.danoeh.antennapod.core.util.DateUtils;
/**
* Creates Atom feeds. See FeedGenerator for more information.
*/
@ -83,9 +85,9 @@ public class AtomGenerator implements FeedGenerator{
if (item.getPubDate() != null) {
xml.startTag(null, "published");
if ((flags & FEATURE_USE_RFC3339LOCAL) != 0) {
xml.text(SyndDateUtils.formatRFC3339Local(item.getPubDate()));
xml.text(DateUtils.formatRFC3339Local(item.getPubDate()));
} else {
xml.text(SyndDateUtils.formatRFC3339UTC(item.getPubDate()));
xml.text(DateUtils.formatRFC3339UTC(item.getPubDate()));
}
xml.endTag(null, "published");
}

View File

@ -3,7 +3,7 @@ package de.test.antennapod.util.syndication.feedgenerator;
import android.util.Xml;
import de.danoeh.antennapod.core.feed.Feed;
import de.danoeh.antennapod.core.feed.FeedItem;
import de.danoeh.antennapod.core.syndication.util.SyndDateUtils;
import de.danoeh.antennapod.core.util.DateUtils;
import org.xmlpull.v1.XmlSerializer;
import java.io.IOException;
@ -79,7 +79,7 @@ public class RSS2Generator implements FeedGenerator{
}
if (item.getPubDate() != null) {
xml.startTag(null, "pubDate");
xml.text(SyndDateUtils.formatRFC822Date(item.getPubDate()));
xml.text(DateUtils.formatRFC822Date(item.getPubDate()));
xml.endTag(null, "pubDate");
}
if ((flags & FEATURE_WRITE_GUID) != 0) {

View File

@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
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.

View File

@ -77,5 +77,8 @@ licensed under the Apache 2.0 license <a href="LICENSE_OKIO.txt">(View)</a>
<h2>Material Design Icons <a href="https://github.com/google/material-design-icons">(Link)</a></h2>
by Google, licensed under an Attribution-ShareAlike 4.0 International license <a href="LICENSE_MATERIAL_DESIGN_ICONS.txt">(View)</a>
<h2>EventBus <a href="https://github.com/greenrobot/EventBus">(Link>)</a></h2>
by greenrobot, licensed under the Apache 2.0 license <a href="LICENSE_EVENTBUS.txt">(View)</a>
</body>
</html>

View File

@ -29,7 +29,6 @@ import com.squareup.picasso.Picasso;
import org.apache.commons.lang3.StringUtils;
import de.danoeh.antennapod.BuildConfig;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.adapter.ChapterListAdapter;
import de.danoeh.antennapod.adapter.NavListAdapter;
@ -96,30 +95,25 @@ public class AudioplayerActivity extends MediaplayerActivity implements ItemDesc
FragmentTransaction fT = getSupportFragmentManager().beginTransaction();
if (coverFragment != null) {
if (BuildConfig.DEBUG)
Log.d(TAG, "Removing cover fragment");
Log.d(TAG, "Removing cover fragment");
fT.remove(coverFragment);
}
if (descriptionFragment != null) {
if (BuildConfig.DEBUG)
Log.d(TAG, "Removing description fragment");
Log.d(TAG, "Removing description fragment");
fT.remove(descriptionFragment);
}
if (chapterFragment != null) {
if (BuildConfig.DEBUG)
Log.d(TAG, "Removing chapter fragment");
Log.d(TAG, "Removing chapter fragment");
fT.remove(chapterFragment);
}
if (currentlyShownFragment != null) {
if (BuildConfig.DEBUG)
Log.d(TAG, "Removing currently shown fragment");
Log.d(TAG, "Removing currently shown fragment");
fT.remove(currentlyShownFragment);
}
for (int i = 0; i < detachedFragments.length; i++) {
Fragment f = detachedFragments[i];
if (f != null) {
if (BuildConfig.DEBUG)
Log.d(TAG, "Removing detached fragment");
Log.d(TAG, "Removing detached fragment");
fT.remove(f);
}
}
@ -135,11 +129,9 @@ public class AudioplayerActivity extends MediaplayerActivity implements ItemDesc
@Override
protected void onStop() {
super.onStop();
if (BuildConfig.DEBUG)
Log.d(TAG, "onStop");
Log.d(TAG, "onStop()");
cancelLoadTask();
EventDistributor.getInstance().unregister(contentUpdate);
}
@Override
@ -154,8 +146,7 @@ public class AudioplayerActivity extends MediaplayerActivity implements ItemDesc
}
private void savePreferences() {
if (BuildConfig.DEBUG)
Log.d(TAG, "Saving preferences");
Log.d(TAG, "Saving preferences");
SharedPreferences prefs = getSharedPreferences(PREFS, MODE_PRIVATE);
SharedPreferences.Editor editor = prefs.edit();
if (currentlyShownPosition >= 0 && controller != null
@ -182,9 +173,7 @@ public class AudioplayerActivity extends MediaplayerActivity implements ItemDesc
@Override
protected void onSaveInstanceState(Bundle outState) {
// super.onSaveInstanceState(outState); would cause crash
if (BuildConfig.DEBUG)
Log.d(TAG, "onSaveInstanceState");
Log.d(TAG, "onSaveInstanceState");
}
@Override
@ -207,8 +196,7 @@ public class AudioplayerActivity extends MediaplayerActivity implements ItemDesc
* @return true if restoreFromPrefernces changed the activity's state
*/
private boolean restoreFromPreferences() {
if (BuildConfig.DEBUG)
Log.d(TAG, "Restoring instance state");
Log.d(TAG, "Restoring instance state");
SharedPreferences prefs = getSharedPreferences(PREFS, MODE_PRIVATE);
int savedPosition = prefs.getInt(PREF_KEY_SELECTED_FRAGMENT_POSITION,
-1);
@ -222,15 +210,10 @@ public class AudioplayerActivity extends MediaplayerActivity implements ItemDesc
switchToFragment(savedPosition);
return true;
} else if (controller == null || controller.getMedia() == null) {
if (BuildConfig.DEBUG)
Log.d(TAG,
"Couldn't restore from preferences: controller or media was null");
Log.d(TAG, "Couldn't restore from preferences: controller or media was null");
} else {
if (BuildConfig.DEBUG)
Log.d(TAG,
"Couldn't restore from preferences: savedPosition was -1 or saved identifier and playable identifier didn't match.\nsavedPosition: "
+ savedPosition + ", id: " + playableId
);
Log.d(TAG, "Couldn't restore from preferences: savedPosition was -1 or saved identifier and playable identifier didn't match.\nsavedPosition: "
+ savedPosition + ", id: " + playableId);
}
return false;
@ -241,9 +224,7 @@ public class AudioplayerActivity extends MediaplayerActivity implements ItemDesc
super.onResume();
if (StringUtils.equals(getIntent().getAction(), Intent.ACTION_VIEW)) {
Intent intent = getIntent();
if (BuildConfig.DEBUG)
Log.d(TAG, "Received VIEW intent: "
+ intent.getData().getPath());
Log.d(TAG, "Received VIEW intent: " + intent.getData().getPath());
ExternalMedia media = new ExternalMedia(intent.getData().getPath(),
MediaType.AUDIO);
Intent launchIntent = new Intent(this, PlaybackService.class);
@ -271,8 +252,7 @@ public class AudioplayerActivity extends MediaplayerActivity implements ItemDesc
@Override
protected void onAwaitingVideoSurface() {
if (BuildConfig.DEBUG)
Log.d(TAG, "onAwaitingVideoSurface was called in audio player -> switching to video player");
Log.d(TAG, "onAwaitingVideoSurface was called in audio player -> switching to video player");
startActivity(new Intent(this, VideoplayerActivity.class));
}
@ -297,8 +277,7 @@ public class AudioplayerActivity extends MediaplayerActivity implements ItemDesc
* @param pos Must be POS_COVER, POS_DESCR, or POS_CHAPTERS
*/
private void switchToFragment(int pos) {
if (BuildConfig.DEBUG)
Log.d(TAG, "Switching contentView to position " + pos);
Log.d(TAG, "Switching contentView to position " + pos);
if (currentlyShownPosition != pos && controller != null) {
Playable media = controller.getMedia();
if (media != null) {
@ -356,9 +335,7 @@ public class AudioplayerActivity extends MediaplayerActivity implements ItemDesc
lastShownPosition = currentlyShownPosition;
currentlyShownPosition = pos;
if (detachedFragments[pos] != null) {
if (BuildConfig.DEBUG)
Log.d(TAG, "Reattaching fragment at position "
+ pos);
Log.d(TAG, "Reattaching fragment at position " + pos);
ft.attach(detachedFragments[pos]);
} else {
ft.add(R.id.contentView, currentlyShownFragment);
@ -632,9 +609,7 @@ public class AudioplayerActivity extends MediaplayerActivity implements ItemDesc
@Override
protected void onReloadNotification(int notificationCode) {
if (notificationCode == PlaybackService.EXTRA_CODE_VIDEO) {
if (BuildConfig.DEBUG)
Log.d(TAG,
"ReloadNotification received, switching to Videoplayer now");
Log.d(TAG, "ReloadNotification received, switching to Videoplayer now");
finish();
startActivity(new Intent(this, VideoplayerActivity.class));
@ -731,8 +706,7 @@ public class AudioplayerActivity extends MediaplayerActivity implements ItemDesc
@Override
public void update(EventDistributor eventDistributor, Integer arg) {
if ((EventDistributor.FEED_LIST_UPDATE & arg) != 0) {
if (BuildConfig.DEBUG)
Log.d(TAG, "Received contentUpdate Intent.");
Log.d(TAG, "Received contentUpdate Intent.");
loadData();
}
}

View File

@ -25,11 +25,11 @@ import org.apache.commons.lang3.Validate;
import java.util.List;
import de.danoeh.antennapod.BuildConfig;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.adapter.NavListAdapter;
import de.danoeh.antennapod.core.feed.EventDistributor;
import de.danoeh.antennapod.core.feed.Feed;
import de.danoeh.antennapod.core.feed.QueueEvent;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.storage.DBReader;
import de.danoeh.antennapod.core.util.StorageUtils;
@ -43,6 +43,7 @@ import de.danoeh.antennapod.fragment.PlaybackHistoryFragment;
import de.danoeh.antennapod.fragment.QueueFragment;
import de.danoeh.antennapod.menuhandler.NavDrawerActivity;
import de.danoeh.antennapod.preferences.PreferenceController;
import de.greenrobot.event.EventBus;
/**
* The activity that is shown when the user launches the app.
@ -52,8 +53,7 @@ public class MainActivity extends ActionBarActivity implements NavDrawerActivity
private static final int EVENTS = EventDistributor.DOWNLOAD_HANDLED
| EventDistributor.DOWNLOAD_QUEUED
| EventDistributor.FEED_LIST_UPDATE
| EventDistributor.UNREAD_ITEMS_UPDATE
| EventDistributor.QUEUE_UPDATE;
| EventDistributor.UNREAD_ITEMS_UPDATE;
public static final String PREF_NAME = "MainActivityPrefs";
public static final String PREF_IS_FIRST_LAUNCH = "prefMainActivityIsFirstLaunch";
@ -342,6 +342,13 @@ public class MainActivity extends ActionBarActivity implements NavDrawerActivity
}
@Override
public void onStart() {
super.onStart();
EventDistributor.getInstance().register(contentUpdate);
EventBus.getDefault().register(this);
}
@Override
protected void onPause() {
super.onPause();
@ -351,7 +358,6 @@ public class MainActivity extends ActionBarActivity implements NavDrawerActivity
protected void onResume() {
super.onResume();
StorageUtils.checkStorageAvailability(this);
EventDistributor.getInstance().register(contentUpdate);
Intent intent = getIntent();
if (navDrawerData != null && intent.hasExtra(EXTRA_NAV_INDEX) && intent.hasExtra(EXTRA_NAV_TYPE)) {
@ -366,6 +372,7 @@ public class MainActivity extends ActionBarActivity implements NavDrawerActivity
super.onStop();
cancelLoadTask();
EventDistributor.getInstance().unregister(contentUpdate);
EventBus.getDefault().unregister(this);
}
@Override
@ -454,13 +461,17 @@ public class MainActivity extends ActionBarActivity implements NavDrawerActivity
}
}
public void onEvent(QueueEvent event) {
Log.d(TAG, "onEvent(" + event + ")");
loadData();
}
private EventDistributor.EventListener contentUpdate = new EventDistributor.EventListener() {
@Override
public void update(EventDistributor eventDistributor, Integer arg) {
if ((EVENTS & arg) != 0) {
if (BuildConfig.DEBUG)
Log.d(TAG, "Received contentUpdate Intent.");
Log.d(TAG, "Received contentUpdate Intent.");
loadData();
}
}

View File

@ -12,13 +12,13 @@ import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.Window;
import android.view.View;
import android.widget.ImageButton;
import android.widget.SeekBar;
import android.widget.SeekBar.OnSeekBarChangeListener;
import android.widget.TextView;
import de.danoeh.antennapod.BuildConfig;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.core.feed.FeedItem;
import de.danoeh.antennapod.core.feed.FeedMedia;
@ -48,7 +48,9 @@ public abstract class MediaplayerActivity extends ActionBarActivity
protected SeekBar sbPosition;
protected ImageButton butPlay;
protected ImageButton butRev;
protected TextView txtvRev;
protected ImageButton butFF;
protected TextView txtvFF;
private PlaybackController newPlaybackController() {
return new PlaybackController(this, false) {
@ -167,8 +169,7 @@ public abstract class MediaplayerActivity extends ActionBarActivity
chooseTheme();
super.onCreate(savedInstanceState);
if (BuildConfig.DEBUG)
Log.d(TAG, "Creating Activity");
Log.d(TAG, "onCreate()");
StorageUtils.checkStorageAvailability(this);
setVolumeControlStream(AudioManager.STREAM_MUSIC);
@ -224,8 +225,7 @@ public abstract class MediaplayerActivity extends ActionBarActivity
@Override
protected void onStop() {
super.onStop();
if (BuildConfig.DEBUG)
Log.d(TAG, "Activity stopped");
Log.d(TAG, "onStop()");
if (controller != null) {
controller.release();
}
@ -234,8 +234,7 @@ public abstract class MediaplayerActivity extends ActionBarActivity
@Override
protected void onDestroy() {
super.onDestroy();
if (BuildConfig.DEBUG)
Log.d(TAG, "Activity destroyed");
Log.d(TAG, "onDestroy()");
}
@Override
@ -358,8 +357,7 @@ public abstract class MediaplayerActivity extends ActionBarActivity
@Override
protected void onResume() {
super.onResume();
if (BuildConfig.DEBUG)
Log.d(TAG, "Resuming Activity");
Log.d(TAG, "onResume()");
StorageUtils.checkStorageAvailability(this);
controller.init();
}
@ -378,6 +376,8 @@ public abstract class MediaplayerActivity extends ActionBarActivity
if (controller != null) {
int currentPosition = controller.getPosition();
int duration = controller.getDuration();
Log.d(TAG, "currentPosition " + Converter
.getDurationStringLong(currentPosition));
if (currentPosition != PlaybackService.INVALID_TIME
&& duration != PlaybackService.INVALID_TIME
&& controller.getMedia() != null) {
@ -386,15 +386,13 @@ public abstract class MediaplayerActivity extends ActionBarActivity
txtvLength.setText(Converter.getDurationStringLong(duration));
updateProgressbarPosition(currentPosition, duration);
} else {
Log.w(TAG,
"Could not react to position observer update because of invalid time");
Log.w(TAG, "Could not react to position observer update because of invalid time");
}
}
}
private void updateProgressbarPosition(int position, int duration) {
if (BuildConfig.DEBUG)
Log.d(TAG, "Updating progressbar info");
Log.d(TAG, "updateProgressbarPosition(" + position + ", " + duration +")");
float progress = ((float) position) / duration;
sbPosition.setProgress((int) (progress * sbPosition.getMax()));
}
@ -406,8 +404,7 @@ public abstract class MediaplayerActivity extends ActionBarActivity
* FeedMedia object.
*/
protected boolean loadMediaInfo() {
if (BuildConfig.DEBUG)
Log.d(TAG, "Loading media info");
Log.d(TAG, "loadMediaInfo()");
Playable media = controller.getMedia();
if (media != null) {
txtvPosition.setText(Converter.getDurationStringLong((media
@ -433,7 +430,11 @@ public abstract class MediaplayerActivity extends ActionBarActivity
txtvLength = (TextView) findViewById(R.id.txtvLength);
butPlay = (ImageButton) findViewById(R.id.butPlay);
butRev = (ImageButton) findViewById(R.id.butRev);
txtvRev = (TextView) findViewById(R.id.txtvRev);
txtvRev.setText(String.valueOf(UserPreferences.getRewindSecs()));
butFF = (ImageButton) findViewById(R.id.butFF);
txtvFF = (TextView) findViewById(R.id.txtvFF);
txtvFF.setText(String.valueOf(UserPreferences.getFastFowardSecs()));
// SEEKBAR SETUP
@ -444,10 +445,100 @@ public abstract class MediaplayerActivity extends ActionBarActivity
butPlay.setOnClickListener(controller.newOnPlayButtonClickListener());
if (butFF != null) {
butFF.setOnClickListener(controller.newOnFFButtonClickListener());
butFF.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
int curr = controller.getPosition();
controller.seekTo(curr + UserPreferences.getFastFowardSecs() * 1000);
}
});
butFF.setOnLongClickListener(new View.OnLongClickListener() {
int choice;
@Override
public boolean onLongClick(View v) {
int checked = 0;
int rewindSecs = UserPreferences.getFastFowardSecs();
final int[] values = getResources().getIntArray(R.array.seek_delta_values);
final String[] choices = new String[values.length];
for(int i=0; i < values.length; i++) {
if (rewindSecs == values[i]) {
checked = i;
}
choices[i] = String.valueOf(values[i]) + " "
+ getString(R.string.time_unit_seconds);
}
choice = values[checked];
AlertDialog.Builder builder = new AlertDialog.Builder(MediaplayerActivity.this);
builder.setTitle(R.string.pref_fast_forward);
builder.setSingleChoiceItems(choices, checked,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
choice = values[which];
}
});
builder.setNegativeButton(R.string.cancel_label, null);
builder.setPositiveButton(R.string.confirm_label, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
UserPreferences.setPrefFastForwardSecs(choice);
txtvFF.setText(String.valueOf(choice));
}
});
builder.create().show();
return true;
}
});
}
if (butRev != null) {
butRev.setOnClickListener(controller.newOnRevButtonClickListener());
butRev.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
int curr = controller.getPosition();
controller.seekTo(curr - UserPreferences.getRewindSecs() * 1000);
}
});
butRev.setOnLongClickListener(new View.OnLongClickListener() {
int choice;
@Override
public boolean onLongClick(View v) {
int checked = 0;
int rewindSecs = UserPreferences.getRewindSecs();
final int[] values = getResources().getIntArray(R.array.seek_delta_values);
final String[] choices = new String[values.length];
for(int i=0; i < values.length; i++) {
if (rewindSecs == values[i]) {
checked = i;
}
choices[i] = String.valueOf(values[i]) + " "
+ getString(R.string.time_unit_seconds);
}
choice = values[checked];
AlertDialog.Builder builder = new AlertDialog.Builder(MediaplayerActivity.this);
builder.setTitle(R.string.pref_rewind);
builder.setSingleChoiceItems(choices, checked,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
choice = values[which];
}
});
builder.setNegativeButton(R.string.cancel_label, null);
builder.setPositiveButton(R.string.confirm_label, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
UserPreferences.setPrefRewindSecs(choice);
txtvRev.setText(String.valueOf(choice));
}
});
builder.create().show();
return true;
}
});
}
}

View File

@ -11,6 +11,8 @@ import de.danoeh.antennapod.core.dialog.DownloadRequestErrorDialogCreator;
import de.danoeh.antennapod.core.feed.FeedItem;
import de.danoeh.antennapod.core.feed.FeedMedia;
import de.danoeh.antennapod.core.service.playback.PlaybackService;
import de.danoeh.antennapod.core.gpoddernet.model.GpodnetEpisodeAction;
import de.danoeh.antennapod.core.preferences.GpodnetPreferences;
import de.danoeh.antennapod.core.storage.DBTasks;
import de.danoeh.antennapod.core.storage.DBWriter;
import de.danoeh.antennapod.core.storage.DownloadRequestException;
@ -61,6 +63,19 @@ public class DefaultActionButtonCallback implements ActionButtonCallback {
} else {
if (!item.isRead()) {
DBWriter.markItemRead(context, item, true, true);
if(GpodnetPreferences.loggedIn()) {
// gpodder: send played action
FeedMedia media = item.getMedia();
GpodnetEpisodeAction action = new GpodnetEpisodeAction.Builder(item, GpodnetEpisodeAction.Action.PLAY)
.currentDeviceId()
.currentTimestamp()
.started(media.getDuration() / 1000)
.position(media.getDuration() / 1000)
.total(media.getDuration() / 1000)
.build();
GpodnetPreferences.enqueueEpisodeAction(action);
}
}
}
}

View File

@ -49,10 +49,11 @@ import de.danoeh.antennapod.menuhandler.NavDrawerActivity;
* Shows unread or recently published episodes
*/
public class AllEpisodesFragment extends Fragment {
private static final String TAG = "AllEpisodesFragment";
private static final int EVENTS = EventDistributor.DOWNLOAD_HANDLED |
EventDistributor.DOWNLOAD_QUEUED |
EventDistributor.QUEUE_UPDATE |
EventDistributor.UNREAD_ITEMS_UPDATE |
EventDistributor.PLAYER_STATUS_UPDATE;
@ -399,7 +400,7 @@ public class AllEpisodesFragment extends Fragment {
private ItemLoader itemLoader;
private void startItemLoader() {
protected void startItemLoader() {
if (itemLoader != null) {
itemLoader.cancel(true);
}
@ -429,9 +430,10 @@ public class AllEpisodesFragment extends Fragment {
protected Object[] doInBackground(Void... params) {
Context context = activity.get();
if (context != null) {
return new Object[]{DBReader.getUnreadItemsList(context),
return new Object[]{
DBReader.getUnreadItemsList(context),
DBReader.getRecentlyPublishedEpisodes(context, RECENT_EPISODES_LIMIT),
QueueAccess.IDListAccess(DBReader.getQueueIDList(context))};
DBReader.getQueueIDList(context)};
} else {
return null;
}

View File

@ -8,6 +8,7 @@ import android.support.v4.app.ListFragment;
import android.view.View;
import android.widget.ListView;
import java.util.Collections;
import java.util.List;
import de.danoeh.antennapod.R;
@ -17,7 +18,6 @@ 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.storage.DBWriter;
import de.danoeh.antennapod.core.util.QueueAccess;
/**
* Displays all running downloads and provides a button to delete them
@ -26,11 +26,9 @@ public class CompletedDownloadsFragment extends ListFragment {
private static final int EVENTS =
EventDistributor.DOWNLOAD_HANDLED |
EventDistributor.DOWNLOADLOG_UPDATE |
EventDistributor.QUEUE_UPDATE |
EventDistributor.UNREAD_ITEMS_UPDATE;
private List<FeedItem> items;
private QueueAccess queue;
private DownloadedEpisodesListAdapter listAdapter;
private boolean viewCreated = false;
@ -155,7 +153,7 @@ public class CompletedDownloadsFragment extends ListFragment {
}
}
private class ItemLoader extends AsyncTask<Void, Void, Object[]> {
private class ItemLoader extends AsyncTask<Void, Void, List<FeedItem>> {
@Override
protected void onPreExecute() {
@ -166,11 +164,10 @@ public class CompletedDownloadsFragment extends ListFragment {
}
@Override
protected void onPostExecute(Object[] results) {
protected void onPostExecute(List<FeedItem> results) {
super.onPostExecute(results);
if (results != null) {
items = (List<FeedItem>) results[0];
queue = (QueueAccess) results[1];
items = results;
itemsLoaded = true;
if (viewCreated && getActivity() != null) {
onFragmentLoaded();
@ -179,13 +176,12 @@ public class CompletedDownloadsFragment extends ListFragment {
}
@Override
protected Object[] doInBackground(Void... params) {
protected List<FeedItem> doInBackground(Void... params) {
Context context = getActivity();
if (context != null) {
return new Object[]{DBReader.getDownloadedItems(context),
QueueAccess.IDListAccess(DBReader.getQueueIDList(context))};
return DBReader.getDownloadedItems(context);
}
return null;
return Collections.emptyList();
}
}
}

View File

@ -13,7 +13,6 @@ import android.widget.TextView;
import com.squareup.picasso.Picasso;
import de.danoeh.antennapod.BuildConfig;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.core.service.playback.PlaybackService;
import de.danoeh.antennapod.core.util.Converter;
@ -54,8 +53,7 @@ public class ExternalPlayerFragment extends Fragment {
@Override
public void onClick(View v) {
if (BuildConfig.DEBUG)
Log.d(TAG, "layoutInfo was clicked");
Log.d(TAG, "layoutInfo was clicked");
if (controller.getMedia() != null) {
startActivity(PlaybackService.getPlayerActivityIntent(
@ -90,14 +88,10 @@ public class ExternalPlayerFragment extends Fragment {
@Override
public void onBufferStart() {
// TODO Auto-generated method stub
}
@Override
public void onBufferEnd() {
// TODO Auto-generated method stub
}
@Override
@ -153,7 +147,6 @@ public class ExternalPlayerFragment extends Fragment {
butPlay.setOnClickListener(controller
.newOnPlayButtonClickListener());
}
}
@Override
@ -170,8 +163,6 @@ public class ExternalPlayerFragment extends Fragment {
@Override
public void onPlaybackSpeedChange() {
// TODO Auto-generated method stub
}
};
}
@ -185,8 +176,7 @@ public class ExternalPlayerFragment extends Fragment {
@Override
public void onDestroy() {
super.onDestroy();
if (BuildConfig.DEBUG)
Log.d(TAG, "Fragment is about to be destroyed");
Log.d(TAG, "Fragment is about to be destroyed");
if (controller != null) {
controller.release();
}
@ -201,8 +191,7 @@ public class ExternalPlayerFragment extends Fragment {
}
private boolean loadMediaInfo() {
if (BuildConfig.DEBUG)
Log.d(TAG, "Loading media info");
Log.d(TAG, "Loading media info");
if (controller.serviceAvailable()) {
Playable media = controller.getMedia();
if (media != null) {
@ -221,13 +210,11 @@ public class ExternalPlayerFragment extends Fragment {
}
return true;
} else {
Log.w(TAG,
"loadMediaInfo was called while the media object of playbackService was null!");
Log.w(TAG, "loadMediaInfo was called while the media object of playbackService was null!");
return false;
}
} else {
Log.w(TAG,
"loadMediaInfo was called while playbackService was null!");
Log.w(TAG, "loadMediaInfo was called while playbackService was null!");
return false;
}
}

View File

@ -17,6 +17,7 @@ import android.support.v7.widget.PopupMenu;
import android.support.v7.widget.Toolbar;
import android.text.TextUtils;
import android.text.format.DateUtils;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
@ -43,6 +44,7 @@ import de.danoeh.antennapod.core.asynctask.DownloadObserver;
import de.danoeh.antennapod.core.feed.EventDistributor;
import de.danoeh.antennapod.core.feed.FeedItem;
import de.danoeh.antennapod.core.feed.FeedMedia;
import de.danoeh.antennapod.core.feed.QueueEvent;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.service.download.Downloader;
import de.danoeh.antennapod.core.storage.DBReader;
@ -51,18 +53,20 @@ 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.util.Converter;
import de.danoeh.antennapod.core.util.QueueAccess;
import de.danoeh.antennapod.core.util.LongList;
import de.danoeh.antennapod.core.util.playback.Timeline;
import de.danoeh.antennapod.menuhandler.FeedItemMenuHandler;
import de.greenrobot.event.EventBus;
/**
* Displays information about a FeedItem and actions.
*/
public class ItemFragment extends Fragment implements LoaderManager.LoaderCallbacks<Pair<FeedItem, QueueAccess>> {
public class ItemFragment extends Fragment implements LoaderManager.LoaderCallbacks<Pair<FeedItem, LongList>> {
private static final String TAG = "ItemFragment";
private static final int EVENTS = EventDistributor.DOWNLOAD_HANDLED |
EventDistributor.DOWNLOAD_QUEUED |
EventDistributor.QUEUE_UPDATE |
EventDistributor.UNREAD_ITEMS_UPDATE;
private static final String ARG_FEEDITEM = "feeditem";
@ -84,7 +88,7 @@ public class ItemFragment extends Fragment implements LoaderManager.LoaderCallba
private boolean itemsLoaded = false;
private long itemID;
private FeedItem item;
private QueueAccess queue;
private LongList queue;
private String webviewData;
private DownloadObserver downloadObserver;
private List<Downloader> downloaderList;
@ -124,6 +128,7 @@ public class ItemFragment extends Fragment implements LoaderManager.LoaderCallba
public void onStart() {
super.onStart();
EventDistributor.getInstance().register(contentUpdate);
EventBus.getDefault().register(this);
if (downloadObserver != null) {
downloadObserver.setActivity(getActivity());
downloadObserver.onResume();
@ -138,6 +143,7 @@ public class ItemFragment extends Fragment implements LoaderManager.LoaderCallba
public void onStop() {
super.onStop();
EventDistributor.getInstance().unregister(contentUpdate);
EventBus.getDefault().unregister(this);
}
private void resetViewState() {
@ -387,25 +393,29 @@ public class ItemFragment extends Fragment implements LoaderManager.LoaderCallba
}
}
public void onEvent(QueueEvent event) {
Log.d(TAG, "onEvent(" + event + ")");
getLoaderManager().restartLoader(0, null, ItemFragment.this);
}
@Override
public Loader<Pair<FeedItem, QueueAccess>> onCreateLoader(int id, Bundle args) {
return new DBTaskLoader<Pair<FeedItem, QueueAccess>>(getActivity()) {
public Loader<Pair<FeedItem,LongList>> onCreateLoader(int id, Bundle args) {
return new DBTaskLoader<Pair<FeedItem,LongList>>(getActivity()) {
@Override
public Pair<FeedItem, QueueAccess> loadInBackground() {
public Pair<FeedItem,LongList> loadInBackground() {
FeedItem data1 = DBReader.getFeedItem(getContext(), itemID);
if (data1 != null) {
Timeline t = new Timeline(getActivity(), data1);
webviewData = t.processShownotes(false);
}
QueueAccess data2 = QueueAccess.IDListAccess(DBReader.getQueueIDList(getContext()));
LongList data2 = DBReader.getQueueIDList(getContext());
return Pair.create(data1, data2);
}
};
}
@Override
public void onLoadFinished(Loader<Pair<FeedItem, QueueAccess>> loader, Pair<FeedItem, QueueAccess> data) {
public void onLoadFinished(Loader<Pair<FeedItem,LongList>> loader, Pair<FeedItem,LongList> data) {
if (data != null) {
item = data.first;
@ -420,8 +430,7 @@ public class ItemFragment extends Fragment implements LoaderManager.LoaderCallba
}
@Override
public void onLoaderReset(Loader<Pair<FeedItem, QueueAccess>> loader) {
public void onLoaderReset(Loader<Pair<FeedItem,LongList>> loader) {
}
private EventDistributor.EventListener contentUpdate = new EventDistributor.EventListener() {

View File

@ -9,6 +9,7 @@ import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.support.v4.app.ListFragment;
import android.support.v4.util.Pair;
import android.support.v7.app.ActionBarActivity;
import android.support.v7.widget.SearchView;
import android.util.Log;
@ -29,7 +30,6 @@ import org.apache.commons.lang3.Validate;
import java.util.List;
import de.danoeh.antennapod.BuildConfig;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.FeedInfoActivity;
import de.danoeh.antennapod.activity.MainActivity;
@ -44,17 +44,19 @@ import de.danoeh.antennapod.core.feed.EventDistributor;
import de.danoeh.antennapod.core.feed.Feed;
import de.danoeh.antennapod.core.feed.FeedItem;
import de.danoeh.antennapod.core.feed.FeedMedia;
import de.danoeh.antennapod.core.feed.QueueEvent;
import de.danoeh.antennapod.core.service.download.DownloadService;
import de.danoeh.antennapod.core.service.download.Downloader;
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.QueueAccess;
import de.danoeh.antennapod.core.util.LongList;
import de.danoeh.antennapod.core.util.gui.MoreContentListFooterUtil;
import de.danoeh.antennapod.menuhandler.FeedMenuHandler;
import de.danoeh.antennapod.menuhandler.MenuItemUtils;
import de.danoeh.antennapod.menuhandler.NavDrawerActivity;
import de.greenrobot.event.EventBus;
/**
* Displays a list of FeedItems.
@ -65,7 +67,6 @@ public class ItemlistFragment extends ListFragment {
private static final int EVENTS = EventDistributor.DOWNLOAD_HANDLED
| EventDistributor.DOWNLOAD_QUEUED
| EventDistributor.QUEUE_UPDATE
| EventDistributor.UNREAD_ITEMS_UPDATE
| EventDistributor.PLAYER_STATUS_UPDATE;
@ -76,7 +77,7 @@ public class ItemlistFragment extends ListFragment {
private long feedID;
private Feed feed;
protected QueueAccess queue;
private LongList queue;
private boolean itemsLoaded = false;
private boolean viewsCreated = false;
@ -118,6 +119,7 @@ public class ItemlistFragment extends ListFragment {
public void onStart() {
super.onStart();
EventDistributor.getInstance().register(contentUpdate);
EventBus.getDefault().register(this);
if (downloadObserver != null) {
downloadObserver.setActivity(getActivity());
downloadObserver.onResume();
@ -131,6 +133,7 @@ public class ItemlistFragment extends ListFragment {
public void onStop() {
super.onStop();
EventDistributor.getInstance().unregister(contentUpdate);
EventBus.getDefault().unregister(this);
stopItemLoader();
}
@ -283,13 +286,17 @@ public class ItemlistFragment extends ListFragment {
}
}
public void onEvent(QueueEvent event) {
Log.d(TAG, "onEvent(" + event + ")");
startItemLoader();
}
private EventDistributor.EventListener contentUpdate = new EventDistributor.EventListener() {
@Override
public void update(EventDistributor eventDistributor, Integer arg) {
if ((EVENTS & arg) != 0) {
if (BuildConfig.DEBUG)
Log.d(TAG, "Received contentUpdate Intent.");
Log.d(TAG, "Received contentUpdate Intent.");
if ((EventDistributor.DOWNLOAD_QUEUED & arg) != 0) {
updateProgressBarVisibility();
} else {
@ -474,25 +481,26 @@ public class ItemlistFragment extends ListFragment {
}
}
private class ItemLoader extends AsyncTask<Long, Void, Object[]> {
private class ItemLoader extends AsyncTask<Long, Void, Pair<Feed,LongList>> {
@Override
protected Object[] doInBackground(Long... params) {
protected Pair<Feed,LongList> doInBackground(Long... params) {
long feedID = params[0];
Context context = getActivity();
if (context != null) {
return new Object[]{DBReader.getFeed(context, feedID),
QueueAccess.IDListAccess(DBReader.getQueueIDList(context))};
Feed feed = DBReader.getFeed(context, feedID);
LongList queue = DBReader.getQueueIDList(context);
return Pair.create(feed, queue);
} else {
return null;
}
}
@Override
protected void onPostExecute(Object[] res) {
protected void onPostExecute(Pair<Feed,LongList> res) {
super.onPostExecute(res);
if (res != null) {
feed = (Feed) res[0];
queue = (QueueAccess) res[1];
feed = res.first;
queue = res.second;
itemsLoaded = true;
if (viewsCreated) {
onFragmentLoaded();

View File

@ -1,7 +1,7 @@
package de.danoeh.antennapod.fragment;
import android.content.Context;
import android.os.Bundle;
import android.os.Parcelable;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
@ -11,17 +11,25 @@ import com.mobeta.android.dslv.DragSortListView;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.core.feed.FeedItem;
import de.danoeh.antennapod.core.feed.FeedMedia;
import de.danoeh.antennapod.core.feed.QueueEvent;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.storage.DBReader;
import de.danoeh.antennapod.core.storage.DBWriter;
import de.danoeh.antennapod.core.util.gui.FeedItemUndoToken;
import de.danoeh.antennapod.core.util.gui.UndoBarController;
import de.greenrobot.event.EventBus;
/**
* Like 'EpisodesFragment' except that it only shows new episodes and
* supports swiping to mark as read.
*/
public class NewEpisodesFragment extends AllEpisodesFragment {
private static final String TAG = "NewEpisodesFragment";
private static final String PREF_NAME = "PrefNewEpisodesFragment";
private UndoBarController undoBarController;
@ -30,6 +38,25 @@ public class NewEpisodesFragment extends AllEpisodesFragment {
super(true, PREF_NAME);
}
public void onEvent(QueueEvent event) {
Log.d(TAG, "onEvent(" + event + ")");
if(event.action == QueueEvent.Action.ADDED) {
startItemLoader();
}
}
@Override
public void onStart() {
super.onStart();
EventBus.getDefault().register(this);
}
@Override
public void onStop() {
super.onStop();
EventBus.getDefault().unregister(this);
}
@Override
protected void resetViewState() {
super.resetViewState();
@ -55,18 +82,30 @@ public class NewEpisodesFragment extends AllEpisodesFragment {
}
});
undoBarController = new UndoBarController(root.findViewById(R.id.undobar), new UndoBarController.UndoListener() {
undoBarController = new UndoBarController<FeedItemUndoToken>(root.findViewById(R.id.undobar), new UndoBarController.UndoListener<FeedItemUndoToken>() {
private final Context context = getActivity();
@Override
public void onUndo(Parcelable token) {
// Perform the undo
FeedItemUndoToken undoToken = (FeedItemUndoToken) token;
public void onUndo(FeedItemUndoToken token) {
if (token != null) {
long itemId = undoToken.getFeedItemId();
int position = undoToken.getPosition();
DBWriter.markItemRead(getActivity(), itemId, false);
long itemId = token.getFeedItemId();
DBWriter.markItemRead(context, itemId, false);
}
}
@Override
public void onHide(FeedItemUndoToken token) {
if (token != null && context != null) {
long itemId = token.getFeedItemId();
FeedItem item = DBReader.getFeedItem(context, itemId);
FeedMedia media = item.getMedia();
if(media != null && media.hasAlmostEnded() && UserPreferences.isAutoDelete()) {
DBWriter.deleteFeedMediaOfItem(context, media.getId());
}
}
}
});
return root;
}
}

View File

@ -7,7 +7,9 @@ import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Handler;
import android.support.v4.app.ListFragment;
import android.support.v4.util.Pair;
import android.support.v4.view.MenuItemCompat;
import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
@ -25,12 +27,14 @@ import de.danoeh.antennapod.core.asynctask.DownloadObserver;
import de.danoeh.antennapod.core.feed.EventDistributor;
import de.danoeh.antennapod.core.feed.FeedItem;
import de.danoeh.antennapod.core.feed.FeedMedia;
import de.danoeh.antennapod.core.feed.QueueEvent;
import de.danoeh.antennapod.core.service.download.Downloader;
import de.danoeh.antennapod.core.storage.DBReader;
import de.danoeh.antennapod.core.storage.DBWriter;
import de.danoeh.antennapod.core.util.QueueAccess;
import de.danoeh.antennapod.core.util.LongList;
import de.danoeh.antennapod.menuhandler.MenuItemUtils;
import de.danoeh.antennapod.menuhandler.NavDrawerActivity;
import de.greenrobot.event.EventBus;
public class PlaybackHistoryFragment extends ListFragment {
private static final String TAG = "PlaybackHistoryFragment";
@ -38,7 +42,7 @@ public class PlaybackHistoryFragment extends ListFragment {
EventDistributor.PLAYER_STATUS_UPDATE;
private List<FeedItem> playbackHistory;
private QueueAccess queue;
private LongList queue;
private FeedItemlistAdapter adapter;
private boolean itemsLoaded = false;
@ -66,12 +70,14 @@ public class PlaybackHistoryFragment extends ListFragment {
public void onStart() {
super.onStart();
EventDistributor.getInstance().register(contentUpdate);
EventBus.getDefault().register(this);
}
@Override
public void onStop() {
super.onStop();
EventDistributor.getInstance().unregister(contentUpdate);
EventBus.getDefault().unregister(this);
stopItemLoader();
}
@ -165,6 +171,11 @@ public class PlaybackHistoryFragment extends ListFragment {
}
}
public void onEvent(QueueEvent event) {
Log.d(TAG, "onEvent(" + event + ")");
startItemLoader();
}
private EventDistributor.EventListener contentUpdate = new EventDistributor.EventListener() {
@Override
@ -251,27 +262,27 @@ public class PlaybackHistoryFragment extends ListFragment {
}
}
private class ItemLoader extends AsyncTask<Void, Void, Object[]> {
private class ItemLoader extends AsyncTask<Void, Void, Pair<List<FeedItem>,LongList>> {
@Override
protected Object[] doInBackground(Void... params) {
protected Pair<List<FeedItem>,LongList> doInBackground(Void... params) {
Context context = activity.get();
if (context != null) {
List<FeedItem> ph = DBReader.getPlaybackHistory(context);
DBReader.loadFeedDataOfFeedItemlist(context, ph);
return new Object[]{ph,
QueueAccess.IDListAccess(DBReader.getQueueIDList(context))};
List<FeedItem> history = DBReader.getPlaybackHistory(context);
LongList queue = DBReader.getQueueIDList(context);
DBReader.loadFeedDataOfFeedItemlist(context, history);
return Pair.create(history, queue);
} else {
return null;
}
}
@Override
protected void onPostExecute(Object[] res) {
protected void onPostExecute(Pair<List<FeedItem>,LongList> res) {
super.onPostExecute(res);
if (res != null) {
playbackHistory = (List<FeedItem>) res[0];
queue = (QueueAccess) res[1];
playbackHistory = res.first;
queue = res.second;
itemsLoaded = true;
if (viewsCreated) {
onFragmentLoaded();

View File

@ -7,7 +7,6 @@ import android.content.SharedPreferences;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Handler;
import android.os.Parcelable;
import android.support.v4.app.Fragment;
import android.support.v7.widget.SearchView;
import android.util.Log;
@ -37,6 +36,8 @@ import de.danoeh.antennapod.core.feed.EventDistributor;
import de.danoeh.antennapod.core.feed.Feed;
import de.danoeh.antennapod.core.feed.FeedItem;
import de.danoeh.antennapod.core.feed.FeedMedia;
import de.danoeh.antennapod.core.feed.QueueEvent;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.service.download.DownloadService;
import de.danoeh.antennapod.core.service.download.Downloader;
import de.danoeh.antennapod.core.storage.DBReader;
@ -48,6 +49,7 @@ import de.danoeh.antennapod.core.util.gui.FeedItemUndoToken;
import de.danoeh.antennapod.core.util.gui.UndoBarController;
import de.danoeh.antennapod.menuhandler.MenuItemUtils;
import de.danoeh.antennapod.menuhandler.NavDrawerActivity;
import de.greenrobot.event.EventBus;
/**
* Shows all items in the queue
@ -56,7 +58,6 @@ public class QueueFragment extends Fragment {
private static final String TAG = "QueueFragment";
private static final int EVENTS = EventDistributor.DOWNLOAD_HANDLED |
EventDistributor.DOWNLOAD_QUEUED |
EventDistributor.QUEUE_UPDATE |
EventDistributor.PLAYER_STATUS_UPDATE;
private DragSortListView listView;
@ -64,7 +65,7 @@ public class QueueFragment extends Fragment {
private TextView txtvEmpty;
private ProgressBar progLoading;
private UndoBarController undoBarController;
private UndoBarController<FeedItemUndoToken> undoBarController;
private List<FeedItem> queue;
private List<Downloader> downloaderList;
@ -104,6 +105,7 @@ public class QueueFragment extends Fragment {
public void onStart() {
super.onStart();
EventDistributor.getInstance().register(contentUpdate);
EventBus.getDefault().register(this);
this.activity.set((MainActivity) getActivity());
if (downloadObserver != null) {
downloadObserver.setActivity(getActivity());
@ -124,7 +126,11 @@ public class QueueFragment extends Fragment {
public void onStop() {
super.onStop();
EventDistributor.getInstance().unregister(contentUpdate);
EventBus.getDefault().unregister(this);
stopItemLoader();
if(undoBarController.isShowing()) {
undoBarController.close();
}
}
@Override
@ -133,6 +139,15 @@ public class QueueFragment extends Fragment {
this.activity.set((MainActivity) activity);
}
public void onEventMainThread(QueueEvent event) {
Log.d(TAG, "onEvent(" + event + ")");
if(event.action == QueueEvent.Action.REMOVED) {
undoBarController.showUndoBar(false, getString(R.string.removed_from_queue),
new FeedItemUndoToken(event.item, event.position));
}
startItemLoader();
}
private void saveScrollPosition() {
SharedPreferences prefs = getActivity().getSharedPreferences(PREFS, Context.MODE_PRIVATE);
SharedPreferences.Editor editor = prefs.edit();
@ -295,7 +310,7 @@ public class QueueFragment extends Fragment {
DBWriter.moveQueueItemToBottom(getActivity(), selectedItem.getId(), true);
return true;
case R.id.remove_from_queue_item:
DBWriter.removeQueueItem(getActivity(), selectedItem.getId(), false);
DBWriter.removeQueueItem(getActivity(), selectedItem, false);
return true;
default:
return super.onContextItemSelected(item);
@ -343,29 +358,42 @@ public class QueueFragment extends Fragment {
@Override
public void remove(int which) {
Log.d(TAG, "remove("+which+")");
Log.d(TAG, "remove(" + which + ")");
stopItemLoader();
FeedItem item = (FeedItem) listView.getAdapter().getItem(which);
DBWriter.removeQueueItem(getActivity(), item.getId(), true);
undoBarController.showUndoBar(false,
getString(R.string.removed_from_queue), new FeedItemUndoToken(item,
which)
);
DBWriter.markItemRead(getActivity(), item.getId(), true);
DBWriter.removeQueueItem(getActivity(), item, true);
}
});
undoBarController = new UndoBarController(root.findViewById(R.id.undobar), new UndoBarController.UndoListener() {
@Override
public void onUndo(Parcelable token) {
// Perform the undo
FeedItemUndoToken undoToken = (FeedItemUndoToken) token;
if (token != null) {
long itemId = undoToken.getFeedItemId();
int position = undoToken.getPosition();
DBWriter.addQueueItemAt(getActivity(), itemId, position, false);
}
}
});
undoBarController = new UndoBarController<FeedItemUndoToken>(root.findViewById(R.id.undobar),
new UndoBarController.UndoListener<FeedItemUndoToken>() {
private final Context context = getActivity();
@Override
public void onUndo(FeedItemUndoToken token) {
if (token != null) {
long itemId = token.getFeedItemId();
int position = token.getPosition();
DBWriter.markItemRead(context, itemId, false);
DBWriter.addQueueItemAt(context, itemId, position, false);
}
}
@Override
public void onHide(FeedItemUndoToken token) {
if (token != null && context != null) {
long itemId = token.getFeedItemId();
FeedItem item = DBReader.getFeedItem(context, itemId);
FeedMedia media = item.getMedia();
if(media != null && media.hasAlmostEnded() && UserPreferences.isAutoDelete()) {
DBWriter.deleteFeedMediaOfItem(context, media.getId());
}
}
}
});
registerForContextMenu(listView);

View File

@ -13,6 +13,7 @@ import android.view.MenuItem;
import android.view.View;
import android.widget.ListView;
import java.util.Collections;
import java.util.List;
import de.danoeh.antennapod.R;
@ -23,9 +24,7 @@ import de.danoeh.antennapod.core.feed.Feed;
import de.danoeh.antennapod.core.feed.FeedComponent;
import de.danoeh.antennapod.core.feed.FeedItem;
import de.danoeh.antennapod.core.feed.SearchResult;
import de.danoeh.antennapod.core.storage.DBReader;
import de.danoeh.antennapod.core.storage.FeedSearcher;
import de.danoeh.antennapod.core.util.QueueAccess;
import de.danoeh.antennapod.menuhandler.MenuItemUtils;
import de.danoeh.antennapod.menuhandler.NavDrawerActivity;
@ -44,8 +43,6 @@ public class SearchFragment extends ListFragment {
private boolean viewCreated = false;
private boolean itemsLoaded = false;
private QueueAccess queue;
/**
* Create a new SearchFragment that searches all feeds.
*/
@ -165,8 +162,7 @@ public class SearchFragment extends ListFragment {
@Override
public void update(EventDistributor eventDistributor, Integer arg) {
if ((arg & (EventDistributor.UNREAD_ITEMS_UPDATE
| EventDistributor.DOWNLOAD_HANDLED
| EventDistributor.QUEUE_UPDATE)) != 0) {
| EventDistributor.DOWNLOAD_HANDLED)) != 0) {
startSearchTask();
}
}
@ -209,17 +205,16 @@ public class SearchFragment extends ListFragment {
}
}
private class SearchTask extends AsyncTask<Bundle, Void, Object[]> {
private class SearchTask extends AsyncTask<Bundle, Void, List<SearchResult>> {
@Override
protected Object[] doInBackground(Bundle... params) {
protected List<SearchResult> doInBackground(Bundle... params) {
String query = params[0].getString(ARG_QUERY);
long feed = params[0].getLong(ARG_FEED);
Context context = getActivity();
if (context != null) {
return new Object[]{FeedSearcher.performSearch(context, query, feed),
QueueAccess.IDListAccess(DBReader.getQueueIDList(context))};
return FeedSearcher.performSearch(context, query, feed);
} else {
return null;
return Collections.emptyList();
}
}
@ -232,12 +227,11 @@ public class SearchFragment extends ListFragment {
}
@Override
protected void onPostExecute(Object[] results) {
protected void onPostExecute(List<SearchResult> results) {
super.onPostExecute(results);
if (results != null) {
itemsLoaded = true;
searchResults = (List<SearchResult>) results[0];
queue = (QueueAccess) results[1];
searchResults = results;
if (viewCreated) {
onFragmentLoaded();
}

View File

@ -4,15 +4,19 @@ import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import de.danoeh.antennapod.core.BuildConfig;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.core.BuildConfig;
import de.danoeh.antennapod.core.feed.FeedItem;
import de.danoeh.antennapod.core.feed.FeedMedia;
import de.danoeh.antennapod.core.gpoddernet.model.GpodnetEpisodeAction;
import de.danoeh.antennapod.core.gpoddernet.model.GpodnetEpisodeAction.Action;
import de.danoeh.antennapod.core.preferences.GpodnetPreferences;
import de.danoeh.antennapod.core.service.playback.PlaybackService;
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.util.QueueAccess;
import de.danoeh.antennapod.core.util.LongList;
import de.danoeh.antennapod.core.util.ShareUtils;
/**
@ -52,7 +56,7 @@ public class FeedItemMenuHandler {
* @return Returns true if selectedItem is not null.
*/
public static boolean onPrepareMenu(MenuInterface mi,
FeedItem selectedItem, boolean showExtendedMenu, QueueAccess queueAccess) {
FeedItem selectedItem, boolean showExtendedMenu, LongList queueAccess) {
if (selectedItem == null) {
return false;
}
@ -122,7 +126,7 @@ public class FeedItemMenuHandler {
* @return true if selectedItem is not null.
*/
public static boolean onPrepareMenu(MenuInterface mi,
FeedItem selectedItem, boolean showExtendedMenu, QueueAccess queueAccess, int... excludeIds) {
FeedItem selectedItem, boolean showExtendedMenu, LongList queueAccess, int... excludeIds) {
boolean rc = onPrepareMenu(mi, selectedItem, showExtendedMenu, queueAccess);
if (rc && excludeIds != null) {
for (int id : excludeIds) {
@ -156,15 +160,33 @@ public class FeedItemMenuHandler {
break;
case R.id.mark_read_item:
DBWriter.markItemRead(context, selectedItem, true, true);
if(GpodnetPreferences.loggedIn()) {
FeedMedia media = selectedItem.getMedia();
GpodnetEpisodeAction actionPlay = new GpodnetEpisodeAction.Builder(selectedItem, Action.PLAY)
.currentDeviceId()
.currentTimestamp()
.started(media.getDuration() / 1000)
.position(media.getDuration() / 1000)
.total(media.getDuration() / 1000)
.build();
GpodnetPreferences.enqueueEpisodeAction(actionPlay);
}
break;
case R.id.mark_unread_item:
DBWriter.markItemRead(context, selectedItem, false, true);
if(GpodnetPreferences.loggedIn()) {
GpodnetEpisodeAction actionNew = new GpodnetEpisodeAction.Builder(selectedItem, Action.NEW)
.currentDeviceId()
.currentTimestamp()
.build();
GpodnetPreferences.enqueueEpisodeAction(actionNew);
}
break;
case R.id.add_to_queue_item:
DBWriter.addQueueItem(context, selectedItem.getId());
break;
case R.id.remove_from_queue_item:
DBWriter.removeQueueItem(context, selectedItem.getId(), true);
DBWriter.removeQueueItem(context, selectedItem, true);
break;
case R.id.stream_item:
DBTasks.playMedia(context, selectedItem.getMedia(), true, true,

View File

@ -343,6 +343,7 @@ public class PreferenceController {
}
});
buildUpdateIntervalPreference();
buildSmartMarkAsPlayedPreference();
buildAutodownloadSelectedNetworsPreference();
setSelectedNetworksEnabled(UserPreferences
.isEnableAutodownloadWifiFilter());
@ -403,6 +404,24 @@ public class PreferenceController {
}
private void buildSmartMarkAsPlayedPreference() {
final Resources res = ui.getActivity().getResources();
ListPreference pref = (ListPreference) ui.findPreference(UserPreferences.PREF_SMART_MARK_AS_PLAYED_SECS);
String[] values = res.getStringArray(
R.array.smart_mark_as_played_values);
String[] entries = new String[values.length];
for (int x = 0; x < values.length; x++) {
if(x == 0) {
entries[x] = res.getString(R.string.pref_smart_mark_as_played_disabled);
} else {
Integer v = Integer.parseInt(values[x]);
entries[x] = v + " " + res.getString(R.string.time_unit_seconds);
}
}
pref.setEntries(entries);
}
private void setSelectedNetworksEnabled(boolean b) {
if (selectedNetworks != null) {
for (Preference p : selectedNetworks) {
@ -430,7 +449,6 @@ public class PreferenceController {
.setEnabled(UserPreferences.isEnableAutodownload());
}
private void setParallelDownloadsText(int downloads) {
final Resources res = ui.getActivity().getResources();

View File

@ -12,14 +12,18 @@ import android.util.Log;
import android.view.KeyEvent;
import android.view.View;
import android.widget.RemoteViews;
import de.danoeh.antennapod.core.BuildConfig;
import de.danoeh.antennapod.R;
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.receiver.MediaButtonReceiver;
import de.danoeh.antennapod.receiver.PlayerWidget;
import de.danoeh.antennapod.core.service.playback.PlaybackService;
import de.danoeh.antennapod.core.service.playback.PlayerStatus;
import de.danoeh.antennapod.core.storage.DBWriter;
import de.danoeh.antennapod.core.util.Converter;
import de.danoeh.antennapod.core.util.playback.Playable;
import de.danoeh.antennapod.receiver.PlayerWidget;
/** Updates the state of the player widget */
public class PlayerWidgetService extends Service {
@ -39,8 +43,7 @@ public class PlayerWidgetService extends Service {
@Override
public void onCreate() {
super.onCreate();
if (BuildConfig.DEBUG)
Log.d(TAG, "Service created");
Log.d(TAG, "Service created");
isUpdating = false;
psLock = new Object();
}
@ -48,8 +51,24 @@ public class PlayerWidgetService extends Service {
@Override
public void onDestroy() {
super.onDestroy();
if (BuildConfig.DEBUG)
Log.d(TAG, "Service is about to be destroyed");
Log.d(TAG, "Service is about to be destroyed");
Playable playable = playbackService.getPlayable();
if(playable != null && playable instanceof FeedMedia) {
FeedMedia media = (FeedMedia) playable;
if(media.hasAlmostEnded()) {
Log.d(TAG, "smart mark as read");
FeedItem item = media.getItem();
DBWriter.markItemRead(this, item, true, false);
DBWriter.removeQueueItem(this, item, false);
DBWriter.addItemToPlaybackHistory(this, media);
if (UserPreferences.isAutoDelete()) {
Log.d(TAG, "Delete " + media.toString());
DBWriter.deleteFeedMediaOfItem(this, media.getId());
}
}
}
try {
unbindService(mConnection);
} catch (IllegalArgumentException e) {
@ -72,9 +91,7 @@ public class PlayerWidgetService extends Service {
startViewUpdaterIfNotRunning();
}
} else {
if (BuildConfig.DEBUG)
Log.d(TAG,
"Service was called while updating. Ignoring update request");
Log.d(TAG, "Service was called while updating. Ignoring update request");
}
return Service.START_NOT_STICKY;
}
@ -153,8 +170,7 @@ public class PlayerWidgetService extends Service {
private ServiceConnection mConnection = new ServiceConnection() {
public void onServiceConnected(ComponentName className, IBinder service) {
if (BuildConfig.DEBUG)
Log.d(TAG, "Connection to service established");
Log.d(TAG, "Connection to service established");
synchronized (psLock) {
playbackService = ((PlaybackService.LocalBinder) service)
.getService();
@ -166,8 +182,7 @@ public class PlayerWidgetService extends Service {
public void onServiceDisconnected(ComponentName name) {
synchronized (psLock) {
playbackService = null;
if (BuildConfig.DEBUG)
Log.d(TAG, "Disconnected from service");
Log.d(TAG, "Disconnected from service");
}
}

View File

@ -95,6 +95,18 @@
tools:src="@drawable/ic_fast_rewind_white_36dp"
tools:background="@android:color/holo_blue_dark" />
<TextView
android:id="@+id/txtvRev"
android:layout_width="wrap_content"
android:layout_height="32dp"
android:layout_alignTop="@id/butRev"
android:layout_alignLeft="@id/butRev"
android:layout_alignRight="@id/butRev"
android:gravity="center"
android:text="30"
android:textSize="8dp"
android:clickable="false"/>
<ImageButton
android:id="@+id/butFF"
android:layout_width="@dimen/audioplayer_playercontrols_length"
@ -106,6 +118,18 @@
tools:src="@drawable/ic_fast_forward_white_36dp"
tools:background="@android:color/holo_blue_dark" />
<TextView
android:id="@+id/txtvFF"
android:layout_width="wrap_content"
android:layout_height="32dp"
android:layout_alignTop="@id/butFF"
android:layout_alignLeft="@id/butFF"
android:layout_alignRight="@id/butFF"
android:gravity="center"
android:text="30"
android:textSize="8dp"
android:clickable="false"/>
<Button
android:id="@+id/butPlaybackSpeed"
android:layout_width="@dimen/audioplayer_playercontrols_length"

View File

@ -59,6 +59,13 @@
android:key="prefAutoDelete"
android:summary="@string/pref_auto_delete_sum"
android:title="@string/pref_auto_delete_title"/>
<ListPreference
android:defaultValue="30"
android:entries="@array/smart_mark_as_played_values"
android:entryValues="@array/smart_mark_as_played_values"
android:key="prefSmartMarkAsPlayedSecs"
android:summary="@string/pref_smart_mark_as_played_sum"
android:title="@string/pref_smart_mark_as_played_title"/>
<Preference
android:key="prefPlaybackSpeedLauncher"
android:summary="@string/pref_playback_speed_sum"
@ -71,14 +78,6 @@
android:summary="@string/pref_pausePlaybackForFocusLoss_sum"
android:title="@string/pref_pausePlaybackForFocusLoss_title" />
<ListPreference
android:defaultValue="30"
android:entries="@array/seek_delta_values"
android:entryValues="@array/seek_delta_values"
android:key="prefSeekDeltaSecs"
android:summary="@string/pref_seek_delta_sum"
android:title="@string/pref_seek_delta_title" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/network_pref">
<ListPreference

View File

@ -45,4 +45,5 @@ dependencies {
compile 'com.squareup.okhttp:okhttp-urlconnection:2.2.0'
compile 'com.squareup.okio:okio:1.2.0'
compile 'com.nineoldandroids:library:2.4.0'
compile 'de.greenrobot:eventbus:2.4.0'
}

View File

@ -0,0 +1,59 @@
package de.danoeh.antennapod.core.util;
import android.test.AndroidTestCase;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.TimeZone;
public class DateUtilsTest extends AndroidTestCase {
public void testParseDateWithMicroseconds() throws Exception {
GregorianCalendar exp = new GregorianCalendar(2015, 2, 28, 13, 31, 4);
Date expected = new Date(exp.getTimeInMillis() + 963);
Date actual = DateUtils.parse("2015-03-28T13:31:04.963870");
assertEquals(expected, actual);
}
public void testParseDateWithCentiseconds() throws Exception {
GregorianCalendar exp = new GregorianCalendar(2015, 2, 28, 13, 31, 4);
Date expected = new Date(exp.getTimeInMillis() + 960);
Date actual = DateUtils.parse("2015-03-28T13:31:04.96");
assertEquals(expected, actual);
}
public void testParseDateWithDeciseconds() throws Exception {
GregorianCalendar exp = new GregorianCalendar(2015, 2, 28, 13, 31, 4);
Date expected = new Date(exp.getTimeInMillis() + 900);
Date actual = DateUtils.parse("2015-03-28T13:31:04.9");
assertEquals(expected.getTime()/1000, actual.getTime()/1000);
assertEquals(900, actual.getTime()%1000);
}
public void testParseDateWithMicrosecondsAndTimezone() throws Exception {
GregorianCalendar exp = new GregorianCalendar(2015, 2, 28, 6, 31, 4);
exp.setTimeZone(TimeZone.getTimeZone("UTC"));
Date expected = new Date(exp.getTimeInMillis() + 963);
Date actual = DateUtils.parse("2015-03-28T13:31:04.963870 +0700");
assertEquals(expected, actual);
}
public void testParseDateWithCentisecondsAndTimezone() throws Exception {
GregorianCalendar exp = new GregorianCalendar(2015, 2, 28, 6, 31, 4);
exp.setTimeZone(TimeZone.getTimeZone("UTC"));
Date expected = new Date(exp.getTimeInMillis() + 960);
Date actual = DateUtils.parse("2015-03-28T13:31:04.96 +0700");
assertEquals(expected, actual);
}
public void testParseDateWithDecisecondsAndTimezone() throws Exception {
GregorianCalendar exp = new GregorianCalendar(2015, 2, 28, 6, 31, 4);
exp.setTimeZone(TimeZone.getTimeZone("UTC"));
Date expected = new Date(exp.getTimeInMillis() + 900);
Date actual = DateUtils.parse("2015-03-28T13:31:04.9 +0700");
assertEquals(expected.getTime()/1000, actual.getTime()/1000);
assertEquals(900, actual.getTime()%1000);
}
}

View File

@ -14,13 +14,13 @@
package com.aocate.media;
import java.io.IOException;
import android.content.Context;
import android.media.MediaPlayer;
import android.net.Uri;
import android.util.Log;
import java.io.IOException;
public class AndroidMediaPlayer extends MediaPlayerImpl {
private final static String AMP_TAG = "AocateAndroidMediaPlayer";

View File

@ -5,8 +5,6 @@ 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;
@ -26,7 +24,6 @@ public class EventDistributor extends Observable {
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;
@ -71,23 +68,17 @@ public class EventDistributor extends Observable {
private void processEventQueue() {
Integer result = 0;
if (BuildConfig.DEBUG)
Log.d(TAG,
"Processing event queue. Number of events: "
+ events.size());
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);
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.");
Log.d(TAG, "Event queue didn't contain any new events. Observers will not be notified.");
}
}
@ -105,10 +96,6 @@ public class EventDistributor extends Observable {
addEvent(UNREAD_ITEMS_UPDATE);
}
public void sendQueueUpdateBroadcast() {
addEvent(QUEUE_UPDATE);
}
public void sendFeedUpdateBroadcast() {
addEvent(FEED_LIST_UPDATE);
}

View File

@ -2,6 +2,9 @@ package de.danoeh.antennapod.core.feed;
import android.net.Uri;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import java.util.Date;
import java.util.List;
import java.util.concurrent.Callable;
@ -384,4 +387,9 @@ public class FeedItem extends FeedComponent implements ShownotesProvider, Flattr
public boolean hasChapters() {
return hasChapters;
}
@Override
public String toString() {
return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE);
}
}

View File

@ -12,6 +12,7 @@ import java.util.concurrent.Callable;
import de.danoeh.antennapod.core.ClientConfig;
import de.danoeh.antennapod.core.preferences.PlaybackPreferences;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.storage.DBReader;
import de.danoeh.antennapod.core.storage.DBWriter;
import de.danoeh.antennapod.core.util.ChapterUtils;
@ -146,6 +147,11 @@ public class FeedMedia extends FeedFile implements Playable {
}
public boolean hasAlmostEnded() {
int smartMarkAsPlayedSecs = UserPreferences.getSmartMarkAsPlayedSecs();
return this.position >= this.duration - smartMarkAsPlayedSecs * 1000;
}
@Override
public int getTypeAsInt() {
return FEEDFILETYPE_FEEDMEDIA;

View File

@ -0,0 +1,51 @@
package de.danoeh.antennapod.core.feed;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import java.util.List;
public class QueueEvent {
public enum Action {
ADDED, ADDED_ITEMS, REMOVED, CLEARED, DELETED_MEDIA, SORTED
}
public final Action action;
public final FeedItem item;
public final int position;
public final List<FeedItem> items;
public QueueEvent(Action action) {
this(action, null, null, -1);
}
public QueueEvent(Action action, FeedItem item) {
this(action, item, null, -1);
}
public QueueEvent(Action action, FeedItem item, int position) {
this(action, item, null, position);
}
public QueueEvent(Action action, List<FeedItem> items) {
this(action, null, items, -1);
}
private QueueEvent(Action action, FeedItem item, List<FeedItem> items, int position) {
this.action = action;
this.item = item;
this.items = items;
this.position = position;
}
@Override
public String toString() {
return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE)
.append("action", action)
.append("item", item)
.append("items", items)
.append("position", position)
.toString();
}
}

View File

@ -45,6 +45,9 @@ import javax.security.auth.x500.X500Principal;
import de.danoeh.antennapod.core.ClientConfig;
import de.danoeh.antennapod.core.gpoddernet.model.GpodnetDevice;
import de.danoeh.antennapod.core.gpoddernet.model.GpodnetEpisodeAction;
import de.danoeh.antennapod.core.gpoddernet.model.GpodnetEpisodeActionGetResponse;
import de.danoeh.antennapod.core.gpoddernet.model.GpodnetEpisodeActionPostResponse;
import de.danoeh.antennapod.core.gpoddernet.model.GpodnetPodcast;
import de.danoeh.antennapod.core.gpoddernet.model.GpodnetSubscriptionChange;
import de.danoeh.antennapod.core.gpoddernet.model.GpodnetTag;
@ -536,6 +539,85 @@ public class GpodnetService {
}
/**
* Updates the episode actions
* <p/>
* This method requires authentication.
*
* @param episodeActions Collection of episode actions.
* @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 GpodnetEpisodeActionPostResponse uploadEpisodeActions(Collection<GpodnetEpisodeAction> episodeActions)
throws GpodnetServiceException {
Validate.notNull(episodeActions);
String username = GpodnetPreferences.getUsername();
try {
URL url = new URI(BASE_SCHEME, BASE_HOST, String.format(
"/api/2/episodes/%s.json", username), null).toURL();
final JSONArray list = new JSONArray();
for(GpodnetEpisodeAction episodeAction : episodeActions) {
JSONObject obj = episodeAction.writeToJSONObject();
if(obj != null) {
list.put(obj);
}
}
RequestBody body = RequestBody.create(JSON, list.toString());
Request.Builder request = new Request.Builder().post(body).url(url);
final String response = executeRequest(request);
return GpodnetEpisodeActionPostResponse.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 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 GpodnetEpisodeActionGetResponse getEpisodeChanges(long timestamp) throws GpodnetServiceException {
String username = GpodnetPreferences.getUsername();
String params = String.format("since=%d", timestamp);
String path = String.format("/api/2/episodes/%s.json",
username);
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 json = new JSONObject(response);
return readEpisodeActionsFromJSONObject(json);
} 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.
@ -773,4 +855,24 @@ public class GpodnetService {
long timestamp = object.getLong("timestamp");
return new GpodnetSubscriptionChange(added, removed, timestamp);
}
private GpodnetEpisodeActionGetResponse readEpisodeActionsFromJSONObject(
JSONObject object) throws JSONException {
Validate.notNull(object);
List<GpodnetEpisodeAction> episodeActions = new ArrayList<GpodnetEpisodeAction>();
long timestamp = object.getLong("timestamp");
JSONArray jsonActions = object.getJSONArray("actions");
for(int i=0; i < jsonActions.length(); i++) {
JSONObject jsonAction = jsonActions.getJSONObject(i);
GpodnetEpisodeAction episodeAction = GpodnetEpisodeAction.readFromJSONObject(jsonAction);
if(episodeAction != null) {
episodeActions.add(episodeAction);
}
}
return new GpodnetEpisodeActionGetResponse(episodeActions, timestamp);
}
}

View File

@ -0,0 +1,315 @@
package de.danoeh.antennapod.core.gpoddernet.model;
import android.util.Log;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import org.json.JSONException;
import org.json.JSONObject;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.TimeZone;
import de.danoeh.antennapod.core.feed.FeedItem;
import de.danoeh.antennapod.core.preferences.GpodnetPreferences;
import de.danoeh.antennapod.core.util.DateUtils;
public class GpodnetEpisodeAction {
private static final String TAG = "GpodnetEpisodeAction";
public enum Action {
NEW, DOWNLOAD, PLAY, DELETE
}
private final String podcast;
private final String episode;
private final String deviceId;
private final Action action;
private final Date timestamp;
private final int started;
private final int position;
private final int total;
private GpodnetEpisodeAction(Builder builder) {
this.podcast = builder.podcast;
this.episode = builder.episode;
this.action = builder.action;
this.deviceId = builder.deviceId;
this.timestamp = builder.timestamp;
this.started = builder.started;
this.position = builder.position;
this.total = builder.total;
}
/**
* Creates an episode action object from a String representation. The representation includes
* all mandatory and optional attributes
*
* @param s String representation (output from {@link #writeToString()})
* @return episode action object, or null if s is invalid
*/
public static GpodnetEpisodeAction readFromString(String s) {
String[] fields = s.split("\t");
if(fields.length != 8) {
return null;
}
String podcast = fields[0];
String episode = fields[1];
String deviceId = fields[2];
try {
Action action = Action.valueOf(fields[3]);
GpodnetEpisodeAction result = new Builder(podcast, episode, action)
.deviceId(deviceId)
.timestamp(new Date(Long.valueOf(fields[4])))
.started(Integer.valueOf(fields[5]))
.position(Integer.valueOf(fields[6]))
.total(Integer.valueOf(fields[7]))
.build();
return result;
} catch(IllegalArgumentException e) {
Log.e(TAG, "readFromString(" + s + "): " + e.getMessage());
return null;
}
}
/**
* Create an episode action object from JSON representation. Mandatory fields are "podcast",
* "episode" and "action".
*
* @param object JSON representation
* @return episode action object, or null if mandatory values are missing
*/
public static GpodnetEpisodeAction readFromJSONObject(JSONObject object) {
String podcast = object.optString("podcast", null);
String episode = object.optString("episode", null);
String actionString = object.optString("action", null);
if(StringUtils.isEmpty(podcast) || StringUtils.isEmpty(episode) || StringUtils.isEmpty(actionString)) {
return null;
}
GpodnetEpisodeAction.Action action = GpodnetEpisodeAction.Action.valueOf(actionString.toUpperCase());
String deviceId = object.optString("device", "");
GpodnetEpisodeAction.Builder builder = new GpodnetEpisodeAction.Builder(podcast, episode, action)
.deviceId(deviceId);
String utcTimestamp = object.optString("timestamp", null);
if(StringUtils.isNotEmpty(utcTimestamp)) {
builder.timestamp(DateUtils.parse(utcTimestamp));
}
if(action == GpodnetEpisodeAction.Action.PLAY) {
int started = object.optInt("started", -1);
int position = object.optInt("position", -1);
int total = object.optInt("total", -1);
if(started >= 0 && position > 0 && total > 0) {
builder
.started(started)
.position(position)
.total(total);
}
}
return builder.build();
}
public String getPodcast() {
return this.podcast;
}
public String getEpisode() {
return this.episode;
}
public String getDeviceId() {
return this.deviceId;
}
public Action getAction() {
return this.action;
}
public String getActionString() {
return this.action.name().toLowerCase();
}
public Date getTimestamp() {
return this.timestamp;
}
/**
* Returns the position (in seconds) at which the client started playback
*
* @return start position (in seconds)
*/
public int getStarted() {
return this.started;
}
/**
* Returns the position (in seconds) at which the client stopped playback
*
* @return stop position (in seconds)
*/
public int getPosition() {
return this.position;
}
/**
* Returns the total length of the file in seconds.
*
* @return total length in seconds
*/
public int getTotal() {
return this.total;
}
@Override
public boolean equals(Object o) {
if(o == null) return false;
if(this == o) return true;
if(this.getClass() != o.getClass()) return false;
GpodnetEpisodeAction that = (GpodnetEpisodeAction)o;
return new EqualsBuilder()
.append(this.podcast, that.podcast)
.append(this.episode, that.episode)
.append(this.deviceId, that.deviceId)
.append(this.action, that.action)
.append(this.timestamp, that.timestamp)
.append(this.started, that.started)
.append(this.position, that.position)
.append(this.total, that.total)
.isEquals();
}
@Override
public int hashCode() {
return new HashCodeBuilder()
.append(this.podcast)
.append(this.episode)
.append(this.deviceId)
.append(this.action)
.append(this.timestamp)
.append(this.started)
.append(this.position)
.append(this.total)
.toHashCode();
}
public String writeToString() {
StringBuilder result = new StringBuilder();
result.append(this.podcast).append("\t");
result.append(this.episode).append("\t");
result.append(this.deviceId).append("\t");
result.append(this.action).append("\t");
result.append(this.timestamp.getTime()).append("\t");
result.append(String.valueOf(this.started)).append("\t");
result.append(String.valueOf(this.position)).append("\t");
result.append(String.valueOf(this.total));
return result.toString();
}
/**
* Returns a JSON object representation of this object
*
* @return JSON object representation, or null if the object is invalid
*/
public JSONObject writeToJSONObject() {
JSONObject obj = new JSONObject();
try {
obj.putOpt("podcast", this.podcast);
obj.putOpt("episode", this.episode);
obj.put("device", this.deviceId);
obj.put("action", this.getActionString());
SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.US);
formatter.setTimeZone(TimeZone.getTimeZone("UTC"));
obj.put("timestamp",formatter.format(this.timestamp));
if (this.getAction() == Action.PLAY) {
obj.put("started", this.started);
obj.put("position", this.position);
obj.put("total", this.total);
}
} catch(JSONException e) {
Log.e(TAG, "writeToJSONObject(): " + e.getMessage());
return null;
}
return obj;
}
@Override
public String toString() {
return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE);
}
public static class Builder {
// mandatory
private final String podcast;
private final String episode;
private final Action action;
// optional
private String deviceId = "";
private Date timestamp;
private int started = -1;
private int position = -1;
private int total = -1;
public Builder(FeedItem item, Action action) {
this(item.getFeed().getDownload_url(), item.getItemIdentifier(), action);
}
public Builder(String podcast, String episode, Action action) {
this.podcast = podcast;
this.episode = episode;
this.action = action;
}
public Builder deviceId(String deviceId) {
this.deviceId = deviceId;
return this;
}
public Builder currentDeviceId() {
return deviceId(GpodnetPreferences.getDeviceID());
}
public Builder timestamp(Date timestamp) {
this.timestamp = timestamp;
return this;
}
public Builder currentTimestamp() {
return timestamp(new Date());
}
public Builder started(int seconds) {
if(action == Action.PLAY) {
this.started = seconds;
}
return this;
}
public Builder position(int seconds) {
if(action == Action.PLAY) {
this.position = seconds;
}
return this;
}
public Builder total(int seconds) {
if(action == Action.PLAY) {
this.total = seconds;
}
return this;
}
public GpodnetEpisodeAction build() {
return new GpodnetEpisodeAction(this);
}
}
}

View File

@ -0,0 +1,34 @@
package de.danoeh.antennapod.core.gpoddernet.model;
import org.apache.commons.lang3.Validate;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import java.util.List;
public class GpodnetEpisodeActionGetResponse {
private final List<GpodnetEpisodeAction> episodeActions;
private final long timestamp;
public GpodnetEpisodeActionGetResponse(List<GpodnetEpisodeAction> episodeActions, long timestamp) {
Validate.notNull(episodeActions);
this.episodeActions = episodeActions;
this.timestamp = timestamp;
}
public List<GpodnetEpisodeAction> getEpisodeActions() {
return this.episodeActions;
}
public long getTimestamp() {
return this.timestamp;
}
@Override
public String toString() {
return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE);
}
}

View File

@ -0,0 +1,53 @@
package de.danoeh.antennapod.core.gpoddernet.model;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.HashMap;
import java.util.Map;
public class GpodnetEpisodeActionPostResponse {
/**
* 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 GpodnetEpisodeActionPostResponse(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 GpodnetEpisodeActionPostResponse fromJSONObject(String objectString) throws JSONException {
final JSONObject object = new JSONObject(objectString);
final long timestamp = object.getLong("timestamp");
Map<String, String> updatedUrls = new HashMap<String, String>();
JSONArray urls = object.getJSONArray("update_urls");
for (int i = 0; i < urls.length(); i++) {
JSONArray urlPair = urls.getJSONArray(i);
updatedUrls.put(urlPair.getString(0), urlPair.getString(1));
}
return new GpodnetEpisodeActionPostResponse(timestamp, updatedUrls);
}
@Override
public String toString() {
return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE);
}
}

View File

@ -4,14 +4,20 @@ import android.content.Context;
import android.content.SharedPreferences;
import android.util.Log;
import org.apache.commons.lang3.StringUtils;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
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.gpoddernet.model.GpodnetEpisodeAction;
import de.danoeh.antennapod.core.service.GpodnetSyncService;
/**
@ -28,9 +34,11 @@ public class GpodnetPreferences {
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_LAST_SUBSCRIPTION_SYNC_TIMESTAMP = "de.danoeh.antennapod.preferences.gpoddernet.last_sync_timestamp";
public static final String PREF_LAST_EPISODE_ACTIONS_SYNC_TIMESTAMP = "de.danoeh.antennapod.preferences.gpoddernet.last_episode_actions_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";
public static final String PREF_SYNC_EPISODE_ACTIONS = "de.danoeh.antennapod.preferences.gpoddernet.sync_queued_episode_actions";
private static String username;
private static String password;
@ -41,10 +49,14 @@ public class GpodnetPreferences {
private static Set<String> addedFeeds;
private static Set<String> removedFeeds;
private static List<GpodnetEpisodeAction> queuedEpisodeActions;
/**
* Last value returned by getSubscriptionChanges call. Will be used for all subsequent calls of getSubscriptionChanges.
*/
private static long lastSyncTimestamp;
private static long lastSubscriptionSyncTimestamp;
private static long lastEpisodeActionsSyncTimeStamp;
private static boolean preferencesLoaded = false;
@ -58,9 +70,11 @@ public class GpodnetPreferences {
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);
lastSubscriptionSyncTimestamp = prefs.getLong(PREF_LAST_SUBSCRIPTION_SYNC_TIMESTAMP, 0);
lastEpisodeActionsSyncTimeStamp = prefs.getLong(PREF_LAST_EPISODE_ACTIONS_SYNC_TIMESTAMP, 0);
addedFeeds = readListFromString(prefs.getString(PREF_SYNC_ADDED, ""));
removedFeeds = readListFromString(prefs.getString(PREF_SYNC_REMOVED, ""));
queuedEpisodeActions = readEpisodeActionsFromString(prefs.getString(PREF_SYNC_EPISODE_ACTIONS, ""));
hostname = checkGpodnetHostname(prefs.getString(PREF_GPODNET_HOSTNAME, GpodnetService.DEFAULT_BASE_HOST));
preferencesLoaded = true;
@ -115,14 +129,24 @@ public class GpodnetPreferences {
writePreference(PREF_GPODNET_DEVICEID, deviceID);
}
public static long getLastSyncTimestamp() {
public static long getLastSubscriptionSyncTimestamp() {
ensurePreferencesLoaded();
return lastSyncTimestamp;
return lastSubscriptionSyncTimestamp;
}
public static void setLastSyncTimestamp(long lastSyncTimestamp) {
GpodnetPreferences.lastSyncTimestamp = lastSyncTimestamp;
writePreference(PREF_LAST_SYNC_TIMESTAMP, lastSyncTimestamp);
public static void setLastSubscriptionSyncTimestamp(long timestamp) {
GpodnetPreferences.lastSubscriptionSyncTimestamp = timestamp;
writePreference(PREF_LAST_SUBSCRIPTION_SYNC_TIMESTAMP, timestamp);
}
public static long getLastEpisodeActionsSyncTimestamp() {
ensurePreferencesLoaded();
return lastEpisodeActionsSyncTimeStamp;
}
public static void setLastEpisodeActionsSyncTimestamp(long timestamp) {
GpodnetPreferences.lastEpisodeActionsSyncTimeStamp = timestamp;
writePreference(PREF_LAST_EPISODE_ACTIONS_SYNC_TIMESTAMP, timestamp);
}
public static String getHostname() {
@ -195,7 +219,23 @@ public class GpodnetPreferences {
ensurePreferencesLoaded();
removedFeeds.removeAll(removed);
writePreference(PREF_SYNC_REMOVED, removedFeeds);
}
public static synchronized void enqueueEpisodeAction(GpodnetEpisodeAction action) {
ensurePreferencesLoaded();
queuedEpisodeActions.add(action);
writePreference(PREF_SYNC_EPISODE_ACTIONS, writeEpisodeActionsToString(queuedEpisodeActions));
}
public static List<GpodnetEpisodeAction> getQueuedEpisodeActions() {
ensurePreferencesLoaded();
return Collections.unmodifiableList(queuedEpisodeActions);
}
public static synchronized void removeQueuedEpisodeActions(Collection<GpodnetEpisodeAction> queued) {
ensurePreferencesLoaded();
queuedEpisodeActions.removeAll(queued);
writePreference(PREF_SYNC_EPISODE_ACTIONS, writeEpisodeActionsToString(queuedEpisodeActions));
}
/**
@ -215,7 +255,9 @@ public class GpodnetPreferences {
writePreference(PREF_SYNC_ADDED, addedFeeds);
removedFeeds.clear();
writePreference(PREF_SYNC_REMOVED, removedFeeds);
setLastSyncTimestamp(0);
queuedEpisodeActions.clear();
writePreference(PREF_SYNC_EPISODE_ACTIONS, writeEpisodeActionsToString(queuedEpisodeActions));
setLastSubscriptionSyncTimestamp(0);
}
private static Set<String> readListFromString(String s) {
@ -235,6 +277,29 @@ public class GpodnetPreferences {
return result.toString().trim();
}
private static List<GpodnetEpisodeAction> readEpisodeActionsFromString(String s) {
String[] lines = s.split("\n");
List<GpodnetEpisodeAction> result = new ArrayList<GpodnetEpisodeAction>(lines.length);
for(String line : lines) {
if(StringUtils.isNotBlank(line)) {
GpodnetEpisodeAction action = GpodnetEpisodeAction.readFromString(line);
if(action != null) {
result.add(GpodnetEpisodeAction.readFromString(line));
}
}
}
return result;
}
private static String writeEpisodeActionsToString(Collection<GpodnetEpisodeAction> c) {
StringBuilder result = new StringBuilder();
for(GpodnetEpisodeAction item : c) {
result.append(item.writeToString());
result.append("\n");
}
return result.toString();
}
private static String checkGpodnetHostname(String value) {
int startIndex = 0;
if (value.startsWith("http://")) {

View File

@ -45,6 +45,7 @@ public class UserPreferences implements
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_SMART_MARK_AS_PLAYED_SECS = "prefSmartMarkAsPlayedSecs";
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";
@ -57,7 +58,8 @@ public class UserPreferences implements
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_FAST_FORWARD_SECS = "prefFastForwardSecs";
private static final String PREF_REWIND_SECS = "prefRewindSecs";
private static final String PREF_EXPANDED_NOTIFICATION = "prefExpandNotify";
private static final String PREF_PERSISTENT_NOTIFICATION = "prefPersistNotify";
public static final String PREF_QUEUE_ADD_TO_FRONT = "prefQueueAddToFront";
@ -79,6 +81,7 @@ public class UserPreferences implements
private boolean allowMobileUpdate;
private boolean displayOnlyEpisodes;
private boolean autoDelete;
private int smartMarkAsPlayedSecs;
private boolean autoFlattr;
private float autoFlattrPlayedDurationThreshold;
private int theme;
@ -91,7 +94,8 @@ public class UserPreferences implements
private String playbackSpeed;
private String[] playbackSpeedArray;
private boolean pauseForFocusLoss;
private int seekDeltaSecs;
private int fastForwardSecs;
private int rewindSecs;
private boolean isFreshInstall;
private int notifyPriority;
private boolean persistNotify;
@ -107,8 +111,7 @@ public class UserPreferences implements
* @throws IllegalArgumentException if context is null
*/
public static void createInstance(Context context) {
if (BuildConfig.DEBUG)
Log.d(TAG, "Creating new instance of UserPreferences");
Log.d(TAG, "Creating new instance of UserPreferences");
Validate.notNull(context);
instance = new UserPreferences(context);
@ -137,6 +140,7 @@ public class UserPreferences implements
allowMobileUpdate = sp.getBoolean(PREF_MOBILE_UPDATE, false);
displayOnlyEpisodes = sp.getBoolean(PREF_DISPLAY_ONLY_EPISODES, false);
autoDelete = sp.getBoolean(PREF_AUTO_DELETE, false);
smartMarkAsPlayedSecs = Integer.valueOf(sp.getString(PREF_SMART_MARK_AS_PLAYED_SECS, "30"));
autoFlattr = sp.getBoolean(PREF_AUTO_FLATTR, false);
autoFlattrPlayedDurationThreshold = sp.getFloat(PREF_AUTO_FLATTR_PLAYED_DURATION_THRESHOLD,
PREF_AUTO_FLATTR_PLAYED_DURATION_THRESHOLD_DEFAULT);
@ -154,7 +158,8 @@ public class UserPreferences implements
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"));
fastForwardSecs = sp.getInt(PREF_FAST_FORWARD_SECS, 30);
rewindSecs = sp.getInt(PREF_REWIND_SECS, 30);
if (sp.getBoolean(PREF_EXPANDED_NOTIFICATION, false)) {
notifyPriority = NotificationCompat.PRIORITY_MAX;
}
@ -267,6 +272,11 @@ public class UserPreferences implements
return instance.autoDelete;
}
public static int getSmartMarkAsPlayedSecs() {
instanceAvailable();;
return instance.smartMarkAsPlayedSecs;
}
public static boolean isAutoFlattr() {
instanceAvailable();
return instance.autoFlattr;
@ -335,9 +345,14 @@ public class UserPreferences implements
return instance.playbackSpeedArray;
}
public static int getSeekDeltaMs() {
public static int getFastFowardSecs() {
instanceAvailable();
return 1000 * instance.seekDeltaSecs;
return instance.fastForwardSecs;
}
public static int getRewindSecs() {
instanceAvailable();
return instance.rewindSecs;
}
/**
@ -372,8 +387,7 @@ public class UserPreferences implements
@Override
public void onSharedPreferenceChanged(SharedPreferences sp, String key) {
if (BuildConfig.DEBUG)
Log.d(TAG, "Registered change of user preferences. Key: " + key);
Log.d(TAG, "Registered change of user preferences. Key: " + key);
if (key.equals(PREF_DOWNLOAD_MEDIA_ON_WIFI_ONLY)) {
downloadMediaOnWifiOnly = sp.getBoolean(
@ -389,10 +403,10 @@ public class UserPreferences implements
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_SMART_MARK_AS_PLAYED_SECS)) {
smartMarkAsPlayedSecs = Integer.valueOf(sp.getString(PREF_SMART_MARK_AS_PLAYED_SECS, "30"));
} else if (key.equals(PREF_AUTO_FLATTR)) {
autoFlattr = sp.getBoolean(PREF_AUTO_FLATTR, false);
} else if (key.equals(PREF_DISPLAY_ONLY_EPISODES)) {
@ -422,8 +436,10 @@ public class UserPreferences implements
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_FAST_FORWARD_SECS)) {
fastForwardSecs = sp.getInt(PREF_FAST_FORWARD_SECS, 30);
} else if (key.equals(PREF_REWIND_SECS)) {
rewindSecs = sp.getInt(PREF_REWIND_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_UNPAUSE_ON_HEADSET_RECONNECT)) {
@ -443,6 +459,20 @@ public class UserPreferences implements
}
}
public static void setPrefFastForwardSecs(int secs) {
Log.d(TAG, "setPrefFastForwardSecs(" + secs +")");
SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(instance.context).edit();
editor.putInt(PREF_FAST_FORWARD_SECS, secs);
editor.commit();
}
public static void setPrefRewindSecs(int secs) {
Log.d(TAG, "setPrefRewindSecs(" + secs +")");
SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(instance.context).edit();
editor.putInt(PREF_REWIND_SECS, secs);
editor.commit();
}
public static void setPlaybackSpeed(String speed) {
PreferenceManager.getDefaultSharedPreferences(instance.context).edit()
.putString(PREF_PLAYBACK_SPEED, speed).apply();
@ -517,8 +547,7 @@ public class UserPreferences implements
.getDefaultSharedPreferences(context.getApplicationContext());
String strDir = prefs.getString(PREF_DATA_FOLDER, null);
if (strDir == null) {
if (BuildConfig.DEBUG)
Log.d(TAG, "Using default data folder");
Log.d(TAG, "Using default data folder");
return context.getExternalFilesDir(type);
} else {
File dataDir = new File(strDir);
@ -549,8 +578,7 @@ public class UserPreferences implements
if (!typeDir.exists()) {
if (dataDir.canWrite()) {
if (!typeDir.mkdir()) {
Log.e(TAG, "Could not create data folder named "
+ type);
Log.e(TAG, "Could not create data folder named " + type);
return null;
}
}
@ -562,8 +590,7 @@ public class UserPreferences implements
}
public static void setDataFolder(String dir) {
if (BuildConfig.DEBUG)
Log.d(TAG, "Result from DirectoryChooser: " + dir);
Log.d(TAG, "Result from DirectoryChooser: " + dir);
instanceAvailable();
SharedPreferences prefs = PreferenceManager
.getDefaultSharedPreferences(instance.context);
@ -600,16 +627,13 @@ public class UserPreferences implements
IMPORT_DIR);
if (importDir != null) {
if (importDir.exists()) {
if (BuildConfig.DEBUG)
Log.d(TAG, "Import directory already exists");
Log.d(TAG, "Import directory already exists");
} else {
if (BuildConfig.DEBUG)
Log.d(TAG, "Creating import directory");
Log.d(TAG, "Creating import directory");
importDir.mkdir();
}
} else {
if (BuildConfig.DEBUG)
Log.d(TAG, "Could not access external storage.");
Log.d(TAG, "Could not access external storage.");
}
}
@ -618,8 +642,7 @@ public class UserPreferences implements
*/
public static void restartUpdateAlarm(long triggerAtMillis, long intervalMillis) {
instanceAvailable();
if (BuildConfig.DEBUG)
Log.d(TAG, "Restarting update alarm.");
Log.d(TAG, "Restarting update alarm.");
AlarmManager alarmManager = (AlarmManager) instance.context
.getSystemService(Context.ALARM_SERVICE);
PendingIntent updateIntent = PendingIntent.getBroadcast(
@ -628,11 +651,9 @@ public class UserPreferences implements
if (intervalMillis != 0) {
alarmManager.setRepeating(AlarmManager.RTC_WAKEUP, triggerAtMillis, intervalMillis,
updateIntent);
if (BuildConfig.DEBUG)
Log.d(TAG, "Changed alarm to new interval");
Log.d(TAG, "Changed alarm to new interval");
} else {
if (BuildConfig.DEBUG)
Log.d(TAG, "Automatic update was deactivated");
Log.d(TAG, "Automatic update was deactivated");
}
}

View File

@ -9,24 +9,31 @@ import android.content.Intent;
import android.os.IBinder;
import android.support.v4.app.NotificationCompat;
import android.util.Log;
import android.util.Pair;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.LinkedList;
import java.util.HashMap;
import java.util.List;
import java.util.Set;
import java.util.Map;
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.feed.FeedItem;
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.GpodnetEpisodeAction;
import de.danoeh.antennapod.core.gpoddernet.model.GpodnetEpisodeActionGetResponse;
import de.danoeh.antennapod.core.gpoddernet.model.GpodnetEpisodeActionPostResponse;
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.DBWriter;
import de.danoeh.antennapod.core.storage.DownloadRequestException;
import de.danoeh.antennapod.core.storage.DownloadRequester;
import de.danoeh.antennapod.core.util.NetworkUtils;
@ -50,7 +57,7 @@ public class GpodnetSyncService extends Service {
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));
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");
@ -61,9 +68,8 @@ public class GpodnetSyncService extends Service {
@Override
public void onDestroy() {
super.onDestroy();
if (BuildConfig.DEBUG) Log.d(TAG, "onDestroy");
Log.d(TAG, "onDestroy");
syncWaiterThread.interrupt();
}
@Override
@ -79,64 +85,94 @@ public class GpodnetSyncService extends Service {
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();
}
private synchronized void sync() {
if (GpodnetPreferences.loggedIn() == false || NetworkUtils.networkAvailable(this) == false) {
stopSelf();
return;
}
syncSubscriptionChanges();
syncEpisodeActions();
stopSelf();
}
private synchronized void syncSubscriptionChanges() {
final long timestamp = GpodnetPreferences.getLastSubscriptionSyncTimestamp();
try {
final List<String> localSubscriptions = DBReader.getFeedListDownloadUrls(this);
GpodnetService service = tryLogin();
// first sync: download all subscriptions...
GpodnetSubscriptionChange subscriptionChanges = service.getSubscriptionChanges(GpodnetPreferences.getUsername(),
GpodnetPreferences.getDeviceID(), timestamp);
long lastUpdate = subscriptionChanges.getTimestamp();
Log.d(TAG, "Downloaded subscription changes: " + subscriptionChanges);
processSubscriptionChanges(localSubscriptions, subscriptionChanges);
Collection<String> added;
Collection<String> removed;
if (timestamp == 0) {
added = localSubscriptions;
GpodnetPreferences.removeRemovedFeeds(GpodnetPreferences.getRemovedFeedsCopy());
removed = Collections.emptyList();
} else {
added = GpodnetPreferences.getAddedFeedsCopy();
removed = GpodnetPreferences.getRemovedFeedsCopy();
}
if(added.size() > 0 || removed.size() > 0) {
Log.d(TAG, String.format("Uploading subscriptions, Added: %s\nRemoved: %s",
added, removed));
GpodnetUploadChangesResponse uploadResponse = service.uploadChanges(GpodnetPreferences.getUsername(),
GpodnetPreferences.getDeviceID(), added, removed);
lastUpdate = uploadResponse.timestamp;
Log.d(TAG, "Upload changes response: " + uploadResponse);
GpodnetPreferences.removeAddedFeeds(added);
GpodnetPreferences.removeRemovedFeeds(removed);
}
GpodnetPreferences.setLastSubscriptionSyncTimestamp(lastUpdate);
clearErrorNotifications();
} catch (GpodnetServiceException e) {
e.printStackTrace();
updateErrorNotification(e);
} catch (DownloadRequestException e) {
e.printStackTrace();
}
}
private synchronized void syncEpisodeActions() {
final long timestamp = GpodnetPreferences.getLastEpisodeActionsSyncTimestamp();
Log.d(TAG, "last episode actions sync timestamp: " + timestamp);
try {
GpodnetService service = tryLogin();
// download episode actions
GpodnetEpisodeActionGetResponse getResponse = service.getEpisodeChanges(timestamp);
long lastUpdate = getResponse.getTimestamp();
Log.d(TAG, "Downloaded episode actions: " + getResponse);
List<GpodnetEpisodeAction> remoteActions = getResponse.getEpisodeActions();
List<GpodnetEpisodeAction> localActions = GpodnetPreferences.getQueuedEpisodeActions();
processEpisodeActions(localActions, remoteActions);
// upload local actions
if(localActions.size() > 0) {
Log.d(TAG, "Uploading episode actions: " + localActions);
GpodnetEpisodeActionPostResponse postResponse = service.uploadEpisodeActions(localActions);
lastUpdate = postResponse.timestamp;
Log.d(TAG, "Upload episode response: " + postResponse);
GpodnetPreferences.removeQueuedEpisodeActions(localActions);
}
GpodnetPreferences.setLastEpisodeActionsSyncTimestamp(lastUpdate);
clearErrorNotifications();
} catch (GpodnetServiceException e) {
e.printStackTrace();
updateErrorNotification(e);
} catch (DownloadRequestException e) {
e.printStackTrace();
}
}
private synchronized void processSubscriptionChanges(List<String> localSubscriptions, GpodnetSubscriptionChange changes) throws DownloadRequestException {
for (String downloadUrl : changes.getAdded()) {
if (!localSubscriptions.contains(downloadUrl)) {
@ -149,6 +185,64 @@ public class GpodnetSyncService extends Service {
}
}
private synchronized void processEpisodeActions(List<GpodnetEpisodeAction> localActions, List<GpodnetEpisodeAction> remoteActions) throws DownloadRequestException {
if(remoteActions.size() == 0) {
return;
}
Map<Pair<String, String>, GpodnetEpisodeAction> localMostRecentPlayAction = new HashMap<Pair<String, String>, GpodnetEpisodeAction>();
Map<Pair<String, String>, GpodnetEpisodeAction> remoteMostRecentPlayAction = new HashMap<Pair<String, String>, GpodnetEpisodeAction>();
// make sure more recent local actions are not overwritten by older remote actions
for(GpodnetEpisodeAction action : localActions) {
Pair key = new Pair(action.getPodcast(), action.getEpisode());
GpodnetEpisodeAction mostRecent = localMostRecentPlayAction.get(key);
if (mostRecent == null) {
localMostRecentPlayAction.put(key, action);
} else if (mostRecent.getTimestamp().before(action.getTimestamp())) {
localMostRecentPlayAction.put(key, action);
}
}
for (GpodnetEpisodeAction action : remoteActions) {
switch (action.getAction()) {
case NEW:
FeedItem newItem = DBReader.getFeedItem(this, action.getPodcast(), action.getEpisode());
if(newItem != null) {
DBWriter.markItemRead(this, newItem, false, true);
} else {
Log.i(TAG, "Unknown feed item: " + action);
}
break;
case DOWNLOAD:
break;
case PLAY:
Pair key = new Pair(action.getPodcast(), action.getEpisode());
GpodnetEpisodeAction localMostRecent = localMostRecentPlayAction.get(key);
if(localMostRecent == null ||
localMostRecent.getTimestamp().before(action.getTimestamp())) {
GpodnetEpisodeAction mostRecent = remoteMostRecentPlayAction.get(key);
if (mostRecent == null) {
remoteMostRecentPlayAction.put(key, action);
} else if (mostRecent.getTimestamp().before(action.getTimestamp())) {
remoteMostRecentPlayAction.put(key, action);
}
}
break;
case DELETE:
// NEVER EVER call DBWriter.deleteFeedMediaOfItem() here, leads to an infinite loop
break;
}
}
for (GpodnetEpisodeAction action : remoteMostRecentPlayAction.values()) {
FeedItem playItem = DBReader.getFeedItem(this, action.getPodcast(), action.getEpisode());
if (playItem != null) {
playItem.getMedia().setPosition(action.getPosition() * 1000);
if(playItem.getMedia().hasAlmostEnded()) {
DBWriter.markItemRead(this, playItem, true, true);
DBWriter.addItemToPlaybackHistory(this, playItem.getMedia());
}
}
}
}
private void clearErrorNotifications() {
NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
nm.cancel(R.id.notification_gpodnet_sync_error);
@ -156,7 +250,7 @@ public class GpodnetSyncService extends Service {
}
private void updateErrorNotification(GpodnetServiceException exception) {
if (BuildConfig.DEBUG) Log.d(TAG, "Posting error notification");
Log.d(TAG, "Posting error notification");
NotificationCompat.Builder builder = new NotificationCompat.Builder(this);
final String title;
@ -186,7 +280,7 @@ public class GpodnetSyncService extends Service {
private WaiterThread syncWaiterThread = new WaiterThread(WAIT_INTERVAL) {
@Override
public void onWaitCompleted() {
syncChanges();
sync();
}
};
@ -209,7 +303,7 @@ public class GpodnetSyncService extends Service {
private void reinit() {
if (thread != null && thread.isAlive()) {
Log.d(TAG, "Interrupting waiter thread");
Log.d(TAG, "Interrupting waiter thread");
thread.interrupt();
}
thread = new Thread() {

View File

@ -60,6 +60,9 @@ 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.gpoddernet.model.GpodnetEpisodeAction;
import de.danoeh.antennapod.core.gpoddernet.model.GpodnetEpisodeAction.Action;
import de.danoeh.antennapod.core.preferences.GpodnetPreferences;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.storage.DBReader;
import de.danoeh.antennapod.core.storage.DBTasks;
@ -800,6 +803,18 @@ public class DownloadService extends Service {
// queue new media files for automatic download
for (FeedItem item : savedFeed.getItems()) {
if(item.getPubDate() == null) {
Log.d(TAG, item.toString());
}
if(item.getImage() != null && item.getImage().isDownloaded() == false) {
item.getImage().setOwner(item);
try {
requester.downloadImage(DownloadService.this,
item.getImage());
} catch (DownloadRequestException e) {
e.printStackTrace();
}
}
if (!item.isRead() && item.hasMedia() && !item.getMedia().isDownloaded()) {
newMediaFiles.add(item.getMedia().getId());
}
@ -1166,6 +1181,15 @@ public class DownloadService extends Service {
saveDownloadStatus(status);
sendDownloadHandledIntent();
if(GpodnetPreferences.loggedIn()) {
FeedItem item = media.getItem();
GpodnetEpisodeAction action = new GpodnetEpisodeAction.Builder(item, Action.DOWNLOAD)
.currentDeviceId()
.currentTimestamp()
.build();
GpodnetPreferences.enqueueEpisodeAction(action);
}
numberOfDownloads.decrementAndGet();
queryDownloadsAsync();
}

View File

@ -36,13 +36,15 @@ 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.gpoddernet.model.GpodnetEpisodeAction;
import de.danoeh.antennapod.core.gpoddernet.model.GpodnetEpisodeAction.Action;
import de.danoeh.antennapod.core.preferences.GpodnetPreferences;
import de.danoeh.antennapod.core.preferences.PlaybackPreferences;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.receiver.MediaButtonReceiver;
@ -167,6 +169,8 @@ public class PlaybackService extends Service {
private PlaybackServiceMediaPlayer mediaPlayer;
private PlaybackServiceTaskManager taskManager;
private int startPosition;
private static volatile MediaType currentMediaType = MediaType.UNKNOWN;
private final IBinder mBinder = new LocalBinder();
@ -179,8 +183,7 @@ public class PlaybackService extends Service {
@Override
public boolean onUnbind(Intent intent) {
if (BuildConfig.DEBUG)
Log.d(TAG, "Received onUnbind event");
Log.d(TAG, "Received onUnbind event");
return super.onUnbind(intent);
}
@ -214,8 +217,7 @@ public class PlaybackService extends Service {
@Override
public void onCreate() {
super.onCreate();
if (BuildConfig.DEBUG)
Log.d(TAG, "Service created.");
Log.d(TAG, "Service created.");
isRunning = true;
registerReceiver(headsetDisconnected, new IntentFilter(
@ -242,8 +244,7 @@ public class PlaybackService extends Service {
@Override
public void onDestroy() {
super.onDestroy();
if (BuildConfig.DEBUG)
Log.d(TAG, "Service is about to be destroyed");
Log.d(TAG, "Service is about to be destroyed");
isRunning = false;
started = false;
currentMediaType = MediaType.UNKNOWN;
@ -259,8 +260,7 @@ public class PlaybackService extends Service {
@Override
public IBinder onBind(Intent intent) {
if (BuildConfig.DEBUG)
Log.d(TAG, "Received onBind event");
Log.d(TAG, "Received onBind event");
return mBinder;
}
@ -268,8 +268,7 @@ public class PlaybackService extends Service {
public int onStartCommand(Intent intent, int flags, int startId) {
super.onStartCommand(intent, flags, startId);
if (BuildConfig.DEBUG)
Log.d(TAG, "OnStartCommand called");
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) {
@ -278,14 +277,12 @@ public class PlaybackService extends Service {
}
if ((flags & Service.START_FLAG_REDELIVERY) != 0) {
if (BuildConfig.DEBUG)
Log.d(TAG, "onStartCommand is a redelivered intent, calling stopForeground now.");
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");
Log.d(TAG, "Received media button event");
handleKeycode(keycode);
} else {
started = true;
@ -305,8 +302,7 @@ public class PlaybackService extends Service {
* Handles media button events
*/
private void handleKeycode(int keycode) {
if (BuildConfig.DEBUG)
Log.d(TAG, "Handling keycode: " + keycode);
Log.d(TAG, "Handling keycode: " + keycode);
final PlaybackServiceMediaPlayer.PSMPInfo info = mediaPlayer.getPSMPInfo();
final PlayerStatus status = info.playerStatus;
switch (keycode) {
@ -348,11 +344,11 @@ public class PlaybackService extends Service {
break;
case KeyEvent.KEYCODE_MEDIA_NEXT:
case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD:
mediaPlayer.seekDelta(UserPreferences.getSeekDeltaMs());
mediaPlayer.seekDelta(UserPreferences.getFastFowardSecs() * 1000);
break;
case KeyEvent.KEYCODE_MEDIA_PREVIOUS:
case KeyEvent.KEYCODE_MEDIA_REWIND:
mediaPlayer.seekDelta(-UserPreferences.getSeekDeltaMs());
mediaPlayer.seekDelta(-UserPreferences.getRewindSecs() * 1000);
break;
case KeyEvent.KEYCODE_MEDIA_STOP:
if (status == PlayerStatus.PLAYING) {
@ -376,8 +372,7 @@ public class PlaybackService extends Service {
* mediaplayer.
*/
public void setVideoSurface(SurfaceHolder sh) {
if (BuildConfig.DEBUG)
Log.d(TAG, "Setting display");
Log.d(TAG, "Setting display");
mediaPlayer.setVideoSurface(sh);
}
@ -445,6 +440,21 @@ public class PlaybackService extends Service {
}
writePlayerStatusPlaybackPreferences();
final Playable playable = mediaPlayer.getPSMPInfo().playable;
// Gpodder: send play action
if(GpodnetPreferences.loggedIn() && playable instanceof FeedMedia) {
FeedMedia media = (FeedMedia) playable;
FeedItem item = media.getItem();
GpodnetEpisodeAction action = new GpodnetEpisodeAction.Builder(item, Action.PLAY)
.currentDeviceId()
.currentTimestamp()
.started(startPosition / 1000)
.position(getCurrentPosition() / 1000)
.total(getDuration() / 1000)
.build();
GpodnetPreferences.enqueueEpisodeAction(action);
}
break;
case STOPPED:
@ -453,16 +463,15 @@ public class PlaybackService extends Service {
break;
case PLAYING:
if (BuildConfig.DEBUG)
Log.d(TAG, "Audiofocus successfully requested");
if (BuildConfig.DEBUG)
Log.d(TAG, "Resuming/Starting playback");
Log.d(TAG, "Audiofocus successfully requested");
Log.d(TAG, "Resuming/Starting playback");
taskManager.startPositionSaver();
taskManager.startWidgetUpdater();
writePlayerStatusPlaybackPreferences();
setupNotification(newInfo);
started = true;
startPosition = mediaPlayer.getPosition();
break;
case ERROR:
@ -472,9 +481,8 @@ public class PlaybackService extends Service {
}
Intent statusUpdate = new Intent(ACTION_PLAYER_STATUS_CHANGED);
statusUpdate.putExtra(EXTRA_NEW_PLAYER_STATUS, newInfo.playerStatus.ordinal());
// 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);
@ -537,11 +545,10 @@ public class PlaybackService extends Service {
};
private void endPlayback(boolean playNextEpisode) {
if (BuildConfig.DEBUG)
Log.d(TAG, "Playback ended");
Log.d(TAG, "Playback ended");
final Playable media = mediaPlayer.getPSMPInfo().playable;
if (media == null) {
final Playable playable = mediaPlayer.getPSMPInfo().playable;
if (playable == null) {
Log.e(TAG, "Cannot end playback: media was null");
return;
}
@ -551,36 +558,46 @@ public class PlaybackService extends Service {
boolean isInQueue = false;
FeedItem nextItem = null;
if (media instanceof FeedMedia) {
FeedItem item = ((FeedMedia) media).getItem();
if (playable instanceof FeedMedia) {
FeedMedia media = (FeedMedia) playable;
FeedItem item = media.getItem();
DBWriter.markItemRead(PlaybackService.this, item, true, true);
try {
final List<FeedItem> queue = taskManager.getQueue();
isInQueue = QueueAccess.ItemListAccess(queue).contains(((FeedMedia) media).getItem().getId());
isInQueue = QueueAccess.ItemListAccess(queue).contains(item.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.removeQueueItem(PlaybackService.this, item, true);
}
DBWriter.addItemToPlaybackHistory(PlaybackService.this, (FeedMedia) media);
DBWriter.addItemToPlaybackHistory(PlaybackService.this, media);
// auto-flattr if enabled
if (isAutoFlattrable(media) && UserPreferences.getAutoFlattrPlayedDurationThreshold() == 1.0f) {
DBTasks.flattrItemIfLoggedIn(PlaybackService.this, item);
}
//Delete episode if enabled
// Delete episode if enabled
if(UserPreferences.isAutoDelete()) {
DBWriter.deleteFeedMediaOfItem(PlaybackService.this, item.getMedia().getId());
if(BuildConfig.DEBUG)
Log.d(TAG, "Episode Deleted");
DBWriter.deleteFeedMediaOfItem(PlaybackService.this, media.getId());
Log.d(TAG, "Episode Deleted");
}
// gpodder play action
if(GpodnetPreferences.loggedIn()) {
GpodnetEpisodeAction action = new GpodnetEpisodeAction.Builder(item, Action.PLAY)
.currentDeviceId()
.currentTimestamp()
.started(startPosition / 1000)
.position(getDuration() / 1000)
.total(getDuration() / 1000)
.build();
GpodnetPreferences.enqueueEpisodeAction(action);
}
}
// Load next episode if previous episode was in the queue and if there
@ -596,8 +613,7 @@ public class PlaybackService extends Service {
UserPreferences.isFollowQueue();
if (loadNextItem) {
if (BuildConfig.DEBUG)
Log.d(TAG, "Loading next item in queue");
Log.d(TAG, "Loading next item in queue");
nextMedia = nextItem.getMedia();
}
final boolean prepareImmediately;
@ -605,13 +621,10 @@ public class PlaybackService extends Service {
final boolean stream;
if (playNextEpisode) {
if (BuildConfig.DEBUG)
Log.d(TAG, "Playback of next episode will start immediately.");
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");
Log.d(TAG, "No more episodes available to play");
prepareImmediately = startWhenPrepared = false;
stopForeground(true);
stopWidgetUpdater();
@ -619,7 +632,7 @@ public class PlaybackService extends Service {
writePlaybackPreferencesNoMediaPlaying();
if (nextMedia != null) {
stream = !media.localFileAvailable();
stream = !playable.localFileAvailable();
mediaPlayer.playMediaObject(nextMedia, stream, startWhenPrepared, prepareImmediately);
sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD,
(nextMedia.getMediaType() == MediaType.VIDEO) ? EXTRA_CODE_VIDEO : EXTRA_CODE_AUDIO);
@ -631,8 +644,7 @@ public class PlaybackService extends Service {
}
public void setSleepTimer(long waitingTime) {
if (BuildConfig.DEBUG)
Log.d(TAG, "Setting sleep timer to " + Long.toString(waitingTime)
Log.d(TAG, "Setting sleep timer to " + Long.toString(waitingTime)
+ " milliseconds");
taskManager.setSleepTimer(waitingTime);
sendNotificationBroadcast(NOTIFICATION_TYPE_SLEEPTIMER_UPDATE, 0);
@ -675,8 +687,7 @@ public class PlaybackService extends Service {
}
private void writePlaybackPreferences() {
if (BuildConfig.DEBUG)
Log.d(TAG, "Writing playback preferences");
Log.d(TAG, "Writing playback preferences");
SharedPreferences.Editor editor = PreferenceManager
.getDefaultSharedPreferences(getApplicationContext()).edit();
@ -727,8 +738,7 @@ public class PlaybackService extends Service {
}
private void writePlayerStatusPlaybackPreferences() {
if (BuildConfig.DEBUG)
Log.d(TAG, "Writing player status playback preferences");
Log.d(TAG, "Writing player status playback preferences");
SharedPreferences.Editor editor = PreferenceManager
.getDefaultSharedPreferences(getApplicationContext()).edit();
@ -777,8 +787,7 @@ public class PlaybackService extends Service {
@Override
protected Void doInBackground(Void... params) {
if (BuildConfig.DEBUG)
Log.d(TAG, "Starting background work");
Log.d(TAG, "Starting background work");
if (android.os.Build.VERSION.SDK_INT >= 11) {
if (info.playable != null) {
try {
@ -888,8 +897,7 @@ public class PlaybackService extends Service {
notification = notificationBuilder.build();
}
startForeground(NOTIFICATION_ID, notification);
if (BuildConfig.DEBUG)
Log.d(TAG, "Notification set up");
Log.d(TAG, "Notification set up");
}
}
@ -915,18 +923,15 @@ public class PlaybackService extends Service {
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);
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)));
FeedMedia media = (FeedMedia) playable;
FeedItem item = media.getItem();
media.setPlayedDuration(media.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())
if (isAutoFlattrable(media) &&
(media.getPlayedDuration() > UserPreferences.getAutoFlattrPlayedDurationThreshold() * duration)) {
Log.d(TAG, "saveCurrentPosition: performing auto flattr since played duration " + Integer.toString(media.getPlayedDuration())
+ " is " + UserPreferences.getAutoFlattrPlayedDurationThreshold() * 100 + "% of file duration " + Integer.toString(duration));
DBTasks.flattrItemIfLoggedIn(this, item);
}
@ -1019,8 +1024,7 @@ public class PlaybackService extends Service {
editor.apply();
}
if (BuildConfig.DEBUG)
Log.d(TAG, "RemoteControlClient state was refreshed");
Log.d(TAG, "RemoteControlClient state was refreshed");
}
}
}
@ -1063,15 +1067,12 @@ public class PlaybackService extends Service {
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);
Log.d(TAG, "Headset plug event. State is " + state);
if (state == UNPLUGGED) {
if (BuildConfig.DEBUG)
Log.d(TAG, "Headset was unplugged during playback.");
Log.d(TAG, "Headset was unplugged during playback.");
pauseIfPauseOnDisconnect();
} else if (state == PLUGGED) {
if (BuildConfig.DEBUG)
Log.d(TAG, "Headset was plugged in during playback.");
Log.d(TAG, "Headset was plugged in during playback.");
unpauseIfPauseOnDisconnect();
}
} else {
@ -1088,8 +1089,7 @@ public class PlaybackService extends Service {
int state = intent.getIntExtra(AudioManager.EXTRA_SCO_AUDIO_STATE, -1);
int prevState = intent.getIntExtra(AudioManager.EXTRA_SCO_AUDIO_PREVIOUS_STATE, -1);
if (state == AudioManager.SCO_AUDIO_STATE_CONNECTED) {
if (BuildConfig.DEBUG)
Log.d(TAG, "Received bluetooth connection intent");
Log.d(TAG, "Received bluetooth connection intent");
unpauseIfPauseOnDisconnect();
}
}
@ -1101,8 +1101,7 @@ public class PlaybackService extends Service {
@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");
Log.d(TAG, "Pausing playback because audio is becoming noisy");
pauseIfPauseOnDisconnect();
}
// android.media.AUDIO_BECOMING_NOISY
@ -1148,8 +1147,7 @@ public class PlaybackService extends Service {
@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");
Log.d(TAG, "Received SKIP_CURRENT_EPISODE intent");
mediaPlayer.endPlayback();
}
}
@ -1159,8 +1157,7 @@ public class PlaybackService extends Service {
@Override
public void onReceive(Context context, Intent intent) {
if (StringUtils.equals(intent.getAction(), ACTION_RESUME_PLAY_CURRENT_EPISODE)) {
if (BuildConfig.DEBUG)
Log.d(TAG, "Received RESUME_PLAY_CURRENT_EPISODE intent");
Log.d(TAG, "Received RESUME_PLAY_CURRENT_EPISODE intent");
mediaPlayer.resume();
}
}
@ -1170,8 +1167,7 @@ public class PlaybackService extends Service {
@Override
public void onReceive(Context context, Intent intent) {
if (StringUtils.equals(intent.getAction(), ACTION_PAUSE_PLAY_CURRENT_EPISODE)) {
if (BuildConfig.DEBUG)
Log.d(TAG, "Received PAUSE_PLAY_CURRENT_EPISODE intent");
Log.d(TAG, "Received PAUSE_PLAY_CURRENT_EPISODE intent");
mediaPlayer.pause(false, false);
}
}
@ -1231,7 +1227,26 @@ public class PlaybackService extends Service {
public void seekTo(final int t) {
if(mediaPlayer.getPlayerStatus() == PlayerStatus.PLAYING
&& GpodnetPreferences.loggedIn()) {
final Playable playable = mediaPlayer.getPSMPInfo().playable;
if (playable instanceof FeedMedia) {
FeedMedia media = (FeedMedia) playable;
FeedItem item = media.getItem();
GpodnetEpisodeAction action = new GpodnetEpisodeAction.Builder(item, Action.PLAY)
.currentDeviceId()
.currentTimestamp()
.started(startPosition / 1000)
.position(getCurrentPosition() / 1000)
.total(getDuration() / 1000)
.build();
GpodnetPreferences.enqueueEpisodeAction(action);
}
}
mediaPlayer.seekTo(t);
if(mediaPlayer.getPlayerStatus() == PlayerStatus.PLAYING ) {
startPosition = t;
}
}
@ -1270,10 +1285,9 @@ public class PlaybackService extends Service {
return mediaPlayer.getVideoSize();
}
private boolean isAutoFlattrable(Playable p) {
if (p != null && p instanceof FeedMedia) {
FeedMedia media = (FeedMedia) p;
FeedItem item = ((FeedMedia) p).getItem();
private boolean isAutoFlattrable(FeedMedia media) {
if (media != null) {
FeedItem item = media.getItem();
return item != null && FlattrUtils.hasToken() && UserPreferences.isAutoFlattr() && item.getPaymentLink() != null && item.getFlattrStatus().getUnflattred();
} else {
return false;

View File

@ -24,11 +24,13 @@ 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.FeedItem;
import de.danoeh.antennapod.core.feed.FeedMedia;
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.storage.DBWriter;
import de.danoeh.antennapod.core.util.playback.AudioPlayer;
import de.danoeh.antennapod.core.util.playback.IPlayer;
import de.danoeh.antennapod.core.util.playback.Playable;
@ -91,7 +93,7 @@ public class PlaybackServiceMediaPlayer {
new RejectedExecutionHandler() {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
if (BuildConfig.DEBUG) Log.d(TAG, "Rejected execution of runnable");
Log.d(TAG, "Rejected execution of runnable");
}
}
);
@ -137,7 +139,7 @@ public class PlaybackServiceMediaPlayer {
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.");
Log.d(TAG, "playMediaObject(...)");
executor.submit(new Runnable() {
@Override
public void run() {
@ -164,16 +166,16 @@ public class PlaybackServiceMediaPlayer {
*/
private void playMediaObject(final Playable playable, final boolean forceReset, final boolean stream, final boolean startWhenPrepared, final boolean prepareImmediately) {
Validate.notNull(playable);
if (!playerLock.isHeldByCurrentThread())
if (!playerLock.isHeldByCurrentThread()) {
throw new IllegalStateException("method requires playerLock");
}
if (media != null) {
if (!forceReset && media.getIdentifier().equals(playable.getIdentifier())
&& playerStatus == PlayerStatus.PLAYING) {
// episode is already playing -> ignore method call
if (BuildConfig.DEBUG)
Log.d(TAG, "Method call to playMediaObject was ignored: media file already playing.");
Log.d(TAG, "Method call to playMediaObject was ignored: media file already playing.");
return;
} else {
// stop playback of this episode
@ -184,6 +186,23 @@ public class PlaybackServiceMediaPlayer {
if (playerStatus == PlayerStatus.PLAYING) {
setPlayerStatus(PlayerStatus.PAUSED, media);
}
// smart mark as played
if(media != null && media instanceof FeedMedia) {
FeedMedia oldMedia = (FeedMedia) media;
if(oldMedia.hasAlmostEnded()) {
Log.d(TAG, "smart mark as read");
FeedItem item = oldMedia.getItem();
DBWriter.markItemRead(context, item, true, false);
DBWriter.removeQueueItem(context, item, false);
DBWriter.addItemToPlaybackHistory(context, oldMedia);
if (UserPreferences.isAutoDelete()) {
Log.d(TAG, "Delete " + oldMedia.toString());
DBWriter.deleteFeedMediaOfItem(context, oldMedia.getId());
}
}
}
setPlayerStatus(PlayerStatus.INDETERMINATE, null);
}
}
@ -281,11 +300,10 @@ public class PlaybackServiceMediaPlayer {
media.onPlaybackStart();
} else {
if (BuildConfig.DEBUG) Log.e(TAG, "Failed to request audio focus");
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);
Log.d(TAG, "Call to resume() was ignored because current state of PSMP object is " + playerStatus);
}
}
@ -307,8 +325,7 @@ public class PlaybackServiceMediaPlayer {
playerLock.lock();
releaseWifiLockIfNecessary();
if (playerStatus == PlayerStatus.PLAYING) {
if (BuildConfig.DEBUG)
Log.d(TAG, "Pausing playback.");
Log.d(TAG, "Pausing playback.");
mediaPlayer.pause();
setPlayerStatus(PlayerStatus.PAUSED, media);
@ -320,8 +337,7 @@ public class PlaybackServiceMediaPlayer {
reinit();
}
} else {
if (BuildConfig.DEBUG)
Log.d(TAG, "Ignoring call to pause: Player is in " + playerStatus + " state");
Log.d(TAG, "Ignoring call to pause: Player is in " + playerStatus + " state");
}
playerLock.unlock();
@ -342,8 +358,7 @@ public class PlaybackServiceMediaPlayer {
playerLock.lock();
if (playerStatus == PlayerStatus.INITIALIZED) {
if (BuildConfig.DEBUG)
Log.d(TAG, "Preparing media player");
Log.d(TAG, "Preparing media player");
setPlayerStatus(PlayerStatus.PREPARING, media);
try {
mediaPlayer.prepare();
@ -370,8 +385,7 @@ public class PlaybackServiceMediaPlayer {
throw new IllegalStateException("Player is not in PREPARING state");
}
if (BuildConfig.DEBUG)
Log.d(TAG, "Resource prepared");
Log.d(TAG, "Resource prepared");
if (mediaType == MediaType.VIDEO) {
VideoPlayer vp = (VideoPlayer) mediaPlayer;
@ -383,8 +397,7 @@ public class PlaybackServiceMediaPlayer {
}
if (media.getDuration() == 0) {
if (BuildConfig.DEBUG)
Log.d(TAG, "Setting duration of media");
Log.d(TAG, "Setting duration of media");
media.setDuration(mediaPlayer.getDuration());
}
setPlayerStatus(PlayerStatus.PREPARED, media);
@ -412,8 +425,7 @@ public class PlaybackServiceMediaPlayer {
} else if (mediaPlayer != null) {
mediaPlayer.reset();
} else {
if (BuildConfig.DEBUG)
Log.d(TAG, "Call to reinit was ignored: media and mediaPlayer were null");
Log.d(TAG, "Call to reinit was ignored: media and mediaPlayer were null");
}
playerLock.unlock();
}
@ -437,15 +449,15 @@ public class PlaybackServiceMediaPlayer {
if (playerStatus == PlayerStatus.PLAYING
|| playerStatus == PlayerStatus.PAUSED
|| playerStatus == PlayerStatus.PREPARED) {
if (stream) {
// statusBeforeSeeking = playerStatus;
// setPlayerStatus(PlayerStatus.SEEKING, media);
if (!stream) {
statusBeforeSeeking = playerStatus;
setPlayerStatus(PlayerStatus.SEEKING, media);
}
mediaPlayer.seekTo(t);
} else if (playerStatus == PlayerStatus.INITIALIZED) {
media.setPosition(t);
startWhenPrepared.set(true);
startWhenPrepared.set(false);
prepare();
}
playerLock.unlock();
@ -522,20 +534,20 @@ public class PlaybackServiceMediaPlayer {
* 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;
}
playerLock.lock();
int retVal = INVALID_TIME;
if (playerStatus == PlayerStatus.PLAYING
|| playerStatus == PlayerStatus.PAUSED
|| playerStatus == PlayerStatus.PREPARED) {
|| playerStatus == PlayerStatus.PREPARED
|| playerStatus == PlayerStatus.SEEKING) {
retVal = mediaPlayer.getCurrentPosition();
} else if (media != null && media.getPosition() > 0) {
retVal = media.getPosition();
}
playerLock.unlock();
Log.d(TAG, "getPosition() -> " + retVal);
return retVal;
}
@ -567,8 +579,7 @@ public class PlaybackServiceMediaPlayer {
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);
Log.d(TAG, "Playback speed was set to " + speed);
callback.playbackSpeedChanged(speed);
}
}
@ -651,8 +662,7 @@ public class PlaybackServiceMediaPlayer {
@Override
public void run() {
playerLock.lock();
if (BuildConfig.DEBUG)
Log.d(TAG, "Resetting video surface");
Log.d(TAG, "Resetting video surface");
mediaPlayer.setDisplay(null);
reinit();
playerLock.unlock();
@ -716,7 +726,7 @@ public class PlaybackServiceMediaPlayer {
private synchronized void setPlayerStatus(PlayerStatus newStatus, Playable newMedia) {
Validate.notNull(newStatus);
if (BuildConfig.DEBUG) Log.d(TAG, "Setting player status to " + newStatus);
Log.d(TAG, "Setting player status to " + newStatus);
this.playerStatus = newStatus;
this.media = newMedia;
@ -725,6 +735,7 @@ public class PlaybackServiceMediaPlayer {
int state;
if (playerStatus != null) {
Log.d(TAG, "playerStatus: " + playerStatus.toString());
switch (playerStatus) {
case PLAYING:
state = PlaybackStateCompat.STATE_PLAYING;
@ -788,17 +799,15 @@ public class PlaybackServiceMediaPlayer {
// 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.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");
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");
Log.d(TAG, "Gained audio focus");
if (pausedBecauseOfTransientAudiofocusLoss) { // we paused => play now
resume();
} else { // we ducked => raise audio level back
@ -808,22 +817,19 @@ public class PlaybackServiceMediaPlayer {
} 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...");
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...");
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...");
Log.d(TAG, "Lost audio focus temporarily. Pausing...");
pause(false, false);
pausedBecauseOfTransientAudiofocusLoss = true;
}
@ -873,8 +879,7 @@ public class PlaybackServiceMediaPlayer {
if (playerStatus == PlayerStatus.INDETERMINATE) {
setPlayerStatus(PlayerStatus.STOPPED, null);
} else {
if (BuildConfig.DEBUG)
Log.d(TAG, "Ignored call to stop: Current player state is: " + playerStatus);
Log.d(TAG, "Ignored call to stop: Current player state is: " + playerStatus);
}
playerLock.unlock();
@ -1091,13 +1096,13 @@ public class PlaybackServiceMediaPlayer {
@Override
public void onFastForward() {
super.onFastForward();
seekDelta(UserPreferences.getSeekDeltaMs());
seekDelta(UserPreferences.getFastFowardSecs() * 1000);
}
@Override
public void onRewind() {
super.onRewind();
seekDelta(-UserPreferences.getSeekDeltaMs());
seekDelta(-UserPreferences.getRewindSecs() * 1000);
}
@Override

View File

@ -5,14 +5,23 @@ import android.util.Log;
import org.apache.commons.lang3.Validate;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import de.danoeh.antennapod.core.BuildConfig;
import de.danoeh.antennapod.core.feed.EventDistributor;
import de.danoeh.antennapod.core.feed.FeedItem;
import de.danoeh.antennapod.core.feed.QueueEvent;
import de.danoeh.antennapod.core.storage.DBReader;
import de.danoeh.antennapod.core.util.playback.Playable;
import java.util.List;
import java.util.concurrent.*;
import de.greenrobot.event.EventBus;
/**
* Manages the background tasks of PlaybackSerivce, i.e.
@ -69,18 +78,13 @@ public class PlaybackServiceTaskManager {
}
});
loadQueue();
EventDistributor.getInstance().register(eventDistributorListener);
EventBus.getDefault().register(this);
}
private final EventDistributor.EventListener eventDistributorListener = new EventDistributor.EventListener() {
@Override
public void update(EventDistributor eventDistributor, Integer arg) {
if ((EventDistributor.QUEUE_UPDATE & arg) != 0) {
cancelQueueLoader();
loadQueue();
}
}
};
public void onEvent(QueueEvent event) {
cancelQueueLoader();
loadQueue();
}
private synchronized boolean isQueueLoaderActive() {
return queueFuture != null && !queueFuture.isDone();
@ -145,9 +149,9 @@ public class PlaybackServiceTaskManager {
positionSaverFuture = schedExecutor.scheduleWithFixedDelay(positionSaver, POSITION_SAVER_WAITING_INTERVAL,
POSITION_SAVER_WAITING_INTERVAL, TimeUnit.MILLISECONDS);
if (BuildConfig.DEBUG) Log.d(TAG, "Started PositionSaver");
Log.d(TAG, "Started PositionSaver");
} else {
if (BuildConfig.DEBUG) Log.d(TAG, "Call to startPositionSaver was ignored.");
Log.d(TAG, "Call to startPositionSaver was ignored.");
}
}
@ -312,7 +316,7 @@ public class PlaybackServiceTaskManager {
* execution of this method.
*/
public synchronized void shutdown() {
EventDistributor.getInstance().unregister(eventDistributorListener);
EventBus.getDefault().unregister(this);
cancelAllTasks();
schedExecutor.shutdown();
}

View File

@ -12,7 +12,7 @@ import java.util.concurrent.ExecutionException;
import de.danoeh.antennapod.core.feed.FeedItem;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.util.QueueAccess;
import de.danoeh.antennapod.core.util.LongList;
/**
* Implementation of the EpisodeCleanupAlgorithm interface used by AntennaPod.
@ -24,7 +24,7 @@ public class APCleanupAlgorithm implements EpisodeCleanupAlgorithm<Integer> {
public int performCleanup(Context context, Integer episodeNumber) {
List<FeedItem> candidates = new ArrayList<FeedItem>();
List<FeedItem> downloadedItems = DBReader.getDownloadedItems(context);
QueueAccess queue = QueueAccess.IDListAccess(DBReader.getQueueIDList(context));
LongList queue = DBReader.getQueueIDList(context);
List<FeedItem> delete;
for (FeedItem item : downloadedItems) {
if (item.hasMedia() && item.getMedia().isDownloaded()

View File

@ -2,7 +2,6 @@ package de.danoeh.antennapod.core.storage;
import android.content.Context;
import android.database.Cursor;
import android.database.SQLException;
import android.util.Log;
import java.util.ArrayList;
@ -22,6 +21,7 @@ import de.danoeh.antennapod.core.feed.SimpleChapter;
import de.danoeh.antennapod.core.feed.VorbisCommentChapter;
import de.danoeh.antennapod.core.service.download.DownloadStatus;
import de.danoeh.antennapod.core.util.DownloadError;
import de.danoeh.antennapod.core.util.LongList;
import de.danoeh.antennapod.core.util.comparator.DownloadStatusComparator;
import de.danoeh.antennapod.core.util.comparator.FeedItemPubdateComparator;
import de.danoeh.antennapod.core.util.comparator.PlaybackCompletionDateComparator;
@ -339,8 +339,7 @@ public final class DBReader {
}
static List<FeedItem> getQueue(Context context, PodDBAdapter adapter) {
if (BuildConfig.DEBUG)
Log.d(TAG, "Extracting queue");
Log.d(TAG, "getQueue()");
Cursor itemlistCursor = adapter.getQueueCursor();
List<FeedItem> items = extractItemlistFromCursor(adapter,
@ -359,21 +358,21 @@ public final class DBReader {
* @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) {
public static LongList getQueueIDList(Context context) {
PodDBAdapter adapter = new PodDBAdapter(context);
adapter.open();
List<Long> result = getQueueIDList(adapter);
LongList result = getQueueIDList(adapter);
adapter.close();
return result;
}
static List<Long> getQueueIDList(PodDBAdapter adapter) {
static LongList getQueueIDList(PodDBAdapter adapter) {
adapter.open();
Cursor queueCursor = adapter.getQueueIDCursor();
List<Long> queueIds = new ArrayList<Long>(queueCursor.getCount());
LongList queueIds = new LongList(queueCursor.getCount());
if (queueCursor.moveToFirst()) {
do {
queueIds.add(queueCursor.getLong(0));
@ -383,6 +382,22 @@ public final class DBReader {
}
/**
* Return the size of the queue.
*
* @param context A context that is used for opening a database connection.
* @return Size of the queue.
*/
public static int getQueueSize(Context context) {
Log.d(TAG, "getQueueSize()");
PodDBAdapter adapter = new PodDBAdapter(context);
adapter.open();
int size = adapter.getQueueSize();
adapter.close();
return size;
}
/**
* Loads a list of the FeedItems in the queue. If the FeedItems of the queue are not used directly, consider using
* {@link #getQueueIDList(android.content.Context)} instead.
@ -392,8 +407,7 @@ public final class DBReader {
* 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");
Log.d(TAG, "getQueue()");
PodDBAdapter adapter = new PodDBAdapter(context);
adapter.open();
@ -680,6 +694,42 @@ public final class DBReader {
}
static FeedItem getFeedItem(final Context context, final String podcastUrl, final String episodeUrl, PodDBAdapter adapter) {
Log.d(TAG, "Loading feeditem with podcast url " + podcastUrl + " and episode url " + episodeUrl);
FeedItem item = null;
Cursor itemCursor = adapter.getFeedItemCursor(podcastUrl, episodeUrl);
if (itemCursor.moveToFirst()) {
List<FeedItem> list = extractItemlistFromCursor(adapter, itemCursor);
if (list.size() > 0) {
item = list.get(0);
loadFeedDataOfFeedItemlist(context, list);
if (item.hasChapters()) {
loadChaptersOfFeedItem(adapter, item);
}
}
}
return item;
}
/**
* Loads a specific FeedItem from the database.
*
* @param context A context that is used for opening a database connection.
* @param podcastUrl the corresponding feed's url
* @param episodeUrl the feed item's url
* @return The FeedItem or null if the FeedItem could not be found. All FeedComponent-attributes
* as well as chapter marks of the FeedItem will also be loaded from the database.
*/
public static FeedItem getFeedItem(final Context context, final String podcastUrl, final String episodeUrl) {
Log.d(TAG, "Loading feeditem with podcast url " + podcastUrl + " and episode url " + episodeUrl);
PodDBAdapter adapter = new PodDBAdapter(context);
adapter.open();
FeedItem item = getFeedItem(context, podcastUrl, episodeUrl, adapter);
adapter.close();
return item;
}
/**
* Loads additional information about a FeedItem, e.g. shownotes
*

View File

@ -34,7 +34,7 @@ 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.QueueAccess;
import de.danoeh.antennapod.core.util.LongList;
import de.danoeh.antennapod.core.util.comparator.FeedItemPubdateComparator;
import de.danoeh.antennapod.core.util.exception.MediaFileNotFoundException;
import de.danoeh.antennapod.core.util.flattr.FlattrUtils;
@ -524,8 +524,8 @@ public final class DBTasks {
* @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);
LongList queue = DBReader.getQueueIDList(context);
return queue.contains(feedItemId);
}
private static Feed searchFeedByIdentifyingValueOrID(Context context, PodDBAdapter adapter,

View File

@ -7,6 +7,7 @@ import android.content.SharedPreferences;
import android.database.Cursor;
import android.preference.PreferenceManager;
import android.util.Log;
import org.shredzone.flattr4j.model.Flattr;
import java.io.File;
@ -32,15 +33,18 @@ import de.danoeh.antennapod.core.feed.FeedImage;
import de.danoeh.antennapod.core.feed.FeedItem;
import de.danoeh.antennapod.core.feed.FeedMedia;
import de.danoeh.antennapod.core.feed.FeedPreferences;
import de.danoeh.antennapod.core.feed.QueueEvent;
import de.danoeh.antennapod.core.gpoddernet.model.GpodnetEpisodeAction;
import de.danoeh.antennapod.core.preferences.GpodnetPreferences;
import de.danoeh.antennapod.core.preferences.PlaybackPreferences;
import de.danoeh.antennapod.core.preferences.UserPreferences;
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.LongList;
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 de.greenrobot.event.EventBus;
/**
* Provides methods for writing data to AntennaPod's database.
@ -120,10 +124,18 @@ public class DBWriter {
PlaybackService.ACTION_SHUTDOWN_PLAYBACK_SERVICE));
}
}
// Gpodder: queue delete action for synchronization
if(GpodnetPreferences.loggedIn()) {
FeedItem item = media.getItem();
GpodnetEpisodeAction action = new GpodnetEpisodeAction.Builder(item, GpodnetEpisodeAction.Action.DELETE)
.currentDeviceId()
.currentTimestamp()
.build();
GpodnetPreferences.enqueueEpisodeAction(action);
}
}
if (BuildConfig.DEBUG)
Log.d(TAG, "Deleting File. Result: " + result);
EventDistributor.getInstance().sendQueueUpdateBroadcast();
Log.d(TAG, "Deleting File. Result: " + result);
EventBus.getDefault().post(new QueueEvent(QueueEvent.Action.DELETED_MEDIA, media.getItem()));
EventDistributor.getInstance().sendUnreadItemsUpdateBroadcast();
}
}
@ -337,8 +349,7 @@ public class DBWriter {
public void run() {
final PodDBAdapter adapter = new PodDBAdapter(context);
adapter.open();
final List<FeedItem> queue = DBReader
.getQueue(context, adapter);
final List<FeedItem> queue = DBReader.getQueue(context, adapter);
FeedItem item = null;
if (queue != null) {
@ -358,8 +369,7 @@ public class DBWriter {
}
if (queueModified) {
adapter.setQueue(queue);
EventDistributor.getInstance()
.sendQueueUpdateBroadcast();
EventBus.getDefault().post(new QueueEvent(QueueEvent.Action.ADDED, item, index));
}
if (unreadItemsModified && item != null) {
adapter.setSingleFeedItem(item);
@ -427,8 +437,7 @@ public class DBWriter {
}
if (queueModified) {
adapter.setQueue(queue);
EventDistributor.getInstance()
.sendQueueUpdateBroadcast();
EventBus.getDefault().post(new QueueEvent(QueueEvent.Action.ADDED_ITEMS, queue));
}
if (unreadItemsModified) {
adapter.setFeedItemlist(itemsToSave);
@ -459,7 +468,7 @@ public class DBWriter {
adapter.clearQueue();
adapter.close();
EventDistributor.getInstance().sendQueueUpdateBroadcast();
EventBus.getDefault().post(new QueueEvent(QueueEvent.Action.CLEARED));
}
});
}
@ -468,34 +477,25 @@ public class DBWriter {
* 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 item 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) {
final FeedItem item, final boolean performAutoDownload) {
return dbExec.submit(new Runnable() {
@Override
public void run() {
final PodDBAdapter adapter = new PodDBAdapter(context);
adapter.open();
final List<FeedItem> queue = DBReader
.getQueue(context, adapter);
FeedItem item = null;
final List<FeedItem> queue = DBReader.getQueue(context, adapter);
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) {
int position = queue.indexOf(item);
if(position >= 0) {
queue.remove(position);
adapter.setQueue(queue);
EventDistributor.getInstance()
.sendQueueUpdateBroadcast();
EventBus.getDefault().post(new QueueEvent(QueueEvent.Action.REMOVED, item, position));
} else {
Log.w(TAG, "Queue was not modified by call to removeQueueItem");
}
@ -523,16 +523,13 @@ public class DBWriter {
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++;
LongList queueIdList = DBReader.getQueueIDList(context);
int index = queueIdList.indexOf(itemId);
if (index >=0) {
moveQueueItemHelper(context, index, 0, broadcastUpdate);
} else {
Log.e(TAG, "moveQueueItemToTop: item not found");
}
Log.e(TAG, "moveQueueItemToTop: item not found");
}
});
}
@ -550,17 +547,14 @@ public class DBWriter {
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++;
LongList queueIdList = DBReader.getQueueIDList(context);
int index = queueIdList.indexOf(itemId);
if(index >= 0) {
moveQueueItemHelper(context, index, queueIdList.size() - 1,
broadcastUpdate);
} else {
Log.e(TAG, "moveQueueItemToBottom: item not found");
}
Log.e(TAG, "moveQueueItemToBottom: item not found");
}
});
}
@ -614,8 +608,8 @@ public class DBWriter {
adapter.setQueue(queue);
if (broadcastUpdate) {
EventDistributor.getInstance()
.sendQueueUpdateBroadcast();
EventBus.getDefault().post(new QueueEvent(QueueEvent.Action.REMOVED, item));
EventBus.getDefault().post(new QueueEvent(QueueEvent.Action.ADDED, item, to));
}
}
@ -625,6 +619,19 @@ public class DBWriter {
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 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);
}
/**
* Sets the 'read'-attribute of a FeedItem to the specified value.
*
@ -639,18 +646,6 @@ public class DBWriter {
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) {
@ -1036,8 +1031,7 @@ public class DBWriter {
Collections.sort(queue, comparator);
adapter.setQueue(queue);
if (broadcastUpdate) {
EventDistributor.getInstance()
.sendQueueUpdateBroadcast();
EventBus.getDefault().post(new QueueEvent(QueueEvent.Action.SORTED));
}
} else {
Log.e(TAG, "sortQueue: Could not load queue");

View File

@ -1120,7 +1120,11 @@ public class PodDBAdapter {
return c;
}
public final Cursor getFeedItemCursor(final String... ids) {
public final Cursor getFeedItemCursor(final String id) {
return getFeedItemCursor(new String[] { id });
}
public final Cursor getFeedItemCursor(final String[] ids) {
if (ids.length > IN_OPERATOR_MAXIMUM) {
throw new IllegalArgumentException(
"number of IDs must not be larger than "
@ -1133,6 +1137,15 @@ public class PodDBAdapter {
}
public final Cursor getFeedItemCursor(final String podcastUrl, final String episodeUrl) {
final String query = "SELECT " + SEL_FI_SMALL_STR + " FROM " + TABLE_NAME_FEED_ITEMS
+ " INNER JOIN " +
TABLE_NAME_FEEDS + " ON " + TABLE_NAME_FEED_ITEMS + "." + KEY_FEED + "=" +
TABLE_NAME_FEEDS + "." + KEY_ID + " WHERE " + TABLE_NAME_FEED_ITEMS + "." + KEY_ITEM_IDENTIFIER + "='" +
episodeUrl + "' AND " + TABLE_NAME_FEEDS + "." + KEY_DOWNLOAD_URL + "='" + podcastUrl + "'";
return db.rawQuery(query, 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);

View File

@ -3,7 +3,7 @@ package de.danoeh.antennapod.core.syndication.namespace;
import org.xml.sax.Attributes;
import de.danoeh.antennapod.core.syndication.handler.HandlerState;
import de.danoeh.antennapod.core.syndication.util.SyndDateUtils;
import de.danoeh.antennapod.core.util.DateUtils;
public class NSDublinCore extends Namespace {
private static final String TAG = "NSDublinCore";
@ -30,7 +30,7 @@ public class NSDublinCore extends Namespace {
String second = secondElement.getName();
if (top.equals(DATE) && second.equals(ITEM)) {
state.getCurrentItem().setPubDate(
SyndDateUtils.parseISO8601Date(content));
DateUtils.parse(content));
}
}
}

View File

@ -1,14 +1,16 @@
package de.danoeh.antennapod.core.syndication.namespace;
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.util.SyndDateUtils;
import de.danoeh.antennapod.core.syndication.util.SyndTypeUtils;
import org.xml.sax.Attributes;
import de.danoeh.antennapod.core.util.DateUtils;
/**
* SAX-Parser for reading RSS-Feeds
@ -129,7 +131,7 @@ public class NSRSS20 extends Namespace {
}
} else if (top.equals(PUBDATE) && second.equals(ITEM)) {
state.getCurrentItem().setPubDate(
SyndDateUtils.parseRFC822Date(content));
DateUtils.parse(content));
} else if (top.equals(URL) && second.equals(IMAGE) && third != null
&& third.equals(CHANNEL)) {
state.getFeed().getImage().setDownload_url(content);

View File

@ -10,7 +10,7 @@ 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;
import de.danoeh.antennapod.core.util.DateUtils;
public class NSSimpleChapters extends Namespace {
private static final String TAG = "NSSimpleChapters";
@ -33,7 +33,7 @@ public class NSSimpleChapters extends Namespace {
try {
state.getCurrentItem()
.getChapters()
.add(new SimpleChapter(SyndDateUtils
.add(new SimpleChapter(DateUtils
.parseTimeString(attributes.getValue(START)),
attributes.getValue(TITLE), state.getCurrentItem(),
attributes.getValue(HREF)));

View File

@ -13,8 +13,8 @@ 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;
import de.danoeh.antennapod.core.util.DateUtils;
public class NSAtom extends Namespace {
private static final String TAG = "NSAtom";
@ -191,12 +191,12 @@ public class NSAtom extends Namespace {
if (second.equals(ENTRY)
&& state.getCurrentItem().getPubDate() == null) {
state.getCurrentItem().setPubDate(
SyndDateUtils.parseRFC3339Date(content));
DateUtils.parse(content));
}
} else if (top.equals(PUBLISHED)) {
if (second.equals(ENTRY)) {
state.getCurrentItem().setPubDate(
SyndDateUtils.parseRFC3339Date(content));
DateUtils.parse(content));
}
} else if (top.equals(IMAGE)) {
state.getFeed().setImage(new FeedImage(state.getFeed(), content, null));

View File

@ -1,194 +0,0 @@
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;
import de.danoeh.antennapod.core.BuildConfig;
/**
* 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",
"dd MMM yy HH:mm 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";
public static final String ISO8601_SHORT = "yyyy-MM-dd";
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);
}
};
private static ThreadLocal<SimpleDateFormat> ISO8601ShortFormatter = new ThreadLocal<SimpleDateFormat>() {
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat(ISO8601_SHORT, 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 (String RFC822DATE : RFC822DATES) {
try {
format.applyPattern(RFC822DATE);
result = format.parse(date);
break;
} catch (ParseException e) {
if (BuildConfig.DEBUG) Log.d(TAG, "ParserException", e);
}
}
if (result == null) {
Log.e(TAG, "Unable to parse feed date correctly:" + date);
}
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;
}
public static Date parseISO8601Date(String date) {
if(date.length() > ISO8601_SHORT.length()) {
return parseRFC3339Date(date);
}
Date result = null;
if(date.length() == "YYYYMMDD".length()) {
date = date.substring(0, 4) + "-" + date.substring(4, 6) + "-" + date.substring(6,8);
}
SimpleDateFormat format = ISO8601ShortFormatter.get();
try {
result = format.parse(date);
} catch (ParseException e) {
e.printStackTrace();
}
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++;
}
if (parts.length >= 2) {
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);
}
}

View File

@ -0,0 +1,140 @@
package de.danoeh.antennapod.core.util;
import org.apache.commons.lang3.StringUtils;
import java.text.ParsePosition;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
/**
* Parses several date formats.
*/
public class DateUtils {
private static final String TAG = "DateUtils";
private static final String[] RFC822DATES = {"dd MMM yy HH:mm:ss Z",
"dd MMM yy HH:mm 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";
public static final String ISO8601_SHORT = "yyyy-MM-dd";
private static ThreadLocal<SimpleDateFormat> RFC822Formatter = new ThreadLocal<SimpleDateFormat>() {
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("dd MMM yy HH:mm:ss Z", Locale.US);
}
};
private static ThreadLocal<SimpleDateFormat> RFC3339Formatter = new ThreadLocal<SimpleDateFormat>() {
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US);
}
};
public static Date parse(String date) {
if(date == null) {
throw new IllegalArgumentException("Date most not be null");
}
date = date.replace('/', ' ');
date = date.replace('-', ' ');
if(date.contains(".")) {
int start = date.indexOf('.');
int current = start+1;
while(current < date.length() && Character.isDigit(date.charAt(current))) {
current++;
}
if(current - start > 4) {
if(current < date.length()-1) {
date = date.substring(0, start + 4) + date.substring(current);
} else {
date = date.substring(0, start + 4);
}
} else if(current - start < 4) {
if(current < date.length()-1) {
date = date.substring(0, current) + StringUtils.repeat("0", 4-(current-start)) + date.substring(current);
} else {
date = date.substring(0, current) + StringUtils.repeat("0", 4-(current-start));
}
}
}
String[] patterns = {
"dd MMM yy HH:mm:ss Z",
"dd MMM yy HH:mm Z",
"EEE, dd MMM yyyy HH:mm:ss Z",
"EEEE, dd MMM yy HH:mm:ss Z",
"EEE MMM d HH:mm:ss yyyy",
"yyyy MM dd'T'HH:mm:ss",
"yyyy MM dd'T'HH:mm:ss.SSS",
"yyyy MM dd'T'HH:mm:ss.SSS Z",
"yyyy MM dd'T'HH:mm:ssZ",
"yyyy MM dd'T'HH:mm:ss'Z'",
"yyyy MM ddZ",
"yyyy MM dd"
};
SimpleDateFormat parser = new SimpleDateFormat("", Locale.US);
parser.setLenient(false);
ParsePosition pos = new ParsePosition(0);
for(String pattern : patterns) {
parser.applyPattern(pattern);
pos.setIndex(0);
Date result = parser.parse(date, pos);
if(result != null && pos.getIndex() == date.length()) {
return result;
}
}
return null;
}
/**
* 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++;
}
if (parts.length >= 2) {
result += Integer.valueOf(parts[idx]) * 60000L;
idx++;
result += (Float.valueOf(parts[idx])) * 1000L;
}
return result;
}
public static String formatRFC822Date(Date date) {
SimpleDateFormat format = new SimpleDateFormat("dd MMM yy HH:mm:ss Z", Locale.US);
return format.format(date);
}
public static String formatRFC3339Local(Date date) {
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.US);
return format.format(date);
}
public static String formatRFC3339UTC(Date date) {
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US);
return format.format(date);
}
}

View File

@ -0,0 +1,241 @@
package de.danoeh.antennapod.core.util;
import java.util.Arrays;
/**
* Fast and memory efficient long list
*/
public final class LongList {
private long[] values;
private int size;
/**
* Constructs an empty instance with a default initial capacity.
*/
public LongList() {
this(4);
}
/**
* Constructs an empty instance.
*
* @param initialCapacity {@code >= 0;} initial capacity of the list
*/
public LongList(int initialCapacity) {
if(initialCapacity < 0) {
throw new IllegalArgumentException("initial capacity must be 0 or higher");
}
values = new long[initialCapacity];
size = 0;
}
@Override
public int hashCode() {
Arrays.hashCode(values);
int hashCode = 1;
for (int i = 0; i < size; i++) {
long value = values[i];
hashCode = 31 * hashCode + (int)(value ^ (value >>> 32));
}
return hashCode;
}
@Override
public boolean equals(Object other) {
if (other == this) {
return true;
}
if (! (other instanceof LongList)) {
return false;
}
LongList otherList = (LongList) other;
if (size != otherList.size) {
return false;
}
for (int i = 0; i < size; i++) {
if (values[i] != otherList.values[i]) {
return false;
}
}
return true;
}
@Override
public String toString() {
StringBuffer sb = new StringBuffer(size * 5 + 10);
sb.append("LongList{");
for (int i = 0; i < size; i++) {
if (i != 0) {
sb.append(", ");
}
sb.append(values[i]);
}
sb.append("}");
return sb.toString();
}
/**
* Gets the number of elements in this list.
*/
public int size() {
return size;
}
/**
* Gets the indicated value.
*
* @param n {@code >= 0, < size();} which element
* @return the indicated element's value
*/
public long get(int n) {
if (n >= size) {
throw new IndexOutOfBoundsException("n >= size()");
} else if(n < 0) {
throw new IndexOutOfBoundsException("n < 0");
}
return values[n];
}
/**
* Sets the value at the given index.
*
* @param index the index at which to put the specified object.
* @param value the object to add.
* @return the previous element at the index.
*/
public long set(int index, long value) {
if (index >= size) {
throw new IndexOutOfBoundsException("n >= size()");
} else if(index < 0) {
throw new IndexOutOfBoundsException("n < 0");
}
long result = values[index];
values[index] = value;
return result;
}
/**
* Adds an element to the end of the list. This will increase the
* list's capacity if necessary.
*
* @param value the value to add
*/
public void add(long value) {
growIfNeeded();
values[size++] = value;
}
/**
* Inserts element into specified index, moving elements at and above
* that index up one. May not be used to insert at an index beyond the
* current size (that is, insertion as a last element is legal but
* no further).
*
* @param n {@code >= 0, <=size();} index of where to insert
* @param value value to insert
*/
public void insert(int n, int value) {
if (n > size) {
throw new IndexOutOfBoundsException("n > size()");
} else if(n < 0) {
throw new IndexOutOfBoundsException("n < 0");
}
growIfNeeded();
System.arraycopy (values, n, values, n+1, size - n);
values[n] = value;
size++;
}
/**
* Removes value from this list.
*
* @param value value to remove
* return {@code true} if the value was removed, {@code false} otherwise
*/
public boolean remove(long value) {
for (int i = 0; i < size; i++) {
if (values[i] == value) {
size--;
System.arraycopy(values, i+1, values, i, size-i);
return true;
}
}
return false;
}
/**
* Removes an element at a given index, shifting elements at greater
* indicies down one.
*
* @param index index of element to remove
*/
public void removeIndex(int index) {
if (index >= size) {
throw new IndexOutOfBoundsException("n >= size()");
} else if(index < 0) {
throw new IndexOutOfBoundsException("n < 0");
}
size--;
System.arraycopy (values, index + 1, values, index, size - index);
}
/**
* Increases size of array if needed
*/
private void growIfNeeded() {
if (size == values.length) {
// Resize.
long[] newArray = new long[size * 3 / 2 + 10];
System.arraycopy(values, 0, newArray, 0, size);
values = newArray;
}
}
/**
* Returns the index of the given value, or -1 if the value does not
* appear in the list.
*
* @param value value to find
* @return index of value or -1
*/
public int indexOf(long value) {
for (int i = 0; i < size; i++) {
if (values[i] == value) {
return i;
}
}
return -1;
}
/**
* Removes all values from this list.
*/
public void clear() {
values = new long[4];
size = 0;
}
/**
* Returns true if the given value is contained in the list
*
* @param value value to look for
* @return {@code true} if this list contains {@code value}, {@code false} otherwise
*/
public boolean contains(long value) {
return indexOf(value) >= 0;
}
/**
* Returns an array with a copy of this list's values
*
* @return array with a copy of this list's values
*/
public long[] toArray() {
return Arrays.copyOf(values, size);
}
}

View File

@ -1,10 +1,10 @@
package de.danoeh.antennapod.core.util;
import de.danoeh.antennapod.core.feed.FeedItem;
import java.util.Iterator;
import java.util.List;
import de.danoeh.antennapod.core.feed.FeedItem;
/**
* 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.
@ -25,23 +25,6 @@ public abstract class QueueAccess {
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) {

View File

@ -1,9 +1,6 @@
package de.danoeh.antennapod.core.util.gui;
import android.os.Bundle;
import android.os.Handler;
import android.os.Parcelable;
import android.text.TextUtils;
import android.view.View;
import android.widget.TextView;
@ -16,23 +13,36 @@ import de.danoeh.antennapod.core.R;
import static com.nineoldandroids.view.ViewPropertyAnimator.animate;
public class UndoBarController {
public class UndoBarController<T> {
private View mBarView;
private TextView mMessageView;
private ViewPropertyAnimator mBarAnimator;
private Handler mHideHandler = new Handler();
private UndoListener mUndoListener;
private UndoListener<T> mUndoListener;
// State objects
private Parcelable mUndoToken;
private T mUndoToken;
private CharSequence mUndoMessage;
public interface UndoListener {
void onUndo(Parcelable token);
public interface UndoListener<T> {
/**
* This callback function is called when the undo button is pressed
*
* @param token
*/
void onUndo(T token);
/**
*
* This callback function is called when the bar fades out without button press
*
* @param token
*/
void onHide(T token);
}
public UndoBarController(View undoBarView, UndoListener undoListener) {
public UndoBarController(View undoBarView, UndoListener<T> undoListener) {
mBarView = undoBarView;
mBarAnimator = animate(mBarView);
mUndoListener = undoListener;
@ -50,7 +60,7 @@ public class UndoBarController {
hideUndoBar(true);
}
public void showUndoBar(boolean immediate, CharSequence message, Parcelable undoToken) {
public void showUndoBar(boolean immediate, CharSequence message, T undoToken) {
mUndoToken = undoToken;
mUndoMessage = message;
mMessageView.setText(mUndoMessage);
@ -73,6 +83,14 @@ public class UndoBarController {
}
}
public boolean isShowing() {
return mBarView.getVisibility() == View.VISIBLE;
}
public void close() {
mHideHandler.post(mHideRunnable);
}
public void hideUndoBar(boolean immediate) {
mHideHandler.removeCallbacks(mHideRunnable);
if (immediate) {
@ -96,26 +114,11 @@ public class UndoBarController {
}
}
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);
mUndoListener.onHide(mUndoToken);
}
};
}

View File

@ -1,7 +1,13 @@
package de.danoeh.antennapod.core.util.playback;
import android.app.Activity;
import android.content.*;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.ServiceConnection;
import android.content.SharedPreferences;
import android.content.res.TypedArray;
import android.media.MediaPlayer;
import android.os.AsyncTask;
@ -19,13 +25,18 @@ import android.widget.TextView;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
import de.danoeh.antennapod.core.BuildConfig;
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 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;
@ -33,8 +44,6 @@ 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.
@ -118,8 +127,7 @@ public abstract class PlaybackController {
* example in the activity's onStop() method.
*/
public void release() {
if (BuildConfig.DEBUG)
Log.d(TAG, "Releasing PlaybackController");
Log.d(TAG, "Releasing PlaybackController");
try {
activity.unregisterReceiver(statusUpdate);
@ -164,8 +172,7 @@ public abstract class PlaybackController {
* as the arguments of the launch intent.
*/
private void bindToService() {
if (BuildConfig.DEBUG)
Log.d(TAG, "Trying to connect to service");
Log.d(TAG, "Trying to connect to service");
AsyncTask<Void, Void, Intent> intentLoader = new AsyncTask<Void, Void, Intent>() {
@Override
protected Intent doInBackground(Void... voids) {
@ -177,7 +184,7 @@ public abstract class PlaybackController {
boolean bound = false;
if (!PlaybackService.started) {
if (serviceIntent != null) {
if (BuildConfig.DEBUG) Log.d(TAG, "Calling start service");
Log.d(TAG, "Calling start service");
activity.startService(serviceIntent);
bound = activity.bindService(serviceIntent, mConnection, 0);
} else {
@ -186,14 +193,11 @@ public abstract class PlaybackController {
handleStatus();
}
} else {
if (BuildConfig.DEBUG)
Log.d(TAG,
"PlaybackService is running, trying to connect without start command.");
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);
Log.d(TAG, "Result for service binding: " + bound);
}
};
intentLoader.execute();
@ -204,8 +208,7 @@ public abstract class PlaybackController {
* 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");
Log.d(TAG, "Trying to restore last played media");
SharedPreferences prefs = PreferenceManager
.getDefaultSharedPreferences(activity.getApplicationContext());
long currentlyPlayingMedia = PlaybackPreferences
@ -233,8 +236,7 @@ public abstract class PlaybackController {
return serviceIntent;
}
}
if (BuildConfig.DEBUG)
Log.d(TAG, "No last played media found");
Log.d(TAG, "No last played media found");
return null;
}
@ -246,8 +248,7 @@ public abstract class PlaybackController {
|| (positionObserverFuture != null && positionObserverFuture
.isDone()) || positionObserverFuture == null) {
if (BuildConfig.DEBUG)
Log.d(TAG, "Setting up position observer");
Log.d(TAG, "Setting up position observer");
positionObserver = new MediaPositionObserver();
positionObserverFuture = schedExecutor.scheduleWithFixedDelay(
positionObserver, MediaPositionObserver.WAITING_INTERVALL,
@ -259,8 +260,7 @@ public abstract class PlaybackController {
private void cancelPositionObserver() {
if (positionObserverFuture != null) {
boolean result = positionObserverFuture.cancel(true);
if (BuildConfig.DEBUG)
Log.d(TAG, "PositionObserver cancelled. Result: " + result);
Log.d(TAG, "PositionObserver cancelled. Result: " + result);
}
}
@ -272,8 +272,7 @@ public abstract class PlaybackController {
.getService();
if (!released) {
queryService();
if (BuildConfig.DEBUG)
Log.d(TAG, "Connection to Service established");
Log.d(TAG, "Connection to Service established");
} else {
Log.i(TAG, "Connection to playback service has been established, but controller has already been released");
}
@ -282,17 +281,14 @@ public abstract class PlaybackController {
@Override
public void onServiceDisconnected(ComponentName name) {
playbackService = null;
if (BuildConfig.DEBUG)
Log.d(TAG, "Disconnected from Service");
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.");
Log.d(TAG, "Received statusUpdate Intent.");
if (isConnectedToPlaybackService()) {
PlaybackServiceMediaPlayer.PSMPInfo info = playbackService.getPSMPInfo();
status = info.playerStatus;
@ -349,8 +345,7 @@ public abstract class PlaybackController {
}
} else {
if (BuildConfig.DEBUG)
Log.d(TAG, "Bad arguments. Won't handle intent");
Log.d(TAG, "Bad arguments. Won't handle intent");
}
} else {
bindToService();
@ -421,6 +416,7 @@ public abstract class PlaybackController {
pauseResource = R.drawable.ic_av_pause_circle_outline_80dp;
}
Log.d(TAG, "status: " + status.toString());
switch (status) {
case ERROR:
@ -466,6 +462,7 @@ public abstract class PlaybackController {
updatePlayButtonAppearance(playResource, playText);
break;
case SEEKING:
onPositionObserverUpdate();
postStatusMsg(R.string.player_seeking_msg);
break;
case INITIALIZED:
@ -501,8 +498,7 @@ public abstract class PlaybackController {
* information has to be refreshed
*/
void queryService() {
if (BuildConfig.DEBUG)
Log.d(TAG, "Querying service info");
Log.d(TAG, "Querying service info");
if (playbackService != null) {
status = playbackService.getStatus();
media = playbackService.getPlayable();
@ -610,28 +606,6 @@ public abstract class PlaybackController {
};
}
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;
}

View File

@ -1,7 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="seek_delta_values">
<string-array name="smart_mark_as_played_values">
<item>0</item>
<item>15</item>
<item>30</item>
<item>45</item>
<item>60</item>
</string-array>
<integer-array name="seek_delta_values">
<item>5</item>
<item>10</item>
<item>15</item>
@ -9,7 +18,7 @@
<item>30</item>
<item>45</item>
<item>60</item>
</string-array>
</integer-array>
<string-array name="update_intervall_options">
<item>Manual</item>

View File

@ -98,9 +98,9 @@
<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="marked_as_read_label">Marked as read</string>
<string name="mark_read_label">Mark as played</string>
<string name="mark_unread_label">Mark as unplayed</string>
<string name="marked_as_read_label">Marked as played</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>
@ -220,6 +220,8 @@
<string name="pref_followQueue_sum">Jump to next queue item when playback completes</string>
<string name="pref_auto_delete_sum">Delete episode when playback completes</string>
<string name="pref_auto_delete_title">Auto Delete</string>
<string name="pref_smart_mark_as_played_sum">Mark episodes as played even if less than a certain amount of seconds of playing time is still left</string>
<string name="pref_smart_mark_as_played_title">Smart mark as played</string>
<string name="playback_pref">Playback</string>
<string name="network_pref">Network</string>
<string name="pref_autoUpdateIntervall_title">Update interval</string>
@ -266,8 +268,8 @@
<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_fast_forward">Fast forward time</string>
<string name="pref_rewind">Rewind time</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>
@ -277,6 +279,8 @@
<string name="pref_expand_notify_unsupport_toast">Android versions before 4.1 do not support expanded notifications.</string>
<string name="pref_queueAddToFront_sum">Add new episodes to the front of the queue.</string>
<string name="pref_queueAddToFront_title">Enqueue at front.</string>
<string name="pref_smart_mark_as_played_disabled">Disabled</string>
<!-- Auto-Flattr dialog -->
<string name="auto_flattr_enable">Enable automatic flattring</string>