diff options
Diffstat (limited to 'src/de/danoeh/antennapod')
24 files changed, 844 insertions, 77 deletions
diff --git a/src/de/danoeh/antennapod/PodcastApp.java b/src/de/danoeh/antennapod/PodcastApp.java index 9bca9a5e2..f4d238c60 100644 --- a/src/de/danoeh/antennapod/PodcastApp.java +++ b/src/de/danoeh/antennapod/PodcastApp.java @@ -39,9 +39,11 @@ public class PodcastApp extends Application implements private static float LOGICAL_DENSITY; private static PodcastApp singleton; - + private boolean displayOnlyEpisodes; + private static long currentlyPlayingMediaId; + public static PodcastApp getInstance() { return singleton; } @@ -53,7 +55,11 @@ public class PodcastApp extends Application implements LOGICAL_DENSITY = getResources().getDisplayMetrics().density; SharedPreferences prefs = PreferenceManager .getDefaultSharedPreferences(this); - displayOnlyEpisodes = prefs.getBoolean(PREF_DISPLAY_ONLY_EPISODES, false); + displayOnlyEpisodes = prefs.getBoolean(PREF_DISPLAY_ONLY_EPISODES, + false); + currentlyPlayingMediaId = prefs.getLong( + PlaybackService.PREF_CURRENTLY_PLAYING_MEDIA, + PlaybackService.NO_MEDIA_PLAYING); createImportDirectory(); createNoMediaFile(); prefs.registerOnSharedPreferenceChangeListener(this); @@ -71,7 +77,8 @@ public class PodcastApp extends Application implements Log.e(TAG, "Could not create .nomedia file"); e.printStackTrace(); } - if (AppConfig.DEBUG) Log.d(TAG, ".nomedia file created"); + if (AppConfig.DEBUG) + Log.d(TAG, ".nomedia file created"); } } @@ -130,11 +137,15 @@ public class PodcastApp extends Application implements Log.d(TAG, "Automatic update was deactivated"); } } else if (key.equals(PREF_DISPLAY_ONLY_EPISODES)) { - if (AppConfig.DEBUG) Log.d(TAG, "PREF_DISPLAY_ONLY_EPISODES changed"); - displayOnlyEpisodes = sharedPreferences.getBoolean(PREF_DISPLAY_ONLY_EPISODES, false); + if (AppConfig.DEBUG) + Log.d(TAG, "PREF_DISPLAY_ONLY_EPISODES changed"); + displayOnlyEpisodes = sharedPreferences.getBoolean( + PREF_DISPLAY_ONLY_EPISODES, false); } else if (key.equals(PlaybackService.PREF_LAST_PLAYED_ID)) { - if (AppConfig.DEBUG) Log.d(TAG, "PREF_LAST_PLAYED_ID changed"); - long mediaId = sharedPreferences.getLong(PlaybackService.PREF_AUTODELETE_MEDIA_ID, -1); + if (AppConfig.DEBUG) + Log.d(TAG, "PREF_LAST_PLAYED_ID changed"); + long mediaId = sharedPreferences.getLong( + PlaybackService.PREF_AUTODELETE_MEDIA_ID, -1); if (mediaId != -1) { FeedManager manager = FeedManager.getInstance(); FeedMedia media = manager.getFeedMedia(mediaId); @@ -142,19 +153,33 @@ public class PodcastApp extends Application implements manager.autoDeleteIfPossible(this, media); } } + } else if (key.equals(PlaybackService.PREF_CURRENTLY_PLAYING_MEDIA)) { + long id = sharedPreferences.getLong( + PlaybackService.PREF_CURRENTLY_PLAYING_MEDIA, + PlaybackService.NO_MEDIA_PLAYING); + if (AppConfig.DEBUG) + Log.d(TAG, "Currently playing media set to " + id); + if (id != currentlyPlayingMediaId) { + currentlyPlayingMediaId = id; + } } } public static float getLogicalDensity() { return LOGICAL_DENSITY; } - + public boolean displayOnlyEpisodes() { return displayOnlyEpisodes; } + public static long getCurrentlyPlayingMediaId() { + return currentlyPlayingMediaId; + } + public boolean isLargeScreen() { - return (getResources().getConfiguration().screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK) == Configuration.SCREENLAYOUT_SIZE_LARGE || (getResources().getConfiguration().screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK) == Configuration.SCREENLAYOUT_SIZE_XLARGE; + return (getResources().getConfiguration().screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK) == Configuration.SCREENLAYOUT_SIZE_LARGE + || (getResources().getConfiguration().screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK) == Configuration.SCREENLAYOUT_SIZE_XLARGE; } } diff --git a/src/de/danoeh/antennapod/activity/AudioplayerActivity.java b/src/de/danoeh/antennapod/activity/AudioplayerActivity.java index 8d0b4a0f5..d6299e602 100644 --- a/src/de/danoeh/antennapod/activity/AudioplayerActivity.java +++ b/src/de/danoeh/antennapod/activity/AudioplayerActivity.java @@ -140,7 +140,7 @@ public class AudioplayerActivity extends MediaplayerActivity { }; sCChapterFragment.setListAdapter(new ChapterListAdapter( - activity, 0, media.getItem().getChapters())); + activity, 0, media.getItem().getChapters(), media)); return sCChapterFragment; default: diff --git a/src/de/danoeh/antennapod/adapter/ChapterListAdapter.java b/src/de/danoeh/antennapod/adapter/ChapterListAdapter.java index 16a868437..9357d0659 100644 --- a/src/de/danoeh/antennapod/adapter/ChapterListAdapter.java +++ b/src/de/danoeh/antennapod/adapter/ChapterListAdapter.java @@ -21,15 +21,21 @@ import android.widget.ArrayAdapter; import android.widget.TextView; import de.danoeh.antennapod.R; import de.danoeh.antennapod.feed.Chapter; +import de.danoeh.antennapod.feed.FeedMedia; import de.danoeh.antennapod.util.Converter; public class ChapterListAdapter extends ArrayAdapter<Chapter> { private static final String TAG = "ChapterListAdapter"; + private List<Chapter> chapters; + private FeedMedia media; + public ChapterListAdapter(Context context, int textViewResourceId, - List<Chapter> objects) { + List<Chapter> objects, FeedMedia media) { super(context, textViewResourceId, objects); + this.chapters = objects; + this.media = media; } @Override @@ -181,4 +187,36 @@ public class ChapterListAdapter extends ArrayAdapter<Chapter> { } }; + + @Override + public int getCount() { + // ignore invalid chapters + int counter = 0; + for (Chapter chapter : chapters) { + if (!ignoreChapter(chapter)) { + counter++; + } + } + return counter; + } + + private boolean ignoreChapter(Chapter c) { + return media.getDuration() > 0 && media.getDuration() < c.getStart(); + } + + @Override + public Chapter getItem(int position) { + int i = 0; + for (Chapter chapter : chapters) { + if (!ignoreChapter(chapter)) { + if (i == position) { + return chapter; + } else { + i++; + } + } + } + return super.getItem(position); + } + } diff --git a/src/de/danoeh/antennapod/feed/Chapter.java b/src/de/danoeh/antennapod/feed/Chapter.java index f50053f93..10575e03d 100644 --- a/src/de/danoeh/antennapod/feed/Chapter.java +++ b/src/de/danoeh/antennapod/feed/Chapter.java @@ -8,6 +8,9 @@ public abstract class Chapter extends FeedComponent { protected FeedItem item; protected String link; + public Chapter() { + } + public Chapter(long start) { super(); this.start = start; diff --git a/src/de/danoeh/antennapod/feed/Feed.java b/src/de/danoeh/antennapod/feed/Feed.java index c3677447b..55256ea31 100644 --- a/src/de/danoeh/antennapod/feed/Feed.java +++ b/src/de/danoeh/antennapod/feed/Feed.java @@ -17,6 +17,7 @@ import de.danoeh.antennapod.PodcastApp; public class Feed extends FeedFile { public static final int FEEDFILETYPE_FEED = 0; public static final String TYPE_RSS2 = "rss"; + public static final String TYPE_RSS091 = "rss"; public static final String TYPE_ATOM1 = "atom"; private String title; diff --git a/src/de/danoeh/antennapod/feed/FeedItem.java b/src/de/danoeh/antennapod/feed/FeedItem.java index ebfff80ad..6227298df 100644 --- a/src/de/danoeh/antennapod/feed/FeedItem.java +++ b/src/de/danoeh/antennapod/feed/FeedItem.java @@ -4,6 +4,8 @@ import java.lang.ref.SoftReference; import java.util.Date; import java.util.List; +import de.danoeh.antennapod.PodcastApp; + /** * Data Object for a XML message * @@ -184,6 +186,15 @@ public class FeedItem extends FeedComponent { this.itemIdentifier = itemIdentifier; } + public boolean isPlaying() { + if (media != null) { + if (PodcastApp.getCurrentlyPlayingMediaId() == media.getId()) { + return true; + } + } + return false; + } + public void setCachedDescription(String d) { cachedDescription = new SoftReference<String>(d); } diff --git a/src/de/danoeh/antennapod/feed/FeedManager.java b/src/de/danoeh/antennapod/feed/FeedManager.java index 285fc3539..7f4a1c5aa 100644 --- a/src/de/danoeh/antennapod/feed/FeedManager.java +++ b/src/de/danoeh/antennapod/feed/FeedManager.java @@ -39,7 +39,7 @@ import de.danoeh.antennapod.util.comparator.PlaybackCompletionDateComparator; public class FeedManager { private static final String TAG = "FeedManager"; - public static final String ACITON_FEED_LIST_UPDATE = "de.danoeh.antennapod.action.feed.feedlistUpdate"; + public static final String ACTION_FEED_LIST_UPDATE = "de.danoeh.antennapod.action.feed.feedlistUpdate"; public static final String ACTION_UNREAD_ITEMS_UPDATE = "de.danoeh.antennapod.action.feed.unreadItemsUpdate"; public static final String ACTION_QUEUE_UPDATE = "de.danoeh.antennapod.action.feed.queueUpdate"; public static final String ACTION_DOWNLOADLOG_UPDATE = "de.danoeh.antennapod.action.feed.downloadLogUpdate"; @@ -264,7 +264,7 @@ public class FeedManager { } private void sendFeedUpdateBroadcast(Context context) { - context.sendBroadcast(new Intent(ACITON_FEED_LIST_UPDATE)); + context.sendBroadcast(new Intent(ACTION_FEED_LIST_UPDATE)); } private void sendPlaybackHistoryUpdateBroadcast(Context context) { @@ -1198,6 +1198,10 @@ public class FeedManager { chapter = new ID3Chapter(start, title, item, link); break; + case VorbisCommentChapter.CHAPTERTYPE_VORBISCOMMENT_CHAPTER: + chapter = new VorbisCommentChapter(start, + title, item, link); + break; } chapter.setId(chapterCursor .getLong(PodDBAdapter.KEY_ID_INDEX)); diff --git a/src/de/danoeh/antennapod/feed/SimpleChapter.java b/src/de/danoeh/antennapod/feed/SimpleChapter.java index 2462497ef..7b74f28f6 100644 --- a/src/de/danoeh/antennapod/feed/SimpleChapter.java +++ b/src/de/danoeh/antennapod/feed/SimpleChapter.java @@ -7,26 +7,6 @@ public class SimpleChapter extends Chapter { super(start, title, item, link); } - public String getTitle() { - return title; - } - - public FeedItem getItem() { - return item; - } - - public long getStart() { - return start; - } - - public void setItem(FeedItem item) { - this.item = item; - } - - public String getLink() { - return link; - } - @Override public int getChapterType() { return CHAPTERTYPE_SIMPLECHAPTER; diff --git a/src/de/danoeh/antennapod/feed/VorbisCommentChapter.java b/src/de/danoeh/antennapod/feed/VorbisCommentChapter.java new file mode 100644 index 000000000..544e762d3 --- /dev/null +++ b/src/de/danoeh/antennapod/feed/VorbisCommentChapter.java @@ -0,0 +1,109 @@ +package de.danoeh.antennapod.feed; + +import java.util.concurrent.TimeUnit; + +import de.danoeh.antennapod.util.vorbiscommentreader.VorbisCommentReaderException; + +public class VorbisCommentChapter extends Chapter { + public static final int CHAPTERTYPE_VORBISCOMMENT_CHAPTER = 3; + + private static final int CHAPTERXXX_LENGTH = "chapterxxx".length(); + + private int vorbisCommentId; + + public VorbisCommentChapter(int vorbisCommentId) { + this.vorbisCommentId = vorbisCommentId; + } + + public VorbisCommentChapter(long start, String title, FeedItem item, + String link) { + super(start, title, item, link); + } + + @Override + public String toString() { + return "VorbisCommentChapter [id=" + id + ", title=" + title + + ", link=" + link + ", start=" + start + "]"; + } + + public static long getStartTimeFromValue(String value) + throws VorbisCommentReaderException { + String[] parts = value.split(":"); + if (parts.length >= 3) { + try { + long hours = TimeUnit.MILLISECONDS.convert( + Long.parseLong(parts[0]), TimeUnit.HOURS); + long minutes = TimeUnit.MILLISECONDS.convert( + Long.parseLong(parts[1]), TimeUnit.MINUTES); + if (parts[2].contains("-->")) { + parts[2] = parts[2].substring(0, parts[2].indexOf("-->")); + } + long seconds = TimeUnit.MILLISECONDS.convert( + ((long) Float.parseFloat(parts[2])), TimeUnit.SECONDS); + return hours + minutes + seconds; + } catch (NumberFormatException e) { + throw new VorbisCommentReaderException(e); + } + } else { + throw new VorbisCommentReaderException("Invalid time string"); + } + } + + /** + * Return the id of a vorbiscomment chapter from a string like CHAPTERxxx* + * + * @return the id of the chapter key or -1 if the id couldn't be read. + * @throws VorbisCommentReaderException + * */ + public static int getIDFromKey(String key) + throws VorbisCommentReaderException { + if (key.length() >= CHAPTERXXX_LENGTH) { // >= CHAPTERxxx + try { + String strId = key.substring(8, 10); + return Integer.parseInt(strId); + } catch (NumberFormatException e) { + throw new VorbisCommentReaderException(e); + } + } + throw new VorbisCommentReaderException("key is too short (" + key + ")"); + } + + /** + * Get the string that comes after 'CHAPTERxxx', for example 'name' or + * 'url'. + */ + public static String getAttributeTypeFromKey(String key) { + if (key.length() > CHAPTERXXX_LENGTH) { + return key.substring(CHAPTERXXX_LENGTH, key.length()); + } + return null; + } + + @Override + public int getChapterType() { + return CHAPTERTYPE_VORBISCOMMENT_CHAPTER; + } + + public void setTitle(String title) { + this.title = title; + } + + public void setLink(String link) { + this.link = link; + } + + public void setStart(long start) { + this.start = start; + } + + public int getVorbisCommentId() { + return vorbisCommentId; + } + + public void setVorbisCommentId(int vorbisCommentId) { + this.vorbisCommentId = vorbisCommentId; + } + + + +} diff --git a/src/de/danoeh/antennapod/fragment/FeedlistFragment.java b/src/de/danoeh/antennapod/fragment/FeedlistFragment.java index 1a205f521..caf6c6a7f 100644 --- a/src/de/danoeh/antennapod/fragment/FeedlistFragment.java +++ b/src/de/danoeh/antennapod/fragment/FeedlistFragment.java @@ -116,7 +116,7 @@ public class FeedlistFragment extends SherlockFragment implements IntentFilter filter = new IntentFilter(); filter.addAction(DownloadRequester.ACTION_DOWNLOAD_QUEUED); filter.addAction(FeedManager.ACTION_UNREAD_ITEMS_UPDATE); - filter.addAction(FeedManager.ACITON_FEED_LIST_UPDATE); + filter.addAction(FeedManager.ACTION_FEED_LIST_UPDATE); filter.addAction(DownloadService.ACTION_DOWNLOAD_HANDLED); pActivity.registerReceiver(contentUpdate, filter); fla.notifyDataSetChanged(); diff --git a/src/de/danoeh/antennapod/service/PlaybackService.java b/src/de/danoeh/antennapod/service/PlaybackService.java index 98721877e..0bf7ab075 100644 --- a/src/de/danoeh/antennapod/service/PlaybackService.java +++ b/src/de/danoeh/antennapod/service/PlaybackService.java @@ -58,6 +58,12 @@ public class PlaybackService extends Service { public static final String PREF_LAST_PLAYED_ID = "de.danoeh.antennapod.preferences.lastPlayedId"; /** Contains the feed id of the last played item. */ public static final String PREF_LAST_PLAYED_FEED_ID = "de.danoeh.antennapod.preferences.lastPlayedFeedId"; + /** + * ID of the media object that is currently being played. This preference is + * set to NO_MEDIA_PLAYING after playback has been completed and is set as + * soon as the 'play' button is pressed. + */ + public static final String PREF_CURRENTLY_PLAYING_MEDIA = "de.danoeh.antennapod.preferences.currentlyPlayingMedia"; /** True if last played media was streamed. */ public static final String PREF_LAST_IS_STREAM = "de.danoeh.antennapod.preferences.lastIsStream"; /** True if last played media was a video. */ @@ -159,6 +165,9 @@ public class PlaybackService extends Service { /** True if mediaplayer was paused because it lost audio focus temporarily */ private boolean pausedBecauseOfTransientAudiofocusLoss; + /** Value of PREF_CURRENTLY_PLAYING_MEDIA if no media is playing. */ + public static final long NO_MEDIA_PLAYING = -1; + private final IBinder mBinder = new LocalBinder(); public class LocalBinder extends Binder { @@ -602,6 +611,11 @@ public class PlaybackService extends Service { ChapterUtils .readID3ChaptersFromFeedMediaDownloadUrl(media .getItem()); + if (media.getItem().getChapters() == null) { + ChapterUtils + .readOggChaptersFromMediaDownloadUrl(media + .getItem()); + } if (media.getItem().getChapters() != null && !interrupted()) { sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD, @@ -657,6 +671,7 @@ public class PlaybackService extends Service { pause(true, true); } sendNotificationBroadcast(NOTIFICATION_TYPE_ERROR, what); + setCurrentlyPlayingMedia(NO_MEDIA_PLAYING); stopSelf(); return true; } @@ -692,6 +707,7 @@ public class PlaybackService extends Service { autoDeleteMediaId = -1; } SharedPreferences.Editor editor = prefs.edit(); + editor.putLong(PREF_CURRENTLY_PLAYING_MEDIA, NO_MEDIA_PLAYING); editor.putLong(PREF_AUTODELETE_MEDIA_ID, autoDeleteMediaId); editor.putBoolean(PREF_AUTO_DELETE_MEDIA_PLAYBACK_COMPLETED, true); editor.commit(); @@ -794,6 +810,7 @@ public class PlaybackService extends Service { if (AppConfig.DEBUG) Log.d(TAG, "Stopping playback"); player.stop(); + setCurrentlyPlayingMedia(NO_MEDIA_PLAYING); stopSelf(); } @@ -834,6 +851,7 @@ public class PlaybackService extends Service { SharedPreferences.Editor editor = PreferenceManager .getDefaultSharedPreferences(getApplicationContext()) .edit(); + editor.putLong(PREF_CURRENTLY_PLAYING_MEDIA, media.getId()); editor.putLong(PREF_LAST_PLAYED_FEED_ID, feed.getId()); editor.putBoolean(PREF_LAST_IS_STREAM, shouldStream); editor.putBoolean(PREF_LAST_IS_VIDEO, playingVideo); @@ -1262,4 +1280,10 @@ public class PlaybackService extends Service { } } + private void setCurrentlyPlayingMedia(long id) { + SharedPreferences.Editor editor = PreferenceManager + .getDefaultSharedPreferences(getApplicationContext()).edit(); + editor.putLong(PREF_CURRENTLY_PLAYING_MEDIA, id); + editor.commit(); + } } diff --git a/src/de/danoeh/antennapod/service/download/DownloadService.java b/src/de/danoeh/antennapod/service/download/DownloadService.java index e2709d141..6da651838 100644 --- a/src/de/danoeh/antennapod/service/download/DownloadService.java +++ b/src/de/danoeh/antennapod/service/download/DownloadService.java @@ -632,7 +632,8 @@ public class DownloadService extends Service { return false; } if (item.getPubDate() == null) { - Log.e(TAG, "Item has no pubDate. Using current time as pubDate"); + Log.e(TAG, + "Item has no pubDate. Using current time as pubDate"); if (item.getTitle() != null) { Log.e(TAG, "Title of invalid item: " + item.getTitle()); } @@ -720,6 +721,10 @@ public class DownloadService extends Service { if (media.getItem().getChapters() == null) { ChapterUtils.readID3ChaptersFromFeedMediaFileUrl(media .getItem()); + if (media.getItem().getChapters() == null) { + ChapterUtils.readOggChaptersFromMediaFileUrl(media + .getItem()); + } if (media.getItem().getChapters() != null) { chaptersRead = true; } diff --git a/src/de/danoeh/antennapod/storage/DownloadRequester.java b/src/de/danoeh/antennapod/storage/DownloadRequester.java index 928b923fd..0201cc542 100644 --- a/src/de/danoeh/antennapod/storage/DownloadRequester.java +++ b/src/de/danoeh/antennapod/storage/DownloadRequester.java @@ -4,6 +4,8 @@ import java.io.File; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import org.apache.commons.io.FilenameUtils; + import android.content.Context; import android.content.Intent; import android.util.Log; @@ -14,7 +16,7 @@ import de.danoeh.antennapod.feed.FeedFile; import de.danoeh.antennapod.feed.FeedImage; import de.danoeh.antennapod.feed.FeedMedia; import de.danoeh.antennapod.service.download.DownloadService; -import de.danoeh.antennapod.util.NumberGenerator; +import de.danoeh.antennapod.util.FileNameGenerator; import de.danoeh.antennapod.util.URLChecker; public class DownloadRequester { @@ -44,12 +46,40 @@ public class DownloadRequester { return downloader; } - private void download(Context context, FeedFile item, File dest) { + private void download(Context context, FeedFile item, File dest, + boolean overwriteIfExists) { if (!isDownloadingFile(item)) { if (dest.exists()) { if (AppConfig.DEBUG) - Log.d(TAG, "File already exists. Deleting !"); - dest.delete(); + Log.d(TAG, "File already exists."); + if (overwriteIfExists) { + boolean result = dest.delete(); + if (AppConfig.DEBUG) + Log.d(TAG, "Deleting file. Result: " + result); + } else { + // find different name + File newDest = null; + for (int i = 1; i < Integer.MAX_VALUE; i++) { + String newName = FilenameUtils.getBaseName(dest + .getName()) + + "-" + + i + + "." + + FilenameUtils.getExtension(dest.getName()); + if (AppConfig.DEBUG) + Log.d(TAG, "Testing filename " + newName); + newDest = new File(dest.getParent(), newName); + if (!newDest.exists()) { + if (AppConfig.DEBUG) + Log.d(TAG, "File doesn't exist yet. Using " + + newName); + break; + } + } + if (newDest != null) { + dest = newDest; + } + } } if (AppConfig.DEBUG) Log.d(TAG, @@ -82,7 +112,7 @@ public class DownloadRequester { throws DownloadRequestException { if (feedFileValid(feed)) { download(context, feed, new File(getFeedfilePath(context), - getFeedfileName(feed))); + getFeedfileName(feed)), true); } } @@ -90,15 +120,16 @@ public class DownloadRequester { throws DownloadRequestException { if (feedFileValid(image)) { download(context, image, new File(getImagefilePath(context), - getImagefileName(image))); + getImagefileName(image)), true); } } - public void downloadMedia(Context context, FeedMedia feedmedia) throws DownloadRequestException { + public void downloadMedia(Context context, FeedMedia feedmedia) + throws DownloadRequestException { if (feedFileValid(feedmedia)) { download(context, feedmedia, new File(getMediafilePath(context, feedmedia), - getMediafilename(feedmedia))); + getMediafilename(feedmedia)), false); } } @@ -199,7 +230,11 @@ public class DownloadRequester { } public String getFeedfileName(Feed feed) { - return "feed-" + NumberGenerator.generateLong(feed.getDownload_url()); + String filename = feed.getDownload_url(); + if (feed.getTitle() != null && !feed.getTitle().isEmpty()) { + filename = feed.getTitle(); + } + return "feed-" + FileNameGenerator.generateFileName(filename); } public String getImagefilePath(Context context) @@ -209,7 +244,11 @@ public class DownloadRequester { } public String getImagefileName(FeedImage image) { - return "image-" + NumberGenerator.generateLong(image.getDownload_url()); + String filename = image.getDownload_url(); + if (image.getFeed() != null && image.getFeed().getTitle() != null) { + filename = image.getFeed().getTitle(); + } + return "image-" + FileNameGenerator.generateFileName(filename); } public String getMediafilePath(Context context, FeedMedia media) @@ -217,7 +256,7 @@ public class DownloadRequester { File externalStorage = getExternalFilesDirOrThrowException( context, MEDIA_DOWNLOADPATH - + NumberGenerator.generateLong(media.getItem() + + FileNameGenerator.generateFileName(media.getItem() .getFeed().getTitle()) + "/"); return externalStorage.toString(); } diff --git a/src/de/danoeh/antennapod/syndication/handler/SyndHandler.java b/src/de/danoeh/antennapod/syndication/handler/SyndHandler.java index ff7942cdb..55757ce18 100644 --- a/src/de/danoeh/antennapod/syndication/handler/SyndHandler.java +++ b/src/de/danoeh/antennapod/syndication/handler/SyndHandler.java @@ -23,7 +23,7 @@ public class SyndHandler extends DefaultHandler { public SyndHandler(Feed feed, TypeGetter.Type type) { state = new HandlerState(feed); - if (type == TypeGetter.Type.RSS20) { + if (type == TypeGetter.Type.RSS20 || type == TypeGetter.Type.RSS091) { state.defaultNamespaces.push(new NSRSS20()); } } diff --git a/src/de/danoeh/antennapod/syndication/handler/TypeGetter.java b/src/de/danoeh/antennapod/syndication/handler/TypeGetter.java index a2766526d..d2454f2b9 100644 --- a/src/de/danoeh/antennapod/syndication/handler/TypeGetter.java +++ b/src/de/danoeh/antennapod/syndication/handler/TypeGetter.java @@ -19,7 +19,7 @@ public class TypeGetter { private static final String TAG = "TypeGetter"; enum Type { - RSS20, ATOM, INVALID + RSS20, RSS091, ATOM, INVALID } private static final String ATOM_ROOT = "feed"; @@ -43,13 +43,25 @@ public class TypeGetter { if (AppConfig.DEBUG) Log.d(TAG, "Recognized type Atom"); return Type.ATOM; - } else if (tag.equals(RSS_ROOT) - && (xpp.getAttributeValue(null, "version") - .equals("2.0"))) { - feed.setType(Feed.TYPE_RSS2); - if (AppConfig.DEBUG) - Log.d(TAG, "Recognized type RSS 2.0"); - return Type.RSS20; + } else if (tag.equals(RSS_ROOT)) { + String strVersion = xpp.getAttributeValue(null, + "version"); + if (strVersion != null) { + + if (strVersion.equals("2.0")) { + feed.setType(Feed.TYPE_RSS2); + if (AppConfig.DEBUG) + Log.d(TAG, "Recognized type RSS 2.0"); + return Type.RSS20; + } else if (strVersion.equals("0.91") + || strVersion.equals("0.92")) { + if (AppConfig.DEBUG) + Log.d(TAG, + "Recognized type RSS 0.91/0.92"); + return Type.RSS091; + } + } + throw new UnsupportedFeedtypeException(Type.INVALID); } else { if (AppConfig.DEBUG) Log.d(TAG, "Type is invalid"); diff --git a/src/de/danoeh/antennapod/syndication/namespace/rss20/NSRSS20.java b/src/de/danoeh/antennapod/syndication/namespace/rss20/NSRSS20.java index 388e1540e..696fef6e1 100644 --- a/src/de/danoeh/antennapod/syndication/namespace/rss20/NSRSS20.java +++ b/src/de/danoeh/antennapod/syndication/namespace/rss20/NSRSS20.java @@ -101,7 +101,7 @@ public class NSRSS20 extends Namespace { } else if (second.equals(CHANNEL)) { state.getFeed().setTitle(content); } else if (second.equals(IMAGE) && third != null && third.equals(CHANNEL)) { - state.getFeed().getImage().setTitle(IMAGE); + state.getFeed().getImage().setTitle(content); } } else if (top.equals(LINK)) { if (second.equals(CHANNEL)) { diff --git a/src/de/danoeh/antennapod/util/ChapterUtils.java b/src/de/danoeh/antennapod/util/ChapterUtils.java index bf88d8fd1..3131b9e9a 100644 --- a/src/de/danoeh/antennapod/util/ChapterUtils.java +++ b/src/de/danoeh/antennapod/util/ChapterUtils.java @@ -3,6 +3,7 @@ package de.danoeh.antennapod.util; import java.io.BufferedInputStream; import java.io.File; import java.io.FileInputStream; +import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.net.MalformedURLException; @@ -10,6 +11,8 @@ import java.net.URL; import java.util.Collections; import java.util.List; +import org.apache.commons.io.IOUtils; + import android.util.Log; import de.danoeh.antennapod.AppConfig; import de.danoeh.antennapod.feed.Chapter; @@ -18,6 +21,8 @@ import de.danoeh.antennapod.feed.FeedMedia; import de.danoeh.antennapod.util.comparator.ChapterStartTimeComparator; import de.danoeh.antennapod.util.id3reader.ChapterReader; import de.danoeh.antennapod.util.id3reader.ID3ReaderException; +import de.danoeh.antennapod.util.vorbiscommentreader.VorbisCommentChapterReader; +import de.danoeh.antennapod.util.vorbiscommentreader.VorbisCommentReaderException; /** Utility class for getting chapter data from media files. */ public class ChapterUtils { @@ -131,6 +136,72 @@ public class ChapterUtils { } } + public static void readOggChaptersFromMediaDownloadUrl(FeedItem item) { + final FeedMedia media = item.getMedia(); + if (media != null && media.getDownload_url() != null) { + InputStream input = null; + try { + URL url = new URL(media.getDownload_url()); + input = url.openStream(); + if (input != null) { + readOggChaptersFromInputStream(item, input); + } + } catch (MalformedURLException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } finally { + IOUtils.closeQuietly(input); + } + } + } + + public static void readOggChaptersFromMediaFileUrl(FeedItem item) { + final FeedMedia media = item.getMedia(); + if (media != null && media.getFile_url() != null) { + File source = new File(media.getFile_url()); + if (source.exists()) { + InputStream input = null; + try { + input = new BufferedInputStream(new FileInputStream(source)); + readOggChaptersFromInputStream(item, input); + } catch (FileNotFoundException e) { + e.printStackTrace(); + } finally { + IOUtils.closeQuietly(input); + } + } + } + } + + private static void readOggChaptersFromInputStream(FeedItem item, + InputStream input) { + if (AppConfig.DEBUG) + Log.d(TAG, + "Trying to read chapters from item with title " + + item.getTitle()); + try { + VorbisCommentChapterReader reader = new VorbisCommentChapterReader(); + reader.readInputStream(input); + List<Chapter> chapters = reader.getChapters(); + if (chapters != null) { + Collections.sort(chapters, new ChapterStartTimeComparator()); + processChapters(chapters, item); + if (chaptersValid(chapters)) { + item.setChapters(chapters); + Log.i(TAG, "Chapters loaded"); + } else { + Log.e(TAG, "Chapter data was invalid"); + } + } else { + Log.i(TAG, + "ChapterReader could not find any Ogg vorbis chapters"); + } + } catch (VorbisCommentReaderException e) { + e.printStackTrace(); + } + } + /** Makes sure that chapter does a title and an item attribute. */ private static void processChapters(List<Chapter> chapters, FeedItem item) { for (int i = 0; i < chapters.size(); i++) { diff --git a/src/de/danoeh/antennapod/util/FileNameGenerator.java b/src/de/danoeh/antennapod/util/FileNameGenerator.java new file mode 100644 index 000000000..3bc193080 --- /dev/null +++ b/src/de/danoeh/antennapod/util/FileNameGenerator.java @@ -0,0 +1,36 @@ +package de.danoeh.antennapod.util; + +import java.util.Arrays; + +/** Generates valid filenames for a given string. */ +public class FileNameGenerator { + + private static final char[] ILLEGAL_CHARACTERS = { '/', '\\', '?', '%', + '*', ':', '|', '"', '<', '>' }; + static { + Arrays.sort(ILLEGAL_CHARACTERS); + } + + private FileNameGenerator() { + + } + + /** + * This method will return a new string that doesn't contain any illegal + * characters of the given string. + */ + public static String generateFileName(String string) { + StringBuilder builder = new StringBuilder(); + for (int i = 0; i < string.length(); i++) { + char c = string.charAt(i); + if (Arrays.binarySearch(ILLEGAL_CHARACTERS, c) < 0) { + builder.append(c); + } + } + return builder.toString(); + } + + public static long generateLong(final String str) { + return str.hashCode(); + } +} diff --git a/src/de/danoeh/antennapod/util/NumberGenerator.java b/src/de/danoeh/antennapod/util/NumberGenerator.java deleted file mode 100644 index ff89180e1..000000000 --- a/src/de/danoeh/antennapod/util/NumberGenerator.java +++ /dev/null @@ -1,21 +0,0 @@ -package de.danoeh.antennapod.util; - - -/**Utility class for creating numbers.*/ -public final class NumberGenerator { - /** Class shall not be instantiated.*/ - private NumberGenerator() { - } - - /**Logging tag.*/ - private static final String TAG = "NumberGenerator"; - - /** Takes a string and generates a random value out of - * the hash-value of that string. - * @param strSeed The string to take for the return value - * @return The generated random value - * */ - public static long generateLong(final String strSeed) { - return strSeed.hashCode(); - } -} diff --git a/src/de/danoeh/antennapod/util/vorbiscommentreader/OggInputStream.java b/src/de/danoeh/antennapod/util/vorbiscommentreader/OggInputStream.java new file mode 100644 index 000000000..e3de5971f --- /dev/null +++ b/src/de/danoeh/antennapod/util/vorbiscommentreader/OggInputStream.java @@ -0,0 +1,81 @@ +package de.danoeh.antennapod.util.vorbiscommentreader; +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; +import java.util.Scanner; + +import org.apache.commons.io.IOUtils; + +public class OggInputStream extends InputStream { + private InputStream input; + + /** True if OggInputStream is currently inside an Ogg page. */ + private boolean isInPage; + private long bytesLeft; + + public OggInputStream(InputStream input) { + super(); + isInPage = false; + this.input = input; + } + + @Override + public int read() throws IOException { + if (!isInPage) { + readOggPage(); + } + + if (isInPage && bytesLeft > 0) { + int result = input.read(); + bytesLeft -= 1; + if (bytesLeft == 0) { + isInPage = false; + } + return result; + } + return -1; + } + + private void readOggPage() throws IOException { + // find OggS + int[] buffer = new int[4]; + int c = 0; + boolean isInOggS = false; + while ((c = input.read()) != -1) { + switch (c) { + case 'O': + isInOggS = true; + buffer[0] = c; + break; + case 'g': + if (buffer[1] != c) { + buffer[1] = c; + } else { + buffer[2] = c; + } + break; + case 'S': + buffer[3] = c; + break; + default: + if (isInOggS) { + Arrays.fill(buffer, 0); + isInOggS = false; + } + } + if (buffer[0] == 'O' && buffer[1] == 'g' && buffer[2] == 'g' + && buffer[3] == 'S') { + break; + } + } + // read segments + IOUtils.skipFully(input, 22); + bytesLeft = 0; + int numSegments = input.read(); + for (int i = 0; i < numSegments; i++) { + bytesLeft += input.read(); + } + isInPage = true; + } + +} diff --git a/src/de/danoeh/antennapod/util/vorbiscommentreader/VorbisCommentChapterReader.java b/src/de/danoeh/antennapod/util/vorbiscommentreader/VorbisCommentChapterReader.java new file mode 100644 index 000000000..190d7f2b5 --- /dev/null +++ b/src/de/danoeh/antennapod/util/vorbiscommentreader/VorbisCommentChapterReader.java @@ -0,0 +1,102 @@ +package de.danoeh.antennapod.util.vorbiscommentreader; + +import java.util.ArrayList; +import java.util.List; + +import android.util.Log; + +import de.danoeh.antennapod.AppConfig; +import de.danoeh.antennapod.feed.Chapter; +import de.danoeh.antennapod.feed.VorbisCommentChapter; + +public class VorbisCommentChapterReader extends VorbisCommentReader { + private static final String TAG = "VorbisCommentChapterReader"; + + private static final String CHAPTER_KEY = "chapter\\d\\d\\d.*"; + private static final String CHAPTER_ATTRIBUTE_TITLE = "name"; + private static final String CHAPTER_ATTRIBUTE_LINK = "url"; + + private List<Chapter> chapters; + + public VorbisCommentChapterReader() { + } + + @Override + public void onVorbisCommentFound() { + System.out.println("Vorbis comment found"); + } + + @Override + public void onVorbisCommentHeaderFound(VorbisCommentHeader header) { + chapters = new ArrayList<Chapter>(); + System.out.println(header.toString()); + } + + @Override + public boolean onContentVectorKey(String content) { + return content.matches(CHAPTER_KEY); + } + + @Override + public void onContentVectorValue(String key, String value) + throws VorbisCommentReaderException { + if (AppConfig.DEBUG) + Log.d(TAG, "Key: " + key + ", value: " + value); + String attribute = VorbisCommentChapter.getAttributeTypeFromKey(key); + int id = VorbisCommentChapter.getIDFromKey(key); + Chapter chapter = getChapterById(id); + if (attribute == null) { + if (getChapterById(id) == null) { + // new chapter + long start = VorbisCommentChapter.getStartTimeFromValue(value); + chapter = new VorbisCommentChapter(id); + chapter.setStart(start); + chapters.add(chapter); + } else { + throw new VorbisCommentReaderException( + "Found chapter with duplicate ID (" + key + ", " + + value + ")"); + } + } else if (attribute.equals(CHAPTER_ATTRIBUTE_TITLE)) { + if (chapter != null) { + chapter.setTitle(value); + } + } else if (attribute.equals(CHAPTER_ATTRIBUTE_LINK)) { + if (chapter != null) { + chapter.setLink(value); + } + } + } + + @Override + public void onNoVorbisCommentFound() { + System.out.println("No vorbis comment found"); + } + + @Override + public void onEndOfComment() { + System.out.println("End of comment"); + for (Chapter c : chapters) { + System.out.println(c.toString()); + } + } + + @Override + public void onError(VorbisCommentReaderException exception) { + exception.printStackTrace(); + } + + private Chapter getChapterById(long id) { + for (Chapter c : chapters) { + if (((VorbisCommentChapter) c).getVorbisCommentId() == id) { + return c; + } + } + return null; + } + + public List<Chapter> getChapters() { + return chapters; + } + +} diff --git a/src/de/danoeh/antennapod/util/vorbiscommentreader/VorbisCommentHeader.java b/src/de/danoeh/antennapod/util/vorbiscommentreader/VorbisCommentHeader.java new file mode 100644 index 000000000..8c47393c9 --- /dev/null +++ b/src/de/danoeh/antennapod/util/vorbiscommentreader/VorbisCommentHeader.java @@ -0,0 +1,26 @@ +package de.danoeh.antennapod.util.vorbiscommentreader; +public class VorbisCommentHeader { + private String vendorString; + private long userCommentLength; + + public VorbisCommentHeader(String vendorString, long userCommentLength) { + super(); + this.vendorString = vendorString; + this.userCommentLength = userCommentLength; + } + + @Override + public String toString() { + return "VorbisCommentHeader [vendorString=" + vendorString + + ", userCommentLength=" + userCommentLength + "]"; + } + + public String getVendorString() { + return vendorString; + } + + public long getUserCommentLength() { + return userCommentLength; + } + +} diff --git a/src/de/danoeh/antennapod/util/vorbiscommentreader/VorbisCommentReader.java b/src/de/danoeh/antennapod/util/vorbiscommentreader/VorbisCommentReader.java new file mode 100644 index 000000000..ceaa8d5cd --- /dev/null +++ b/src/de/danoeh/antennapod/util/vorbiscommentreader/VorbisCommentReader.java @@ -0,0 +1,197 @@ +package de.danoeh.antennapod.util.vorbiscommentreader; + +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.Charset; +import java.nio.charset.CharsetDecoder; +import java.nio.charset.CoderResult; +import java.util.Arrays; + +import org.apache.commons.io.EndianUtils; +import org.apache.commons.io.IOUtils; + + +public abstract class VorbisCommentReader { + /** Length of first page in an ogg file in bytes. */ + private static final int FIRST_PAGE_LENGTH = 58; + private static final int SECOND_PAGE_MAX_LENGTH = 64 * 1024 * 1024; + private static final int PACKET_TYPE_IDENTIFICATION = 1; + private static final int PACKET_TYPE_COMMENT = 3; + + /** Called when Reader finds identification header. */ + public abstract void onVorbisCommentFound(); + + public abstract void onVorbisCommentHeaderFound(VorbisCommentHeader header); + + /** + * Is called every time the Reader finds a content vector. The handler + * should return true if it wants to handle the content vector. + */ + public abstract boolean onContentVectorKey(String content); + + /** + * Is called if onContentVectorKey returned true for the key. + * + * @throws VorbisCommentReaderException + */ + public abstract void onContentVectorValue(String key, String value) + throws VorbisCommentReaderException; + + public abstract void onNoVorbisCommentFound(); + + public abstract void onEndOfComment(); + + public abstract void onError(VorbisCommentReaderException exception); + + public void readInputStream(InputStream input) + throws VorbisCommentReaderException { + try { + // look for identification header + if (findIdentificationHeader(input)) { + + onVorbisCommentFound(); + input = new OggInputStream(input); + if (findCommentHeader(input)) { + VorbisCommentHeader commentHeader = readCommentHeader(input); + if (commentHeader != null) { + onVorbisCommentHeaderFound(commentHeader); + for (int i = 0; i < commentHeader + .getUserCommentLength(); i++) { + try { + long vectorLength = EndianUtils + .readSwappedUnsignedInteger(input); + String key = readContentVectorKey(input, + vectorLength).toLowerCase(); + boolean readValue = onContentVectorKey(key); + if (readValue) { + String value = readUTF8String( + input, + (int) (vectorLength - key.length() - 1)); + onContentVectorValue(key, value); + } else { + IOUtils.skipFully(input, + vectorLength - key.length() - 1); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + onEndOfComment(); + } + + } else { + onError(new VorbisCommentReaderException( + "No comment header found")); + } + } else { + onNoVorbisCommentFound(); + } + } catch (IOException e) { + onError(new VorbisCommentReaderException(e)); + } + } + + private String readUTF8String(InputStream input, long length) + throws IOException { + byte[] buffer = new byte[(int) length]; + + IOUtils.readFully(input, buffer); + Charset charset = Charset.forName("UTF-8"); + return charset.newDecoder().decode(ByteBuffer.wrap(buffer)).toString(); + } + + /** + * Looks for an identification header in the first page of the file. If an + * identification header is found, it will be skipped completely and the + * method will return true, otherwise false. + * + * @throws IOException + */ + private boolean findIdentificationHeader(InputStream input) + throws IOException { + byte[] buffer = new byte[FIRST_PAGE_LENGTH]; + IOUtils.readFully(input, buffer); + int i; + for (i = 6; i < buffer.length; i++) { + if (buffer[i - 5] == 'v' && buffer[i - 4] == 'o' + && buffer[i - 3] == 'r' && buffer[i - 2] == 'b' + && buffer[i - 1] == 'i' && buffer[i] == 's' + && buffer[i - 6] == PACKET_TYPE_IDENTIFICATION) { + return true; + } + } + return false; + } + + private boolean findCommentHeader(InputStream input) throws IOException { + char[] buffer = new char["vorbis".length() + 1]; + for (int bytesRead = 0; bytesRead < SECOND_PAGE_MAX_LENGTH; bytesRead++) { + char c = (char) input.read(); + int dest = -1; + switch (c) { + case PACKET_TYPE_COMMENT: + dest = 0; + break; + case 'v': + dest = 1; + break; + case 'o': + dest = 2; + break; + case 'r': + dest = 3; + break; + case 'b': + dest = 4; + break; + case 'i': + dest = 5; + break; + case 's': + dest = 6; + break; + } + if (dest >= 0) { + buffer[dest] = c; + if (buffer[1] == 'v' && buffer[2] == 'o' && buffer[3] == 'r' + && buffer[4] == 'b' && buffer[5] == 'i' + && buffer[6] == 's' && buffer[0] == PACKET_TYPE_COMMENT) { + return true; + } + } else { + Arrays.fill(buffer, (char) 0); + } + } + return false; + } + + private VorbisCommentHeader readCommentHeader(InputStream input) + throws IOException, VorbisCommentReaderException { + try { + long vendorLength = EndianUtils.readSwappedUnsignedInteger(input); + String vendorName = readUTF8String(input, vendorLength); + long userCommentLength = EndianUtils + .readSwappedUnsignedInteger(input); + return new VorbisCommentHeader(vendorName, userCommentLength); + } catch (UnsupportedEncodingException e) { + throw new VorbisCommentReaderException(e); + } + } + + private String readContentVectorKey(InputStream input, long vectorLength) + throws IOException { + StringBuffer buffer = new StringBuffer(); + for (int i = 0; i < vectorLength; i++) { + char c = (char) input.read(); + if (c == '=') { + return buffer.toString(); + } else { + buffer.append(c); + } + } + return null; // no key found + } +} diff --git a/src/de/danoeh/antennapod/util/vorbiscommentreader/VorbisCommentReaderException.java b/src/de/danoeh/antennapod/util/vorbiscommentreader/VorbisCommentReaderException.java new file mode 100644 index 000000000..574373241 --- /dev/null +++ b/src/de/danoeh/antennapod/util/vorbiscommentreader/VorbisCommentReaderException.java @@ -0,0 +1,24 @@ +package de.danoeh.antennapod.util.vorbiscommentreader; +public class VorbisCommentReaderException extends Exception { + + public VorbisCommentReaderException() { + super(); + // TODO Auto-generated constructor stub + } + + public VorbisCommentReaderException(String arg0, Throwable arg1) { + super(arg0, arg1); + // TODO Auto-generated constructor stub + } + + public VorbisCommentReaderException(String arg0) { + super(arg0); + // TODO Auto-generated constructor stub + } + + public VorbisCommentReaderException(Throwable arg0) { + super(arg0); + // TODO Auto-generated constructor stub + } + +} |