diff options
author | Tom Hennen <TomHennen@users.noreply.github.com> | 2015-04-12 18:11:00 -0400 |
---|---|---|
committer | Tom Hennen <TomHennen@users.noreply.github.com> | 2015-04-12 18:11:00 -0400 |
commit | 6a1a9afa6b31e9bc2b422d0e75866626af1bc90e (patch) | |
tree | 602b96fb1a9318ac6ffbf52d0647e24f92ca555d | |
parent | 7eee089bb8653916c45880ed31bcefbc389df362 (diff) | |
parent | 09bd600f5cbd78c02bf60b8ed399b211014820ee (diff) | |
download | AntennaPod-1.1.zip |
Merge pull request #735 from AntennaPod/version_1.11.1
Version 1.1
148 files changed, 7279 insertions, 594 deletions
diff --git a/.gitignore b/.gitignore index 12a398578..5a3070e8f 100644 --- a/.gitignore +++ b/.gitignore @@ -42,3 +42,4 @@ libs *.DS_Store src/de/danoeh/antennapod/util/flattr/FlattrConfig.java gradle.properties +*.keystore diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 2529169ca..000000000 --- a/.gitmodules +++ /dev/null @@ -1,6 +0,0 @@ -[submodule "submodules/dslv"] - path = submodules/dslv - url = git://github.com/danieloeh/drag-sort-listview.git -[submodule "app/dslv"] - path = app/dslv - url = https://github.com/danieloeh/drag-sort-listview.git diff --git a/CHANGELOG.md b/CHANGELOG.md index e5cba02b9..7f9cd8041 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ Change Log ========== +Version 1.1 +----------- +* iTunes podcast integration +* Swipe to remove items from the queue +* Set the number of parallel downloads +* Fix for gpodder.net on old devices +* Fixed date problems for some feeds +* Display improvements +* Usability improvements +* Several other bugfixes Version 1.0 ----------- @@ -278,4 +288,4 @@ Version 0.8.1 ------------ * Added support for SimpleChapters -* OPML import
\ No newline at end of file +* OPML import diff --git a/CONTRIBUTORS b/CONTRIBUTORS index 2c8c15bf5..9e6042bf4 100644 --- a/CONTRIBUTORS +++ b/CONTRIBUTORS @@ -10,6 +10,11 @@ hzulla andrewgaul peschmae0 TomHennen +mfietz +volhol +eerden +twiceyuan +rharriso Translations: diff --git a/app/build.gradle b/app/build.gradle index 8f829d0af..b59106248 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -11,7 +11,6 @@ dependencies { exclude group: 'org.json', module: 'json' } compile 'commons-io:commons-io:2.4' - compile project('dslv:library') compile 'com.jayway.android.robotium:robotium-solo:5.2.1' compile 'org.jsoup:jsoup:1.7.3' compile 'com.squareup.picasso:picasso:2.4.0' @@ -19,6 +18,7 @@ dependencies { compile 'com.squareup.okhttp:okhttp-urlconnection:2.2.0' compile 'com.squareup.okio:okio:1.2.0' compile project(':core') + compile project(':library:drag-sort-listview') } android { diff --git a/app/dslv b/app/dslv deleted file mode 160000 -Subproject 80011c50e444e1c7d5e13b57bdb127b524a1ff9 diff --git a/app/proguard.cfg b/app/proguard.cfg index 1838f007c..d0aa1b6a9 100644 --- a/app/proguard.cfg +++ b/app/proguard.cfg @@ -69,3 +69,13 @@ -keep class org.apache.commons.** { *; } -dontskipnonpubliclibraryclassmembers + +# disable logging +-assumenosideeffects class android.util.Log { + public static boolean isLoggable(java.lang.String, int); + public static *** v(...); + public static *** i(...); + public static *** w(...); + public static *** d(...); + public static *** e(...); +}
\ No newline at end of file diff --git a/app/settings.gradle b/app/settings.gradle deleted file mode 100644 index e2d4f844d..000000000 --- a/app/settings.gradle +++ /dev/null @@ -1 +0,0 @@ -include ':app:dslv:library' diff --git a/app/src/androidTest/java/de/test/antennapod/service/download/HttpDownloaderTest.java b/app/src/androidTest/java/de/test/antennapod/service/download/HttpDownloaderTest.java index 65b1145a2..443fbed7e 100644 --- a/app/src/androidTest/java/de/test/antennapod/service/download/HttpDownloaderTest.java +++ b/app/src/androidTest/java/de/test/antennapod/service/download/HttpDownloaderTest.java @@ -2,7 +2,12 @@ package de.test.antennapod.service.download; import android.test.InstrumentationTestCase; import android.util.Log; + +import java.io.File; +import java.io.IOException; + import de.danoeh.antennapod.core.feed.FeedFile; +import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.service.download.DownloadRequest; import de.danoeh.antennapod.core.service.download.DownloadStatus; import de.danoeh.antennapod.core.service.download.Downloader; @@ -10,9 +15,6 @@ import de.danoeh.antennapod.core.service.download.HttpDownloader; import de.danoeh.antennapod.core.util.DownloadError; import de.test.antennapod.util.service.download.HTTPBin; -import java.io.File; -import java.io.IOException; - public class HttpDownloaderTest extends InstrumentationTestCase { private static final String TAG = "HttpDownloaderTest"; private static final String DOWNLOAD_DIR = "testdownloads"; @@ -41,6 +43,7 @@ public class HttpDownloaderTest extends InstrumentationTestCase { @Override protected void setUp() throws Exception { super.setUp(); + UserPreferences.createInstance(getInstrumentation().getTargetContext()); destDir = getInstrumentation().getTargetContext().getExternalFilesDir(DOWNLOAD_DIR); assertNotNull(destDir); assertTrue(destDir.exists()); @@ -90,7 +93,7 @@ public class HttpDownloaderTest extends InstrumentationTestCase { } public void testGzip() { - download("http://httpbin.org/gzip", "testGzip", true); + download(HTTPBin.BASE_URL + "/gzip/100", "testGzip", true); } public void test404() { diff --git a/app/src/androidTest/java/de/test/antennapod/ui/MainActivityTest.java b/app/src/androidTest/java/de/test/antennapod/ui/MainActivityTest.java index ce7b790e0..b092264cd 100644 --- a/app/src/androidTest/java/de/test/antennapod/ui/MainActivityTest.java +++ b/app/src/androidTest/java/de/test/antennapod/ui/MainActivityTest.java @@ -3,12 +3,13 @@ package de.test.antennapod.ui; import android.content.Context; import android.content.SharedPreferences; import android.test.ActivityInstrumentationTestCase2; -import android.view.View; +import android.widget.ListView; + import com.robotium.solo.Solo; + import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.DefaultOnlineFeedViewActivity; import de.danoeh.antennapod.activity.MainActivity; -import de.danoeh.antennapod.activity.PreferenceActivityGingerbread; import de.danoeh.antennapod.core.feed.Feed; import de.danoeh.antennapod.core.storage.PodDBAdapter; import de.danoeh.antennapod.preferences.PreferenceController; @@ -49,10 +50,14 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2<MainActiv super.tearDown(); } + private void openNavDrawer() { + solo.clickOnScreen(50, 50); + } + public void testAddFeed() throws Exception { uiTestUtils.addHostedFeedData(); final Feed feed = uiTestUtils.hostedFeeds.get(0); - solo.setNavigationDrawer(Solo.OPENED); + openNavDrawer(); solo.clickOnText(solo.getString(R.string.add_feed_label)); solo.enterText(0, feed.getDownload_url()); solo.clickOnButton(0); @@ -65,39 +70,43 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2<MainActiv public void testClickNavDrawer() throws Exception { uiTestUtils.addLocalFeedData(false); - final View home = solo.getView(UITestUtils.HOME_VIEW); // all episodes + openNavDrawer(); + solo.clickOnText(solo.getString(R.string.new_episodes_label)); solo.waitForView(android.R.id.list); - assertEquals(solo.getString(R.string.all_episodes_label), getActionbarTitle()); + assertEquals(solo.getString(R.string.new_episodes_label), getActionbarTitle()); + // queue - solo.clickOnView(home); + openNavDrawer(); solo.clickOnText(solo.getString(R.string.queue_label)); solo.waitForView(android.R.id.list); assertEquals(solo.getString(R.string.queue_label), getActionbarTitle()); // downloads - solo.clickOnView(home); + openNavDrawer(); solo.clickOnText(solo.getString(R.string.downloads_label)); solo.waitForView(android.R.id.list); assertEquals(solo.getString(R.string.downloads_label), getActionbarTitle()); // playback history - solo.clickOnView(home); + openNavDrawer(); solo.clickOnText(solo.getString(R.string.playback_history_label)); solo.waitForView(android.R.id.list); assertEquals(solo.getString(R.string.playback_history_label), getActionbarTitle()); // add podcast - solo.clickOnView(home); + openNavDrawer(); solo.clickOnText(solo.getString(R.string.add_feed_label)); solo.waitForView(R.id.txtvFeedurl); assertEquals(solo.getString(R.string.add_feed_label), getActionbarTitle()); // podcasts + ListView list = (ListView)solo.getView(R.id.nav_list); for (int i = 0; i < uiTestUtils.hostedFeeds.size(); i++) { Feed f = uiTestUtils.hostedFeeds.get(i); - solo.clickOnView(home); + solo.clickOnScreen(50, 50); // open nav drawer + solo.scrollListToLine(list, i); solo.clickOnText(f.getTitle()); solo.waitForView(android.R.id.list); assertEquals("", getActionbarTitle()); @@ -109,7 +118,7 @@ public class MainActivityTest extends ActivityInstrumentationTestCase2<MainActiv } public void testGoToPreferences() { - solo.setNavigationDrawer(Solo.CLOSED); + openNavDrawer(); solo.clickOnMenuItem(solo.getString(R.string.settings_label)); solo.waitForActivity(PreferenceController.getPreferenceActivity()); } diff --git a/app/src/androidTest/java/de/test/antennapod/ui/PlaybackTest.java b/app/src/androidTest/java/de/test/antennapod/ui/PlaybackTest.java index 2235ee6f4..346ef6c18 100644 --- a/app/src/androidTest/java/de/test/antennapod/ui/PlaybackTest.java +++ b/app/src/androidTest/java/de/test/antennapod/ui/PlaybackTest.java @@ -5,7 +5,11 @@ import android.content.SharedPreferences; import android.preference.PreferenceManager; import android.test.ActivityInstrumentationTestCase2; import android.widget.TextView; + import com.robotium.solo.Solo; + +import java.util.List; + import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.AudioplayerActivity; import de.danoeh.antennapod.activity.MainActivity; @@ -16,8 +20,6 @@ import de.danoeh.antennapod.core.storage.DBReader; import de.danoeh.antennapod.core.storage.DBWriter; import de.danoeh.antennapod.core.storage.PodDBAdapter; -import java.util.List; - /** * Test cases for starting and ending playback from the MainActivity and AudioPlayerActivity */ @@ -40,6 +42,7 @@ public class PlaybackTest extends ActivityInstrumentationTestCase2<MainActivity> PodDBAdapter adapter = new PodDBAdapter(getInstrumentation().getTargetContext()); adapter.open(); adapter.close(); + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getInstrumentation().getTargetContext()); prefs.edit().putBoolean(UserPreferences.PREF_UNPAUSE_ON_HEADSET_RECONNECT, false).commit(); prefs.edit().putBoolean(UserPreferences.PREF_PAUSE_ON_HEADSET_DISCONNECT, false).commit(); @@ -59,6 +62,10 @@ public class PlaybackTest extends ActivityInstrumentationTestCase2<MainActivity> super.tearDown(); } + private void openNavDrawer() { + solo.clickOnScreen(50, 50); + } + private void setContinuousPlaybackPreference(boolean value) { SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getInstrumentation().getTargetContext()); prefs.edit().putBoolean(UserPreferences.PREF_FOLLOW_QUEUE, value).commit(); @@ -71,7 +78,9 @@ public class PlaybackTest extends ActivityInstrumentationTestCase2<MainActivity> private void startLocalPlayback() { assertTrue(solo.waitForActivity(MainActivity.class)); - solo.setNavigationDrawer(Solo.CLOSED); + openNavDrawer(); + solo.clickOnText(solo.getString(R.string.new_episodes_label)); + solo.waitForView(android.R.id.list); solo.clickOnView(solo.getView(R.id.butSecondaryAction)); assertTrue(solo.waitForActivity(AudioplayerActivity.class)); assertTrue(solo.waitForView(solo.getView(R.id.butPlay))); @@ -79,10 +88,10 @@ public class PlaybackTest extends ActivityInstrumentationTestCase2<MainActivity> private void startLocalPlaybackFromQueue() { assertTrue(solo.waitForActivity(MainActivity.class)); - solo.clickOnView(solo.getView(UITestUtils.HOME_VIEW)); + openNavDrawer(); solo.clickOnText(solo.getString(R.string.queue_label)); assertTrue(solo.waitForView(solo.getView(R.id.butSecondaryAction))); - solo.clickOnImageButton(0); + solo.clickOnImageButton(1); assertTrue(solo.waitForActivity(AudioplayerActivity.class)); assertTrue(solo.waitForView(solo.getView(R.id.butPlay))); } @@ -108,7 +117,7 @@ public class PlaybackTest extends ActivityInstrumentationTestCase2<MainActivity> setContinuousPlaybackPreference(false); uiTestUtils.addLocalFeedData(true); List<FeedItem> queue = DBReader.getQueue(getInstrumentation().getTargetContext()); - FeedItem second = queue.get(1); + FeedItem second = queue.get(0); startLocalPlaybackFromQueue(); assertTrue(solo.waitForText(second.getTitle())); @@ -147,4 +156,6 @@ public class PlaybackTest extends ActivityInstrumentationTestCase2<MainActivity> public void testReplayEpisodeContinuousPlaybackOff() throws Exception { replayEpisodeCheck(false); } + + } diff --git a/app/src/androidTest/java/de/test/antennapod/ui/UITestUtils.java b/app/src/androidTest/java/de/test/antennapod/ui/UITestUtils.java index 55fffb80a..249cb0dd6 100644 --- a/app/src/androidTest/java/de/test/antennapod/ui/UITestUtils.java +++ b/app/src/androidTest/java/de/test/antennapod/ui/UITestUtils.java @@ -5,12 +5,8 @@ import android.content.Context; import android.graphics.Bitmap; import android.os.Build; -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.core.feed.*; -import de.danoeh.antennapod.core.storage.PodDBAdapter; -import de.test.antennapod.util.service.download.HTTPBin; -import de.test.antennapod.util.syndication.feedgenerator.RSS2Generator; import junit.framework.Assert; + import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; @@ -23,6 +19,16 @@ import java.util.ArrayList; import java.util.Date; import java.util.List; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.core.feed.EventDistributor; +import de.danoeh.antennapod.core.feed.Feed; +import de.danoeh.antennapod.core.feed.FeedImage; +import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.core.feed.FeedMedia; +import de.danoeh.antennapod.core.storage.PodDBAdapter; +import de.test.antennapod.util.service.download.HTTPBin; +import de.test.antennapod.util.syndication.feedgenerator.RSS2Generator; + /** * Utility methods for UI tests. * Starts a web server that hosts feeds, episodes and images. @@ -174,7 +180,8 @@ public class UITestUtils { feed.setDownloaded(true); if (feed.getImage() != null) { FeedImage image = feed.getImage(); - image.setFile_url(image.getDownload_url()); + int fileId = Integer.parseInt(StringUtils.substringAfter(image.getDownload_url(), "files/")); + image.setFile_url(server.accessFile(fileId).getAbsolutePath()); image.setDownloaded(true); } if (downloadEpisodes) { diff --git a/app/src/androidTest/java/de/test/antennapod/util/service/download/HTTPBin.java b/app/src/androidTest/java/de/test/antennapod/util/service/download/HTTPBin.java index 5cb723446..2f2c3fe5b 100644 --- a/app/src/androidTest/java/de/test/antennapod/util/service/download/HTTPBin.java +++ b/app/src/androidTest/java/de/test/antennapod/util/service/download/HTTPBin.java @@ -2,15 +2,28 @@ package de.test.antennapod.util.service.download; import android.util.Base64; import android.util.Log; -import de.danoeh.antennapod.BuildConfig; + import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; -import java.io.*; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; import java.net.URLConnection; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Random; import java.util.zip.GZIPOutputStream; +import de.danoeh.antennapod.BuildConfig; + /** * Http server for testing purposes * <p/> @@ -264,7 +277,7 @@ public class HTTPBin extends NanoHTTPD { private Response getGzippedResponse(int size) throws IOException { try { - Thread.sleep(5000); + Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } @@ -272,14 +285,15 @@ public class HTTPBin extends NanoHTTPD { Random random = new Random(System.currentTimeMillis()); random.nextBytes(buffer); - ByteArrayOutputStream compressed = new ByteArrayOutputStream(); + ByteArrayOutputStream compressed = new ByteArrayOutputStream(buffer.length); GZIPOutputStream gzipOutputStream = new GZIPOutputStream(compressed); gzipOutputStream.write(buffer); + gzipOutputStream.close(); InputStream inputStream = new ByteArrayInputStream(compressed.toByteArray()); Response response = new Response(Response.Status.OK, MIME_PLAIN, inputStream); - response.addHeader("Content-encoding", "gzip"); - response.addHeader("Content-length", String.valueOf(compressed.size())); + response.addHeader("Content-Encoding", "gzip"); + response.addHeader("Content-Length", String.valueOf(compressed.size())); return response; } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 5b00941de..a04b1df07 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,8 +1,8 @@ <?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="de.danoeh.antennapod" - android:versionCode="44" - android:versionName="1.0"> + android:versionCode="49" + android:versionName="1.1"> <uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> diff --git a/app/src/main/assets/about.html b/app/src/main/assets/about.html index 3398dbb67..d8face548 100644 --- a/app/src/main/assets/about.html +++ b/app/src/main/assets/about.html @@ -41,7 +41,7 @@ <div id="header" align="center"> <img src="logo.png" alt="Logo" width="100px" height="100px"/> - <p>AntennaPod, Version 1.0</p> + <p>AntennaPod, Version 1.1</p> <p>Copyright © 2014 Daniel Oeh</p> diff --git a/app/src/main/java/de/danoeh/antennapod/activity/DefaultOnlineFeedViewActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/DefaultOnlineFeedViewActivity.java index 8401b41a7..287ae3568 100644 --- a/app/src/main/java/de/danoeh/antennapod/activity/DefaultOnlineFeedViewActivity.java +++ b/app/src/main/java/de/danoeh/antennapod/activity/DefaultOnlineFeedViewActivity.java @@ -138,7 +138,7 @@ public class DefaultOnlineFeedViewActivity extends OnlineFeedViewActivity { @Override public void onClick(View v) { try { - Feed f = new Feed(selectedDownloadUrl, new Date(), feed.getTitle()); + Feed f = new Feed(selectedDownloadUrl, new Date(0), feed.getTitle()); f.setPreferences(feed.getPreferences()); DefaultOnlineFeedViewActivity.this.feed = f; diff --git a/app/src/main/java/de/danoeh/antennapod/activity/MainActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/MainActivity.java index b3e95f0c0..2efee838d 100644 --- a/app/src/main/java/de/danoeh/antennapod/activity/MainActivity.java +++ b/app/src/main/java/de/danoeh/antennapod/activity/MainActivity.java @@ -66,8 +66,8 @@ public class MainActivity extends ActionBarActivity implements NavDrawerActivity public static final String SAVE_TITLE = "title"; - public static final int POS_NEW = 0, - POS_QUEUE = 1, + public static final int POS_QUEUE = 0, + POS_NEW = 1, POS_DOWNLOADS = 2, POS_HISTORY = 3, POS_ADD = 4; diff --git a/app/src/main/java/de/danoeh/antennapod/activity/OnlineFeedViewActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/OnlineFeedViewActivity.java index 9f028000e..3b03ed2db 100644 --- a/app/src/main/java/de/danoeh/antennapod/activity/OnlineFeedViewActivity.java +++ b/app/src/main/java/de/danoeh/antennapod/activity/OnlineFeedViewActivity.java @@ -13,9 +13,21 @@ import android.widget.ArrayAdapter; import android.widget.LinearLayout; import android.widget.ProgressBar; import android.widget.RelativeLayout; + +import org.apache.commons.lang3.StringUtils; +import org.xml.sax.SAXException; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Map; + +import javax.xml.parsers.ParserConfigurationException; + import de.danoeh.antennapod.BuildConfig; import de.danoeh.antennapod.R; -import de.danoeh.antennapod.dialog.AuthenticationDialog; import de.danoeh.antennapod.core.feed.Feed; import de.danoeh.antennapod.core.feed.FeedPreferences; import de.danoeh.antennapod.core.preferences.UserPreferences; @@ -31,16 +43,7 @@ import de.danoeh.antennapod.core.util.FileNameGenerator; import de.danoeh.antennapod.core.util.StorageUtils; import de.danoeh.antennapod.core.util.URLChecker; import de.danoeh.antennapod.core.util.syndication.FeedDiscoverer; -import org.apache.commons.lang3.StringUtils; -import org.xml.sax.SAXException; - -import javax.xml.parsers.ParserConfigurationException; -import java.io.File; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Date; -import java.util.List; -import java.util.Map; +import de.danoeh.antennapod.dialog.AuthenticationDialog; /** * Downloads a feed from a feed URL and parses it. Subclasses can display the @@ -181,7 +184,7 @@ public abstract class OnlineFeedViewActivity extends ActionBarActivity { if (BuildConfig.DEBUG) Log.d(TAG, "Starting feed download"); url = URLChecker.prepareURL(url); - feed = new Feed(url, new Date()); + feed = new Feed(url, new Date(0)); if (username != null && password != null) { feed.setPreferences(new FeedPreferences(0, false, username, password)); } diff --git a/app/src/main/java/de/danoeh/antennapod/activity/OpmlImportFromPathActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/OpmlImportFromPathActivity.java index 162a8f2e5..c1bbb7e52 100644 --- a/app/src/main/java/de/danoeh/antennapod/activity/OpmlImportFromPathActivity.java +++ b/app/src/main/java/de/danoeh/antennapod/activity/OpmlImportFromPathActivity.java @@ -1,7 +1,9 @@ package de.danoeh.antennapod.activity; -import android.app.AlertDialog; -import android.content.DialogInterface; +import android.content.ActivityNotFoundException; +import android.content.Intent; +import android.content.pm.ResolveInfo; +import android.net.Uri; import android.os.Bundle; import android.util.Log; import android.view.Menu; @@ -10,23 +12,31 @@ import android.view.View; import android.view.View.OnClickListener; import android.widget.Button; import android.widget.TextView; -import android.widget.Toast; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.InputStreamReader; +import java.io.Reader; +import java.util.List; + import de.danoeh.antennapod.BuildConfig; import de.danoeh.antennapod.R; import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.util.LangUtils; import de.danoeh.antennapod.core.util.StorageUtils; -import java.io.*; - /** * Lets the user start the OPML-import process from a path */ public class OpmlImportFromPathActivity extends OpmlImportBaseActivity { + private static final String TAG = "OpmlImportFromPathActivity"; - private TextView txtvPath; - private Button butStart; - private String importPath; + + private static final int CHOOSE_OPML_FILE = 1; + + private Intent intentPickAction; + private Intent intentGetContentAction; @Override protected void onCreate(Bundle savedInstanceState) { @@ -36,47 +46,74 @@ public class OpmlImportFromPathActivity extends OpmlImportBaseActivity { getSupportActionBar().setDisplayHomeAsUpEnabled(true); setContentView(R.layout.opml_import); - txtvPath = (TextView) findViewById(R.id.txtvPath); - butStart = (Button) findViewById(R.id.butStartImport); + final TextView txtvHeaderExplanation1 = (TextView) findViewById(R.id.txtvHeadingExplanation1); + final TextView txtvExplanation1 = (TextView) findViewById(R.id.txtvExplanation1); + final TextView txtvHeaderExplanation2 = (TextView) findViewById(R.id.txtvHeadingExplanation2); + final TextView txtvExplanation2 = (TextView) findViewById(R.id.txtvExplanation2); + final TextView txtvHeaderExplanation3 = (TextView) findViewById(R.id.txtvHeadingExplanation3); - butStart.setOnClickListener(new OnClickListener() { + Button butChooseFilesystem = (Button) findViewById(R.id.butChooseFileFromFilesystem); + butChooseFilesystem.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { - checkFolderForFiles(); + chooseFileFromFilesystem(); } }); + + Button butChooseExternal = (Button) findViewById(R.id.butChooseFileFromExternal); + butChooseExternal.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + chooseFileFromExternal(); + } + }); + + int nextOption = 1; + intentPickAction = new Intent(Intent.ACTION_PICK); + intentPickAction.setData(Uri.parse("file://")); + List<ResolveInfo> intentActivities = getPackageManager() + .queryIntentActivities(intentPickAction, CHOOSE_OPML_FILE); + if(intentActivities.size() == 0) { + intentPickAction.setData(null); + intentActivities = getPackageManager() + .queryIntentActivities(intentPickAction, CHOOSE_OPML_FILE); + if(intentActivities.size() == 0) { + txtvHeaderExplanation1.setVisibility(View.GONE); + txtvExplanation1.setVisibility(View.GONE); + findViewById(R.id.divider1).setVisibility(View.GONE); + butChooseFilesystem.setVisibility(View.GONE); + } + } + if(txtvExplanation1.getVisibility() == View.VISIBLE) { + txtvHeaderExplanation1.setText("Option " + nextOption); + nextOption++; + } + + intentGetContentAction = new Intent(Intent.ACTION_GET_CONTENT); + intentGetContentAction.addCategory(Intent.CATEGORY_OPENABLE); + intentGetContentAction.setType("*/*"); + intentActivities = getPackageManager() + .queryIntentActivities(intentGetContentAction, CHOOSE_OPML_FILE); + if(intentActivities.size() == 0) { + txtvHeaderExplanation2.setVisibility(View.GONE); + txtvExplanation2.setVisibility(View.GONE); + findViewById(R.id.divider2).setVisibility(View.GONE); + butChooseExternal.setVisibility(View.GONE); + } else { + txtvHeaderExplanation2.setText("Option " + nextOption); + nextOption++; + } + + txtvHeaderExplanation3.setText("Option " + nextOption); } @Override protected void onResume() { super.onResume(); StorageUtils.checkStorageAvailability(this); - setImportPath(); } - /** - * Sets the importPath variable and makes txtvPath display the import - * directory. - */ - private void setImportPath() { - File importDir = UserPreferences.getDataFolder(this, UserPreferences.IMPORT_DIR); - boolean success = true; - if (!importDir.exists()) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Import directory doesn't exist. Creating..."); - success = importDir.mkdir(); - if (!success) { - Log.e(TAG, "Could not create directory"); - } - } - if (success) { - txtvPath.setText(importDir.toString()); - importPath = importDir.toString(); - } else { - txtvPath.setText(R.string.opml_directory_error); - } - } @Override public boolean onCreateOptionsMenu(Menu menu) { @@ -95,32 +132,6 @@ public class OpmlImportFromPathActivity extends OpmlImportBaseActivity { } } - /** - * Looks at the contents of the import directory and decides what to do. If - * more than one file is in the directory, a dialog will be created to let - * the user choose which item to import - */ - private void checkFolderForFiles() { - File dir = new File(importPath); - if (dir.isDirectory()) { - File[] fileList = dir.listFiles(); - if (fileList.length == 1) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Found one file, choosing that one."); - startImport(fileList[0]); - } else if (fileList.length > 1) { - Log.w(TAG, "Import directory contains more than one file."); - askForFile(dir); - } else { - Log.e(TAG, "Import directory is empty"); - Toast toast = Toast - .makeText(this, R.string.opml_import_error_dir_empty, - Toast.LENGTH_LONG); - toast.show(); - } - } - } - private void startImport(File file) { Reader mReader = null; try { @@ -134,38 +145,36 @@ public class OpmlImportFromPathActivity extends OpmlImportBaseActivity { } } - /** - * Asks the user to choose from a list of files in a directory and returns - * his choice. + /* + * Creates an implicit intent to launch a file manager which lets + * the user choose a specific OPML-file to import from. */ - private void askForFile(File dir) { - final File[] fileList = dir.listFiles(); - String[] fileNames = dir.list(); - - AlertDialog.Builder dialog = new AlertDialog.Builder(this); - dialog.setTitle(R.string.choose_file_to_import_label); - dialog.setNeutralButton(android.R.string.cancel, - new DialogInterface.OnClickListener() { - - @Override - public void onClick(DialogInterface dialog, int which) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Dialog was cancelled"); - dialog.dismiss(); - } - }); - dialog.setItems(fileNames, new DialogInterface.OnClickListener() { + private void chooseFileFromFilesystem() { + try { + startActivityForResult(intentPickAction, CHOOSE_OPML_FILE); + } catch (ActivityNotFoundException e) { + Log.e(TAG, "No activity found. Should never happen..."); + } + } - @Override - public void onClick(DialogInterface dialog, int which) { - if (BuildConfig.DEBUG) - Log.d(TAG, "File at index " + which + " was chosen"); - dialog.dismiss(); - startImport(fileList[which]); - } - }); - dialog.create().show(); + private void chooseFileFromExternal() { + try { + startActivityForResult(intentGetContentAction, CHOOSE_OPML_FILE); + } catch (ActivityNotFoundException e) { + Log.e(TAG, "No activity found. Should never happen..."); + } } + /** + * Gets the path of the file chosen with chooseFileToImport() + */ + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if (resultCode == RESULT_OK && requestCode == CHOOSE_OPML_FILE) { + String filename = data.getData().getPath(); + startImport(new File(filename)); + } + } } diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/ActionButtonUtils.java b/app/src/main/java/de/danoeh/antennapod/adapter/ActionButtonUtils.java index c35bb9694..8d3e73429 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/ActionButtonUtils.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/ActionButtonUtils.java @@ -57,7 +57,7 @@ public class ActionButtonUtils { } else { // item is not being downloaded butSecondary.setVisibility(View.VISIBLE); - if (media.isPlaying()) { + if (media.isCurrentlyPlaying()) { butSecondary.setImageDrawable(drawables.getDrawable(3)); } else { butSecondary diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/DefaultActionButtonCallback.java b/app/src/main/java/de/danoeh/antennapod/adapter/DefaultActionButtonCallback.java index 800462023..14644dd68 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/DefaultActionButtonCallback.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/DefaultActionButtonCallback.java @@ -1,6 +1,7 @@ package de.danoeh.antennapod.adapter; import android.content.Context; +import android.content.Intent; import android.widget.Toast; import org.apache.commons.lang3.Validate; @@ -9,6 +10,7 @@ import de.danoeh.antennapod.R; 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.storage.DBTasks; import de.danoeh.antennapod.core.storage.DBWriter; import de.danoeh.antennapod.core.storage.DownloadRequestException; @@ -46,7 +48,15 @@ public class DefaultActionButtonCallback implements ActionButtonCallback { DownloadRequester.getInstance().cancelDownload(context, media); Toast.makeText(context, R.string.download_cancelled_msg, Toast.LENGTH_SHORT).show(); } else { // media is downloaded - DBTasks.playMedia(context, media, true, true, false); + if (item.hasMedia() && item.getMedia().isCurrentlyPlaying()) { + context.sendBroadcast(new Intent(PlaybackService.ACTION_PAUSE_PLAY_CURRENT_EPISODE)); + } + else if (item.hasMedia() && item.getMedia().isCurrentlyPaused()) { + context.sendBroadcast(new Intent(PlaybackService.ACTION_RESUME_PLAY_CURRENT_EPISODE)); + } + else { + DBTasks.playMedia(context, media, false, true, false); + } } } else { if (!item.isRead()) { diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/FeedItemlistAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/FeedItemlistAdapter.java index 8f1a838f9..d56bfc587 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/FeedItemlistAdapter.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/FeedItemlistAdapter.java @@ -9,6 +9,7 @@ import android.view.View.OnClickListener; import android.view.ViewGroup; import android.widget.*; import de.danoeh.antennapod.R; +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.MediaType; diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/NavListAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/NavListAdapter.java index a0ba0f794..05783e3ee 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/NavListAdapter.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/NavListAdapter.java @@ -25,7 +25,7 @@ public class NavListAdapter extends BaseAdapter { public static final int VIEW_TYPE_SECTION_DIVIDER = 1; public static final int VIEW_TYPE_SUBSCRIPTION = 2; - public static final int[] NAV_TITLES = {R.string.all_episodes_label, R.string.queue_label, R.string.downloads_label, R.string.playback_history_label, R.string.add_feed_label}; + public static final int[] NAV_TITLES = {R.string.queue_label, R.string.new_episodes_label, R.string.downloads_label, R.string.playback_history_label, R.string.add_feed_label}; private final Drawable[] drawables; @@ -38,7 +38,7 @@ public class NavListAdapter extends BaseAdapter { this.itemAccess = itemAccess; this.context = context; - TypedArray ta = context.obtainStyledAttributes(new int[]{R.attr.ic_new, R.attr.stat_playlist, + TypedArray ta = context.obtainStyledAttributes(new int[]{R.attr.stat_playlist, R.attr.ic_new, R.attr.av_download, R.attr.ic_history, R.attr.content_new}); drawables = new Drawable[]{ta.getDrawable(0), ta.getDrawable(1), ta.getDrawable(2), ta.getDrawable(3), ta.getDrawable(4)}; @@ -132,7 +132,7 @@ public class NavListAdapter extends BaseAdapter { } else { holder.count.setVisibility(View.GONE); } - } else if (NAV_TITLES[position] == R.string.all_episodes_label) { + } else if (NAV_TITLES[position] == R.string.new_episodes_label) { int unreadItems = itemAccess.getNumberOfUnreadItems(); if (unreadItems > 0) { holder.count.setVisibility(View.VISIBLE); diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/NewEpisodesListAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/NewEpisodesListAdapter.java index 1f98ec158..2d481a7ef 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/NewEpisodesListAdapter.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/NewEpisodesListAdapter.java @@ -1,6 +1,7 @@ package de.danoeh.antennapod.adapter; import android.content.Context; +import android.graphics.Color; import android.text.format.DateUtils; import android.view.LayoutInflater; import android.view.View; @@ -11,9 +12,11 @@ import android.widget.ImageView; import android.widget.ProgressBar; import android.widget.TextView; +import com.nineoldandroids.view.ViewHelper; import com.squareup.picasso.Picasso; import de.danoeh.antennapod.R; +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.storage.DownloadRequester; @@ -139,6 +142,13 @@ public class NewEpisodesListAdapter extends BaseAdapter { .fit() .into(holder.imageView); + if (item.isRead()) { + // grey it out + ViewHelper.setAlpha(convertView, .2f); + } else { + ViewHelper.setAlpha(convertView, 1.0f); + } + return convertView; } diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/QueueListAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/QueueListAdapter.java index d5b85575b..a256dc129 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/QueueListAdapter.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/QueueListAdapter.java @@ -1,6 +1,7 @@ package de.danoeh.antennapod.adapter; import android.content.Context; +import android.text.format.DateUtils; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -13,9 +14,11 @@ import android.widget.TextView; import com.squareup.picasso.Picasso; import de.danoeh.antennapod.R; +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.storage.DownloadRequester; +import de.danoeh.antennapod.core.util.Converter; /** * List adapter for the queue. @@ -64,12 +67,16 @@ public class QueueListAdapter extends BaseAdapter { .getSystemService(Context.LAYOUT_INFLATER_SERVICE); convertView = inflater.inflate(R.layout.queue_listitem, parent, false); + holder.imageView = (ImageView) convertView.findViewById(R.id.imgvImage); holder.title = (TextView) convertView.findViewById(R.id.txtvTitle); + holder.pubDate = (TextView) convertView.findViewById(R.id.txtvPubDate); + holder.progressLeft = (TextView) convertView.findViewById(R.id.txtvProgressLeft); + holder.progressRight = (TextView) convertView + .findViewById(R.id.txtvProgressRight); holder.butSecondary = (ImageButton) convertView .findViewById(R.id.butSecondaryAction); - holder.position = (TextView) convertView.findViewById(R.id.txtvPosition); holder.progress = (ProgressBar) convertView - .findViewById(R.id.pbar_download_progress); + .findViewById(R.id.progressBar); holder.imageView = (ImageView) convertView.findViewById(R.id.imgvImage); convertView.setTag(holder); } else { @@ -77,19 +84,39 @@ public class QueueListAdapter extends BaseAdapter { } holder.title.setText(item.getTitle()); + FeedMedia media = item.getMedia(); - AdapterUtils.updateEpisodePlaybackProgress(item, context.getResources(), holder.position, holder.progress); - FeedMedia media = item.getMedia(); + holder.title.setText(item.getTitle()); + String pubDate = DateUtils.formatDateTime(context, item.getPubDate().getTime(), DateUtils.FORMAT_ABBREV_ALL); + holder.pubDate.setText(pubDate.replace(" ", "\n")); + if (media != null) { final boolean isDownloadingMedia = DownloadRequester.getInstance().isDownloadingFile(media); - - if (!media.isDownloaded()) { - if (isDownloadingMedia) { - // item is being downloaded + FeedItem.State state = item.getState(); + if (isDownloadingMedia) { + holder.progressLeft.setText(Converter.byteToString(itemAccess.getItemDownloadedBytes(item))); + if(itemAccess.getItemDownloadSize(item) > 0) { + holder.progressRight.setText(Converter.byteToString(itemAccess.getItemDownloadSize(item))); + } else { + holder.progressRight.setText(Converter.byteToString(media.getSize())); + } + holder.progress.setProgress(itemAccess.getItemDownloadProgressPercent(item)); + holder.progress.setVisibility(View.VISIBLE); + } else if (state == FeedItem.State.PLAYING + || state == FeedItem.State.IN_PROGRESS) { + if (media.getDuration() > 0) { + int progress = (int) (100.0 * media.getPosition() / media.getDuration()); + holder.progress.setProgress(progress); holder.progress.setVisibility(View.VISIBLE); - holder.progress.setProgress(itemAccess.getItemDownloadProgressPercent(item)); + holder.progressLeft.setText(Converter + .getDurationStringLong(media.getPosition())); + holder.progressRight.setText(Converter.getDurationStringLong(media.getDuration())); } + } else { + holder.progressLeft.setText(Converter.byteToString(media.getSize())); + holder.progressRight.setText(Converter.getDurationStringLong(media.getDuration())); + holder.progress.setVisibility(View.GONE); } } @@ -116,18 +143,20 @@ public class QueueListAdapter extends BaseAdapter { static class Holder { - TextView title; ImageView imageView; - TextView position; + TextView title; + TextView pubDate; + TextView progressLeft; + TextView progressRight; ProgressBar progress; ImageButton butSecondary; } public interface ItemAccess { - int getCount(); - FeedItem getItem(int position); - + int getCount(); + long getItemDownloadedBytes(FeedItem item); + long getItemDownloadSize(FeedItem item); int getItemDownloadProgressPercent(FeedItem item); } } diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/gpodnet/PodcastListAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/gpodnet/PodcastListAdapter.java index 58af2c4d5..b85709c5e 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/gpodnet/PodcastListAdapter.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/gpodnet/PodcastListAdapter.java @@ -39,16 +39,15 @@ public class PodcastListAdapter extends ArrayAdapter<GpodnetPodcast> { .getSystemService(Context.LAYOUT_INFLATER_SERVICE); convertView = inflater.inflate(R.layout.gpodnet_podcast_listitem, parent, false); - holder.title = (TextView) convertView.findViewById(R.id.txtvTitle); holder.image = (ImageView) convertView.findViewById(R.id.imgvCover); - + holder.title = (TextView) convertView.findViewById(R.id.txtvTitle); + holder.subscribers = (TextView) convertView.findViewById(R.id.txtvSubscribers); + holder.url = (TextView) convertView.findViewById(R.id.txtvUrl); convertView.setTag(holder); } else { holder = (Holder) convertView.getTag(); } - holder.title.setText(podcast.getTitle()); - if (StringUtils.isNotBlank(podcast.getLogoUrl())) { Picasso.with(convertView.getContext()) .load(podcast.getLogoUrl()) @@ -56,11 +55,17 @@ public class PodcastListAdapter extends ArrayAdapter<GpodnetPodcast> { .into(holder.image); } + holder.title.setText(podcast.getTitle()); + holder.subscribers.setText(String.valueOf(podcast.getSubscribers())); + holder.url.setText(podcast.getUrl()); + return convertView; } static class Holder { - TextView title; ImageView image; + TextView title; + TextView subscribers; + TextView url; } } diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/gpodnet/TagListAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/gpodnet/TagListAdapter.java new file mode 100644 index 000000000..b4eadefb5 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/adapter/gpodnet/TagListAdapter.java @@ -0,0 +1,54 @@ +package de.danoeh.antennapod.adapter.gpodnet; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.TextView; + +import java.util.List; + +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.core.gpoddernet.model.GpodnetTag; + +/** + * Adapter for displaying a list of GPodnetPodcast-Objects. + */ +public class TagListAdapter extends ArrayAdapter<GpodnetTag> { + + public TagListAdapter(Context context, int resource, List<GpodnetTag> objects) { + super(context, resource, objects); + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + Holder holder; + + GpodnetTag tag = getItem(position); + + // Inflate Layout + if (convertView == null) { + holder = new Holder(); + LayoutInflater inflater = (LayoutInflater) getContext() + .getSystemService(Context.LAYOUT_INFLATER_SERVICE); + + convertView = inflater.inflate(R.layout.gpodnet_tag_listitem, parent, false); + holder.title = (TextView) convertView.findViewById(R.id.txtvTitle); + holder.usage = (TextView) convertView.findViewById(R.id.txtvUsage); + convertView.setTag(holder); + } else { + holder = (Holder) convertView.getTag(); + } + + holder.title.setText(tag.getTitle()); + holder.usage.setText(String.valueOf(tag.getUsage())); + + return convertView; + } + + static class Holder { + TextView title; + TextView usage; + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/itunes/ItunesAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/itunes/ItunesAdapter.java new file mode 100644 index 000000000..4fc2838b7 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/adapter/itunes/ItunesAdapter.java @@ -0,0 +1,187 @@ +package de.danoeh.antennapod.adapter.itunes; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.os.AsyncTask; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.ImageView; +import android.widget.TextView; + +import org.apache.http.HttpResponse; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.impl.client.DefaultHttpClient; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.IOException; +import java.util.List; + +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.activity.MainActivity; + +public class ItunesAdapter extends ArrayAdapter<ItunesAdapter.Podcast> { + /** + * Related Context + */ + private final Context context; + + /** + * List holding the podcasts found in the search + */ + private final List<Podcast> data; + + /** + * Constructor. + * + * @param context Related context + * @param objects Search result + */ + public ItunesAdapter(Context context, List<Podcast> objects) { + super(context, 0, objects); + this.data = objects; + this.context = context; + } + + /** + * Updates the given ImageView with the image in the given Podcast's imageUrl + */ + class FetchImageTask extends AsyncTask<Void,Void,Bitmap>{ + /** + * Current podcast + */ + private final Podcast podcast; + + /** + * ImageView to be updated + */ + private final ImageView imageView; + + /** + * Constructor + * + * @param podcast Podcast that has the image + * @param imageView UI image to be updated + */ + FetchImageTask(Podcast podcast, ImageView imageView){ + this.podcast = podcast; + this.imageView = imageView; + } + + //Get the image from the url + @Override + protected Bitmap doInBackground(Void... params) { + HttpClient client = new DefaultHttpClient(); + HttpGet get = new HttpGet(podcast.imageUrl); + try { + HttpResponse response = client.execute(get); + return BitmapFactory.decodeStream(response.getEntity().getContent()); + } catch (IOException e) { + e.printStackTrace(); + } + return null; + } + + //Set the background image for the podcast + @Override + protected void onPostExecute(Bitmap img) { + super.onPostExecute(img); + if(img!=null) { + imageView.setImageBitmap(img); + } + } + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + //Current podcast + Podcast podcast = data.get(position); + + //ViewHolder + PodcastViewHolder viewHolder; + + //Resulting view + View view; + + //Handle view holder stuff + if(convertView == null) { + view = ((MainActivity) context).getLayoutInflater() + .inflate(R.layout.itunes_podcast_listitem, parent, false); + viewHolder = new PodcastViewHolder(view); + view.setTag(viewHolder); + } else { + view = convertView; + viewHolder = (PodcastViewHolder) view.getTag(); + } + + //Set the title + viewHolder.titleView.setText(podcast.title); + + //Update the empty imageView with the image from the feed + new FetchImageTask(podcast,viewHolder.coverView).execute(); + + //Feed the grid view + return view; + } + + /** + * View holder object for the GridView + */ + class PodcastViewHolder { + + /** + * ImageView holding the Podcast image + */ + public final ImageView coverView; + + /** + * TextView holding the Podcast title + */ + public final TextView titleView; + + + /** + * Constructor + * @param view GridView cell + */ + PodcastViewHolder(View view){ + coverView = (ImageView) view.findViewById(R.id.imgvCover); + titleView = (TextView) view.findViewById(R.id.txtvTitle); + } + } + + /** + * Represents an individual podcast on the iTunes Store. + */ + public static class Podcast { //TODO: Move this out eventually. Possibly to core.itunes.model + + /** + * The name of the podcast + */ + public final String title; + + /** + * URL of the podcast image + */ + public final String imageUrl; + /** + * URL of the podcast feed + */ + public final String feedUrl; + + /** + * Constructor. + * + * @param json object holding the podcast information + * @throws JSONException + */ + public Podcast(JSONObject json) throws JSONException { + title = json.getString("collectionName"); + imageUrl = json.getString("artworkUrl100"); + feedUrl = json.getString("feedUrl"); + } + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/asynctask/OpmlFeedQueuer.java b/app/src/main/java/de/danoeh/antennapod/asynctask/OpmlFeedQueuer.java index cb9197b8e..00327bce0 100644 --- a/app/src/main/java/de/danoeh/antennapod/asynctask/OpmlFeedQueuer.java +++ b/app/src/main/java/de/danoeh/antennapod/asynctask/OpmlFeedQueuer.java @@ -4,16 +4,17 @@ import android.annotation.SuppressLint; import android.app.ProgressDialog; import android.content.Context; import android.os.AsyncTask; -import de.danoeh.antennapod.core.R; + +import java.util.Arrays; +import java.util.Date; + import de.danoeh.antennapod.activity.OpmlImportHolder; +import de.danoeh.antennapod.core.R; import de.danoeh.antennapod.core.feed.Feed; import de.danoeh.antennapod.core.opml.OpmlElement; import de.danoeh.antennapod.core.storage.DownloadRequestException; import de.danoeh.antennapod.core.storage.DownloadRequester; -import java.util.Arrays; -import java.util.Date; - /** Queues items for download in the background. */ public class OpmlFeedQueuer extends AsyncTask<Void, Void, Void> { private Context context; @@ -46,7 +47,7 @@ public class OpmlFeedQueuer extends AsyncTask<Void, Void, Void> { for (int idx = 0; idx < selection.length; idx++) { OpmlElement element = OpmlImportHolder.getReadElements().get( selection[idx]); - Feed feed = new Feed(element.getXmlUrl(), new Date(), + Feed feed = new Feed(element.getXmlUrl(), new Date(0), element.getText()); try { requester.downloadFeed(context.getApplicationContext(), feed); diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/AddFeedFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/AddFeedFragment.java index f5ae5a777..e4ae1683b 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/AddFeedFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/AddFeedFragment.java @@ -8,6 +8,7 @@ import android.view.View; import android.view.ViewGroup; import android.widget.Button; import android.widget.EditText; + import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.DefaultOnlineFeedViewActivity; import de.danoeh.antennapod.activity.MainActivity; @@ -41,10 +42,18 @@ public class AddFeedFragment extends Fragment { Button butBrowserGpoddernet = (Button) root.findViewById(R.id.butBrowseGpoddernet); Button butOpmlImport = (Button) root.findViewById(R.id.butOpmlImport); Button butConfirm = (Button) root.findViewById(R.id.butConfirm); + Button butSearchITunes = (Button) root.findViewById(R.id.butSearchItunes); final MainActivity activity = (MainActivity) getActivity(); activity.getMainActivtyActionBar().setTitle(R.string.add_feed_label); + butSearchITunes.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + activity.loadChildFragment(new ItunesSearchFragment()); + } + }); + butBrowserGpoddernet.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { @@ -53,7 +62,6 @@ public class AddFeedFragment extends Fragment { }); butOpmlImport.setOnClickListener(new View.OnClickListener() { - @Override public void onClick(View v) { startActivity(new Intent(getActivity(), diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/DownloadLogFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/DownloadLogFragment.java index c40fce351..0f6f7d53c 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/DownloadLogFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/DownloadLogFragment.java @@ -1,25 +1,35 @@ package de.danoeh.antennapod.fragment; import android.content.Context; +import android.content.res.TypedArray; import android.os.AsyncTask; import android.os.Bundle; import android.support.v4.app.ListFragment; +import android.support.v4.view.MenuItemCompat; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; import android.view.View; import android.widget.ListView; +import java.util.List; + import de.danoeh.antennapod.R; import de.danoeh.antennapod.adapter.DownloadLogAdapter; import de.danoeh.antennapod.core.feed.EventDistributor; import de.danoeh.antennapod.core.service.download.DownloadStatus; import de.danoeh.antennapod.core.storage.DBReader; - -import java.util.List; +import de.danoeh.antennapod.core.storage.DBWriter; +import de.danoeh.antennapod.menuhandler.MenuItemUtils; +import de.danoeh.antennapod.menuhandler.NavDrawerActivity; /** * Shows the download log */ public class DownloadLogFragment extends ListFragment { + private static final String TAG = "DownloadLogFragment"; + private List<DownloadStatus> downloadLog; private DownloadLogAdapter adapter; @@ -29,6 +39,7 @@ public class DownloadLogFragment extends ListFragment { @Override public void onStart() { super.onStart(); + setHasOptionsMenu(true); EventDistributor.getInstance().register(contentUpdate); startItemLoader(); } @@ -63,7 +74,7 @@ public class DownloadLogFragment extends ListFragment { } setListShown(true); adapter.notifyDataSetChanged(); - + getActivity().supportInvalidateOptionsMenu(); } private DownloadLogAdapter.ItemAccess itemAccess = new DownloadLogAdapter.ItemAccess() { @@ -105,6 +116,41 @@ public class DownloadLogFragment extends ListFragment { } } + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + super.onCreateOptionsMenu(menu, inflater); + if (itemsLoaded && !MenuItemUtils.isActivityDrawerOpen((NavDrawerActivity) getActivity())) { + MenuItem clearHistory = menu.add(Menu.NONE, R.id.clear_history_item, Menu.CATEGORY_CONTAINER, R.string.clear_history_label); + MenuItemCompat.setShowAsAction(clearHistory, MenuItemCompat.SHOW_AS_ACTION_IF_ROOM); + TypedArray drawables = getActivity().obtainStyledAttributes(new int[]{R.attr.content_discard}); + clearHistory.setIcon(drawables.getDrawable(0)); + drawables.recycle(); + } + } + + @Override + public void onPrepareOptionsMenu(Menu menu) { + super.onPrepareOptionsMenu(menu); + if (itemsLoaded && !MenuItemUtils.isActivityDrawerOpen((NavDrawerActivity) getActivity())) { + menu.findItem(R.id.clear_history_item).setVisible(downloadLog != null && !downloadLog.isEmpty()); + } + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (!super.onOptionsItemSelected(item)) { + switch (item.getItemId()) { + case R.id.clear_history_item: + DBWriter.clearDownloadLog(getActivity()); + return true; + default: + return false; + } + } else { + return true; + } + } + private class ItemLoader extends AsyncTask<Void, Void, List<DownloadStatus>> { @Override diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/ItemFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/ItemFragment.java index ac9e744ed..e80bf5f14 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/ItemFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/ItemFragment.java @@ -16,6 +16,7 @@ import android.support.v4.util.Pair; import android.support.v7.widget.PopupMenu; import android.support.v7.widget.Toolbar; import android.text.TextUtils; +import android.text.format.DateUtils; import android.view.LayoutInflater; import android.view.MenuItem; import android.view.View; @@ -49,6 +50,7 @@ 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.Converter; import de.danoeh.antennapod.core.util.QueueAccess; import de.danoeh.antennapod.core.util.playback.Timeline; import de.danoeh.antennapod.menuhandler.FeedItemMenuHandler; @@ -91,6 +93,8 @@ public class ItemFragment extends Fragment implements LoaderManager.LoaderCallba private View header; private WebView webvDescription; private TextView txtvTitle; + private TextView txtvDuration; + private TextView txtvPublished; private ImageView imgvCover; private ProgressBar progbarDownload; private ProgressBar progbarLoading; @@ -166,6 +170,8 @@ public class ItemFragment extends Fragment implements LoaderManager.LoaderCallba header = inflater.inflate(R.layout.feeditem_fragment_header, toolbar, false); root = (ViewGroup) layout.findViewById(R.id.content_root); txtvTitle = (TextView) header.findViewById(R.id.txtvTitle); + txtvDuration = (TextView) header.findViewById(R.id.txtvDuration); + txtvPublished = (TextView) header.findViewById(R.id.txtvPublished); if (Build.VERSION.SDK_INT >= 14) { // ellipsize is causing problems on old versions, see #448 txtvTitle.setEllipsize(TextUtils.TruncateAt.END); } @@ -313,6 +319,8 @@ public class ItemFragment extends Fragment implements LoaderManager.LoaderCallba private void updateAppearance() { txtvTitle.setText(item.getTitle()); + txtvPublished.setText(DateUtils.formatDateTime(getActivity(), item.getPubDate().getTime(), DateUtils.FORMAT_ABBREV_ALL)); + Picasso.with(getActivity()).load(item.getImageUri()) .fit() .into(imgvCover); @@ -348,7 +356,10 @@ public class ItemFragment extends Fragment implements LoaderManager.LoaderCallba } drawables.recycle(); - } else { + } else {if(media.getDuration() > 0) { + txtvDuration.setText(Converter.getDurationStringLong(media.getDuration())); + } + boolean isDownloading = DownloadRequester.getInstance().isDownloadingFile(media); TypedArray drawables = getActivity().obtainStyledAttributes(new int[]{R.attr.av_play, R.attr.av_download, R.attr.action_stream, R.attr.content_discard, R.attr.navigation_cancel}); diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/ItemlistFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/ItemlistFragment.java index 5312beeeb..acb07626c 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/ItemlistFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/ItemlistFragment.java @@ -66,7 +66,8 @@ public class ItemlistFragment extends ListFragment { private static final int EVENTS = EventDistributor.DOWNLOAD_HANDLED | EventDistributor.DOWNLOAD_QUEUED | EventDistributor.QUEUE_UPDATE - | EventDistributor.UNREAD_ITEMS_UPDATE; + | EventDistributor.UNREAD_ITEMS_UPDATE + | EventDistributor.PLAYER_STATUS_UPDATE; public static final String EXTRA_SELECTED_FEEDITEM = "extra.de.danoeh.antennapod.activity.selected_feeditem"; public static final String ARGUMENT_FEED_ID = "argument.de.danoeh.antennapod.feed_id"; diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/ItunesSearchFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/ItunesSearchFragment.java new file mode 100644 index 000000000..c14b0cc6e --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/fragment/ItunesSearchFragment.java @@ -0,0 +1,193 @@ +package de.danoeh.antennapod.fragment; + +import android.content.Intent; +import android.os.AsyncTask; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.support.v7.widget.SearchView; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.GridView; + +import org.apache.http.HttpResponse; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.impl.client.DefaultHttpClient; +import org.apache.http.util.EntityUtils; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.activity.DefaultOnlineFeedViewActivity; +import de.danoeh.antennapod.activity.OnlineFeedViewActivity; +import de.danoeh.antennapod.adapter.itunes.ItunesAdapter; + +import static de.danoeh.antennapod.adapter.itunes.ItunesAdapter.*; + +//Searches iTunes store for given string and displays results in a list +public class ItunesSearchFragment extends Fragment { + final String TAG = "ItunesSearchFragment"; + /** + * Search input field + */ + private SearchView searchView; + + /** + * Adapter responsible with the search results + */ + private ItunesAdapter adapter; + + /** + * List of podcasts retreived from the search + */ + private List<Podcast> searchResults; + + /** + * Replace adapter data with provided search results from SearchTask. + * @param result List of Podcast objects containing search results + */ + void updateData(List<Podcast> result) { + this.searchResults = result; + adapter.clear(); + + //ArrayAdapter.addAll() requires minsdk > 10 + for(Podcast p: result) { + adapter.add(p); + } + + adapter.notifyDataSetInvalidated(); + } + + /** + * Constructor + */ + public ItunesSearchFragment() { + // Required empty public constructor + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + adapter = new ItunesAdapter(getActivity(), new ArrayList<Podcast>()); + + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + // Inflate the layout for this fragment + View view = inflater.inflate(R.layout.fragment_itunes_search, container, false); + GridView gridView = (GridView) view.findViewById(R.id.gridView); + gridView.setAdapter(adapter); + + //Show information about the podcast when the list item is clicked + gridView.setOnItemClickListener(new AdapterView.OnItemClickListener() { + @Override + public void onItemClick(AdapterView<?> parent, View view, int position, long id) { + Intent intent = new Intent(getActivity(), + DefaultOnlineFeedViewActivity.class); + + //Tell the OnlineFeedViewActivity where to go + String url = searchResults.get(position).feedUrl; + intent.putExtra(OnlineFeedViewActivity.ARG_FEEDURL, url); + + intent.putExtra(DefaultOnlineFeedViewActivity.ARG_TITLE, "iTunes"); + startActivity(intent); + } + }); + + //Configure search input view to be expanded by default with a visible submit button + searchView = (SearchView) view.findViewById(R.id.itunes_search_view); + searchView.setIconifiedByDefault(false); + searchView.setIconified(false); + searchView.setSubmitButtonEnabled(true); + searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { + @Override + public boolean onQueryTextSubmit(String s) { + //This prevents onQueryTextSubmit() from being called twice when keyboard is used + //to submit the query. + searchView.clearFocus(); + new SearchTask(s).execute(); + return false; + } + + @Override + public boolean onQueryTextChange(String s) { + return false; + } + }); + + return view; + } + + /** + * Search the iTunes store for podcasts using the given query + */ + class SearchTask extends AsyncTask<Void,Void,Void> { + /** + * Incomplete iTunes API search URL + */ + final String apiUrl = "https://itunes.apple.com/search?media=podcast&term=%s"; + + /** + * Search terms + */ + final String query; + + /** + * Search result + */ + final List<Podcast> taskData = new ArrayList<>(); + + /** + * Constructor + * + * @param query Search string + */ + public SearchTask(String query){ + this.query = query; + } + + //Get the podcast data + @Override + protected Void doInBackground(Void... params) { + + //Spaces in the query need to be replaced with '+' character. + String formattedUrl = String.format(apiUrl, query).replace(' ', '+'); + + HttpClient client = new DefaultHttpClient(); + HttpGet get = new HttpGet(formattedUrl); + + try { + HttpResponse response = client.execute(get); + String resultString = EntityUtils.toString(response.getEntity()); + JSONObject result = new JSONObject(resultString); + JSONArray j = result.getJSONArray("results"); + + for (int i = 0; i < j.length(); i++){ + JSONObject podcastJson = j.getJSONObject(i); + Podcast podcast = new Podcast(podcastJson); + taskData.add(podcast); + } + + } catch (IOException | JSONException e) { + e.printStackTrace(); + } + return null; + } + + //Save the data and update the list + @Override + protected void onPostExecute(Void aVoid) { + super.onPostExecute(aVoid); + updateData(taskData); + } + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/NewEpisodesFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/NewEpisodesFragment.java index d97ede0ef..8bc4099a9 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/NewEpisodesFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/NewEpisodesFragment.java @@ -2,12 +2,15 @@ package de.danoeh.antennapod.fragment; import android.app.Activity; import android.content.Context; +import android.content.DialogInterface; 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; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; @@ -29,6 +32,7 @@ import de.danoeh.antennapod.activity.MainActivity; import de.danoeh.antennapod.adapter.DefaultActionButtonCallback; import de.danoeh.antennapod.adapter.NewEpisodesListAdapter; import de.danoeh.antennapod.core.asynctask.DownloadObserver; +import de.danoeh.antennapod.core.dialog.ConfirmationDialog; import de.danoeh.antennapod.core.feed.EventDistributor; import de.danoeh.antennapod.core.feed.Feed; import de.danoeh.antennapod.core.feed.FeedItem; @@ -41,6 +45,8 @@ import de.danoeh.antennapod.core.storage.DBTasks; import de.danoeh.antennapod.core.storage.DBWriter; import de.danoeh.antennapod.core.storage.DownloadRequester; import de.danoeh.antennapod.core.util.QueueAccess; +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; @@ -52,18 +58,22 @@ public class NewEpisodesFragment extends Fragment { private static final int EVENTS = EventDistributor.DOWNLOAD_HANDLED | EventDistributor.DOWNLOAD_QUEUED | EventDistributor.QUEUE_UPDATE | - EventDistributor.UNREAD_ITEMS_UPDATE; + EventDistributor.UNREAD_ITEMS_UPDATE | + EventDistributor.PLAYER_STATUS_UPDATE; private static final int RECENT_EPISODES_LIMIT = 150; private static final String PREF_NAME = "PrefNewEpisodesFragment"; private static final String PREF_EPISODE_FILTER_BOOL = "newEpisodeFilterEnabled"; - + private static final String PREF_KEY_LIST_TOP = "list_top"; + private static final String PREF_KEY_LIST_SELECTION = "list_selection"; private DragSortListView listView; private NewEpisodesListAdapter listAdapter; private TextView txtvEmpty; private ProgressBar progLoading; + private UndoBarController undoBarController; + private List<FeedItem> unreadItems; private List<FeedItem> recentItems; private QueueAccess queueAccess; @@ -109,6 +119,12 @@ public class NewEpisodesFragment extends Fragment { } @Override + public void onPause() { + super.onPause(); + saveScrollPosition(); + } + + @Override public void onStop() { super.onStop(); EventDistributor.getInstance().unregister(contentUpdate); @@ -127,10 +143,35 @@ public class NewEpisodesFragment extends Fragment { resetViewState(); } + private void saveScrollPosition() { + SharedPreferences prefs = getActivity().getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE); + SharedPreferences.Editor editor = prefs.edit(); + View v = listView.getChildAt(0); + int top = (v == null) ? 0 : (v.getTop() - listView.getPaddingTop()); + editor.putInt(PREF_KEY_LIST_SELECTION, listView.getFirstVisiblePosition()); + editor.putInt(PREF_KEY_LIST_TOP, top); + editor.commit(); + } + + private void restoreScrollPosition() { + SharedPreferences prefs = getActivity().getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE); + int listSelection = prefs.getInt(PREF_KEY_LIST_SELECTION, 0); + int top = prefs.getInt(PREF_KEY_LIST_TOP, 0); + if(listSelection > 0 || top > 0) { + listView.setSelectionFromTop(listSelection, top); + // restore once, then forget + SharedPreferences.Editor editor = prefs.edit(); + editor.putInt(PREF_KEY_LIST_SELECTION, 0); + editor.putInt(PREF_KEY_LIST_TOP, 0); + editor.commit(); + } + } + private void resetViewState() { listAdapter = null; activity.set(null); viewsCreated = false; + undoBarController = null; if (downloadObserver != null) { downloadObserver.onPause(); } @@ -190,8 +231,19 @@ public class NewEpisodesFragment extends Fragment { } return true; case R.id.mark_all_read_item: - DBWriter.markAllItemsRead(getActivity()); - Toast.makeText(getActivity(), R.string.mark_all_read_msg, Toast.LENGTH_SHORT).show(); + ConfirmationDialog conDialog = new ConfirmationDialog(getActivity(), + R.string.mark_all_read_label, + R.string.mark_all_read_confirmation_msg) { + + @Override + public void onConfirmButtonPressed( + DialogInterface dialog) { + dialog.dismiss(); + DBWriter.markAllItemsRead(getActivity()); + Toast.makeText(getActivity(), R.string.mark_all_read_msg, Toast.LENGTH_SHORT).show(); + } + }; + conDialog.createNewDialog().show(); return true; case R.id.episode_filter_item: boolean newVal = !item.isChecked(); @@ -210,7 +262,7 @@ public class NewEpisodesFragment extends Fragment { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { super.onCreateView(inflater, container, savedInstanceState); - ((MainActivity) getActivity()).getSupportActionBar().setTitle(R.string.all_episodes_label); + ((MainActivity) getActivity()).getSupportActionBar().setTitle(R.string.new_episodes_label); View root = inflater.inflate(R.layout.new_episodes_fragment, container, false); @@ -229,6 +281,33 @@ public class NewEpisodesFragment extends Fragment { } }); + listView.setRemoveListener(new DragSortListView.RemoveListener() { + @Override + public void remove(int which) { + Log.d(TAG, "remove("+which+")"); + stopItemLoader(); + FeedItem item = (FeedItem) listView.getAdapter().getItem(which); + DBWriter.markItemRead(getActivity(), item.getId(), true); + undoBarController.showUndoBar(false, + getString(R.string.marked_as_read_label), new FeedItemUndoToken(item, + which) + ); + } + }); + + 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.markItemRead(getActivity(), itemId, false); + } + } + }); + final int secondColor = (UserPreferences.getTheme() == R.style.Theme_AntennaPod_Dark) ? R.color.swipe_refresh_secondary_color_dark : R.color.swipe_refresh_secondary_color_light; if (!itemsLoaded) { @@ -254,6 +333,7 @@ public class NewEpisodesFragment extends Fragment { downloadObserver.onResume(); } listAdapter.notifyDataSetChanged(); + restoreScrollPosition(); getActivity().supportInvalidateOptionsMenu(); updateShowOnlyEpisodesListViewState(); } @@ -332,7 +412,7 @@ public class NewEpisodesFragment extends Fragment { private void updateShowOnlyEpisodes() { SharedPreferences prefs = getActivity().getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE); - showOnlyNewEpisodes = prefs.getBoolean(PREF_EPISODE_FILTER_BOOL, false); + showOnlyNewEpisodes = prefs.getBoolean(PREF_EPISODE_FILTER_BOOL, true); } private void setShowOnlyNewEpisodes(boolean newVal) { diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/PlaybackHistoryFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/PlaybackHistoryFragment.java index f6d2d5d07..ab38af106 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/PlaybackHistoryFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/PlaybackHistoryFragment.java @@ -34,6 +34,8 @@ import de.danoeh.antennapod.menuhandler.NavDrawerActivity; public class PlaybackHistoryFragment extends ListFragment { private static final String TAG = "PlaybackHistoryFragment"; + private static final int EVENTS = EventDistributor.PLAYBACK_HISTORY_UPDATE | + EventDistributor.PLAYER_STATUS_UPDATE; private List<FeedItem> playbackHistory; private QueueAccess queue; @@ -167,7 +169,7 @@ public class PlaybackHistoryFragment extends ListFragment { @Override public void update(EventDistributor eventDistributor, Integer arg) { - if ((arg & EventDistributor.PLAYBACK_HISTORY_UPDATE) != 0) { + if ((arg & EVENTS) != 0) { startItemLoader(); getActivity().supportInvalidateOptionsMenu(); } diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/QueueFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/QueueFragment.java index ca8543b4c..70a231cad 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/QueueFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/QueueFragment.java @@ -2,10 +2,12 @@ package de.danoeh.antennapod.fragment; import android.app.Activity; import android.content.Context; +import android.content.DialogInterface; +import android.content.SharedPreferences; import android.os.AsyncTask; -import android.os.Build; 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; @@ -30,6 +32,7 @@ import de.danoeh.antennapod.activity.MainActivity; import de.danoeh.antennapod.adapter.DefaultActionButtonCallback; import de.danoeh.antennapod.adapter.QueueListAdapter; import de.danoeh.antennapod.core.asynctask.DownloadObserver; +import de.danoeh.antennapod.core.dialog.ConfirmationDialog; import de.danoeh.antennapod.core.feed.EventDistributor; import de.danoeh.antennapod.core.feed.Feed; import de.danoeh.antennapod.core.feed.FeedItem; @@ -41,6 +44,8 @@ import de.danoeh.antennapod.core.storage.DBTasks; import de.danoeh.antennapod.core.storage.DBWriter; import de.danoeh.antennapod.core.storage.DownloadRequester; import de.danoeh.antennapod.core.util.QueueSorter; +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; @@ -51,13 +56,16 @@ 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.QUEUE_UPDATE | + EventDistributor.PLAYER_STATUS_UPDATE; private DragSortListView listView; private QueueListAdapter listAdapter; private TextView txtvEmpty; private ProgressBar progLoading; + private UndoBarController undoBarController; + private List<FeedItem> queue; private List<Downloader> downloaderList; @@ -65,6 +73,10 @@ public class QueueFragment extends Fragment { private boolean viewsCreated = false; private boolean isUpdatingFeeds = false; + private static final String PREFS = "QueueFragment"; + private static final String PREF_KEY_LIST_TOP = "list_top"; + private static final String PREF_KEY_LIST_SELECTION = "list_selection"; + private AtomicReference<Activity> activity = new AtomicReference<Activity>(); private DownloadObserver downloadObserver = null; @@ -103,6 +115,12 @@ public class QueueFragment extends Fragment { } @Override + public void onPause() { + super.onPause(); + saveScrollPosition(); + } + + @Override public void onStop() { super.onStop(); EventDistributor.getInstance().unregister(contentUpdate); @@ -115,10 +133,35 @@ public class QueueFragment extends Fragment { this.activity.set((MainActivity) activity); } + private void saveScrollPosition() { + SharedPreferences prefs = getActivity().getSharedPreferences(PREFS, Context.MODE_PRIVATE); + SharedPreferences.Editor editor = prefs.edit(); + View v = listView.getChildAt(0); + int top = (v == null) ? 0 : (v.getTop() - listView.getPaddingTop()); + editor.putInt(PREF_KEY_LIST_SELECTION, listView.getFirstVisiblePosition()); + editor.putInt(PREF_KEY_LIST_TOP, top); + editor.commit(); + } + + private void restoreScrollPosition() { + SharedPreferences prefs = getActivity().getSharedPreferences(PREFS, Context.MODE_PRIVATE); + int listSelection = prefs.getInt(PREF_KEY_LIST_SELECTION, 0); + int top = prefs.getInt(PREF_KEY_LIST_TOP, 0); + if(listSelection > 0 || top > 0) { + listView.setSelectionFromTop(listSelection, top); + // restore once, then forget + SharedPreferences.Editor editor = prefs.edit(); + editor.putInt(PREF_KEY_LIST_SELECTION, 0); + editor.putInt(PREF_KEY_LIST_TOP, 0); + editor.commit(); + } + } + private void resetViewState() { unregisterForContextMenu(listView); listAdapter = null; activity.set(null); + undoBarController = null; viewsCreated = false; blockDownloadObserverUpdate = false; if (downloadObserver != null) { @@ -175,6 +218,21 @@ public class QueueFragment extends Fragment { DBTasks.refreshAllFeeds(getActivity(), feeds); } return true; + case R.id.clear_queue: + // make sure the user really wants to clear the queue + ConfirmationDialog conDialog = new ConfirmationDialog(getActivity(), + R.string.clear_queue_label, + R.string.clear_queue_confirmation_msg) { + + @Override + public void onConfirmButtonPressed( + DialogInterface dialog) { + dialog.dismiss(); + DBWriter.clearQueue(getActivity()); + } + }; + conDialog.createNewDialog().show(); + return true; case R.id.queue_sort_alpha_asc: QueueSorter.sort(getActivity(), QueueSorter.Rule.ALPHA_ASC, true); return true; @@ -285,9 +343,31 @@ public class QueueFragment extends Fragment { @Override public void remove(int 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) + ); } }); + 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); + } + } + }); + + registerForContextMenu(listView); if (!itemsLoaded) { @@ -313,6 +393,8 @@ public class QueueFragment extends Fragment { } listAdapter.notifyDataSetChanged(); + restoreScrollPosition(); + // we need to refresh the options menu because it sometimes // needs data that may have just been loaded. getActivity().supportInvalidateOptionsMenu(); @@ -347,6 +429,33 @@ public class QueueFragment extends Fragment { } @Override + public long getItemDownloadedBytes(FeedItem item) { + if (downloaderList != null) { + for (Downloader downloader : downloaderList) { + if (downloader.getDownloadRequest().getFeedfileType() == FeedMedia.FEEDFILETYPE_FEEDMEDIA + && downloader.getDownloadRequest().getFeedfileId() == item.getMedia().getId()) { + Log.d(TAG, "downloaded bytes: " + downloader.getDownloadRequest().getSoFar()); + return downloader.getDownloadRequest().getSoFar(); + } + } + } + return 0; + } + + @Override + public long getItemDownloadSize(FeedItem item) { + if (downloaderList != null) { + for (Downloader downloader : downloaderList) { + if (downloader.getDownloadRequest().getFeedfileType() == FeedMedia.FEEDFILETYPE_FEEDMEDIA + && downloader.getDownloadRequest().getFeedfileId() == item.getMedia().getId()) { + Log.d(TAG, "downloaded size: " + downloader.getDownloadRequest().getSize()); + return downloader.getDownloadRequest().getSize(); + } + } + } + return 0; + } + @Override public int getItemDownloadProgressPercent(FeedItem item) { if (downloaderList != null) { for (Downloader downloader : downloaderList) { diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/TagFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/TagFragment.java index c8cdbcfed..e2450f03d 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/TagFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/TagFragment.java @@ -24,11 +24,11 @@ public class TagFragment extends PodcastListFragment { private GpodnetTag tag; - public static TagFragment newInstance(String tagName) { - Validate.notNull(tagName); + public static TagFragment newInstance(GpodnetTag tag) { + Validate.notNull(tag); TagFragment fragment = new TagFragment(); Bundle args = new Bundle(); - args.putString("tag", tagName); + args.putParcelable("tag", tag); fragment.setArguments(args); return fragment; } @@ -38,14 +38,14 @@ public class TagFragment extends PodcastListFragment { super.onCreate(savedInstanceState); Bundle args = getArguments(); - Validate.isTrue(args != null && args.getString("tag") != null, "args invalid"); - tag = new GpodnetTag(args.getString("tag")); + Validate.isTrue(args != null && args.getParcelable("tag") != null, "args invalid"); + tag = args.getParcelable("tag"); } @Override public void onActivityCreated(@Nullable Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); - ((MainActivity) getActivity()).getMainActivtyActionBar().setTitle(tag.getName()); + ((MainActivity) getActivity()).getMainActivtyActionBar().setTitle(tag.getTitle()); } @Override diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/TagListFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/TagListFragment.java index 24e0e4caa..cc87407b4 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/TagListFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/TagListFragment.java @@ -10,14 +10,13 @@ import android.view.Menu; import android.view.MenuInflater; import android.view.View; import android.widget.AdapterView; -import android.widget.ArrayAdapter; import android.widget.TextView; -import java.util.ArrayList; import java.util.List; import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.MainActivity; +import de.danoeh.antennapod.adapter.gpodnet.TagListAdapter; import de.danoeh.antennapod.core.gpoddernet.GpodnetService; import de.danoeh.antennapod.core.gpoddernet.GpodnetServiceException; import de.danoeh.antennapod.core.gpoddernet.model.GpodnetTag; @@ -67,9 +66,9 @@ public class TagListFragment extends ListFragment { getListView().setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { - String selectedTag = (String) getListAdapter().getItem(position); + GpodnetTag tag = (GpodnetTag) getListAdapter().getItem(position); MainActivity activity = (MainActivity) getActivity(); - activity.loadChildFragment(TagFragment.newInstance(selectedTag)); + activity.loadChildFragment(TagFragment.newInstance(tag)); } }); @@ -77,6 +76,12 @@ public class TagListFragment extends ListFragment { } @Override + public void onResume() { + super.onResume(); + ((MainActivity) getActivity()).getMainActivtyActionBar().setTitle(R.string.add_feed_label); + } + + @Override public void onDestroyView() { super.onDestroyView(); cancelLoadTask(); @@ -121,11 +126,7 @@ public class TagListFragment extends ListFragment { final Context context = getActivity(); if (context != null) { if (gpodnetTags != null) { - List<String> tagNames = new ArrayList<String>(); - for (GpodnetTag tag : gpodnetTags) { - tagNames.add(tag.getName()); - } - setListAdapter(new ArrayAdapter<String>(context, android.R.layout.simple_list_item_1, tagNames)); + setListAdapter(new TagListAdapter(context, android.R.layout.simple_list_item_1, gpodnetTags)); } else if (exception != null) { TextView txtvError = new TextView(getActivity()); txtvError.setText(exception.getMessage()); diff --git a/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedMenuHandler.java b/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedMenuHandler.java index b62fd22b2..efb4adb01 100644 --- a/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedMenuHandler.java +++ b/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedMenuHandler.java @@ -1,6 +1,7 @@ package de.danoeh.antennapod.menuhandler; import android.content.Context; +import android.content.DialogInterface; import android.content.Intent; import android.net.Uri; import android.util.Log; @@ -10,6 +11,7 @@ import android.view.MenuItem; import de.danoeh.antennapod.R; import de.danoeh.antennapod.core.BuildConfig; +import de.danoeh.antennapod.core.dialog.ConfirmationDialog; import de.danoeh.antennapod.core.feed.Feed; import de.danoeh.antennapod.core.storage.DBTasks; import de.danoeh.antennapod.core.storage.DBWriter; @@ -51,8 +53,8 @@ public class FeedMenuHandler { * * @throws DownloadRequestException */ - public static boolean onOptionsItemClicked(Context context, MenuItem item, - Feed selectedFeed) throws DownloadRequestException { + public static boolean onOptionsItemClicked(final Context context, final MenuItem item, + final Feed selectedFeed) throws DownloadRequestException { switch (item.getItemId()) { case R.id.refresh_item: DBTasks.refreshFeed(context, selectedFeed); @@ -61,7 +63,18 @@ public class FeedMenuHandler { DBTasks.refreshCompleteFeed(context, selectedFeed); break; case R.id.mark_all_read_item: - DBWriter.markFeedRead(context, selectedFeed.getId()); + ConfirmationDialog conDialog = new ConfirmationDialog(context, + R.string.mark_all_read_label, + R.string.mark_all_read_feed_confirmation_msg) { + + @Override + public void onConfirmButtonPressed( + DialogInterface dialog) { + dialog.dismiss(); + DBWriter.markFeedRead(context, selectedFeed.getId()); + } + }; + conDialog.createNewDialog().show(); break; case R.id.visit_website_item: Uri uri = Uri.parse(selectedFeed.getLink()); diff --git a/app/src/main/java/de/danoeh/antennapod/menuhandler/MenuItemUtils.java b/app/src/main/java/de/danoeh/antennapod/menuhandler/MenuItemUtils.java index 05d6ded4d..fc942ce20 100644 --- a/app/src/main/java/de/danoeh/antennapod/menuhandler/MenuItemUtils.java +++ b/app/src/main/java/de/danoeh/antennapod/menuhandler/MenuItemUtils.java @@ -14,7 +14,7 @@ public class MenuItemUtils extends de.danoeh.antennapod.core.menuhandler.MenuIte public static MenuItem addSearchItem(Menu menu, SearchView searchView) { MenuItem item = menu.add(Menu.NONE, R.id.search_item, Menu.NONE, R.string.search_label); - MenuItemCompat.setShowAsAction(item, MenuItemCompat.SHOW_AS_ACTION_IF_ROOM); + MenuItemCompat.setShowAsAction(item, MenuItemCompat.SHOW_AS_ACTION_ALWAYS); MenuItemCompat.setActionView(item, searchView); return item; } diff --git a/app/src/main/java/de/danoeh/antennapod/preferences/CustomEditTextPreference.java b/app/src/main/java/de/danoeh/antennapod/preferences/CustomEditTextPreference.java new file mode 100644 index 000000000..898a56004 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/preferences/CustomEditTextPreference.java @@ -0,0 +1,33 @@ +package de.danoeh.antennapod.preferences; + +import android.app.AlertDialog; +import android.content.Context; +import android.os.Build; +import android.preference.EditTextPreference; +import android.util.AttributeSet; + +import de.danoeh.antennapod.R; + +public class CustomEditTextPreference extends EditTextPreference { + + public CustomEditTextPreference(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + public CustomEditTextPreference(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public CustomEditTextPreference(Context context) { + super(context); + } + + @Override + protected void onPrepareDialogBuilder(AlertDialog.Builder builder) { + if(Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) { + builder.setInverseBackgroundForced(true); + getEditText().setTextColor(getContext().getResources().getColor(R.color.black)); + } + } + +} diff --git a/app/src/main/java/de/danoeh/antennapod/preferences/PreferenceController.java b/app/src/main/java/de/danoeh/antennapod/preferences/PreferenceController.java index ffac05321..227ea8dfb 100644 --- a/app/src/main/java/de/danoeh/antennapod/preferences/PreferenceController.java +++ b/app/src/main/java/de/danoeh/antennapod/preferences/PreferenceController.java @@ -9,10 +9,14 @@ import android.net.wifi.WifiConfiguration; import android.net.wifi.WifiManager; import android.os.Build; import android.preference.CheckBoxPreference; +import android.preference.EditTextPreference; import android.preference.ListPreference; import android.preference.Preference; import android.preference.PreferenceScreen; +import android.text.Editable; +import android.text.TextWatcher; import android.util.Log; +import android.widget.EditText; import android.widget.Toast; import java.io.File; @@ -59,8 +63,6 @@ public class PreferenceController { public static final String PREF_GPODNET_LOGOUT = "pref_gpodnet_logout"; public static final String PREF_GPODNET_HOSTNAME = "pref_gpodnet_hostname"; public static final String PREF_EXPANDED_NOTIFICATION = "prefExpandNotify"; - private static final String PREF_PERSISTENT_NOTIFICATION = "prefPersistNotify"; - private final PreferenceUI ui; @@ -216,6 +218,52 @@ public class PreferenceController { } } ); + ui.findPreference(UserPreferences.PREF_PARALLEL_DOWNLOADS) + .setOnPreferenceChangeListener( + new Preference.OnPreferenceChangeListener() { + @Override + public boolean onPreferenceChange(Preference preference, Object o) { + if (o instanceof String) { + try { + int value = Integer.valueOf((String) o); + if (1 <= value && value <= 50) { + setParallelDownloadsText(value); + return true; + } + } catch(NumberFormatException e) { + return false; + } + } + return false; + } + } + ); + // validate and set correct value: number of downloads between 1 and 50 (inclusive) + final EditText ev = ((EditTextPreference)ui.findPreference(UserPreferences.PREF_PARALLEL_DOWNLOADS)).getEditText(); + ev.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) {} + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) {} + + @Override + public void afterTextChanged(Editable s) { + if(s.length() > 0) { + try { + int value = Integer.valueOf(s.toString()); + if (value <= 0) { + ev.setText("1"); + } else if (value > 50) { + ev.setText("50"); + } + } catch(NumberFormatException e) { + ev.setText("6"); + } + ev.setSelection(ev.getText().length()); + } + } + }); ui.findPreference(UserPreferences.PREF_EPISODE_CACHE_SIZE) .setOnPreferenceChangeListener( new Preference.OnPreferenceChangeListener() { @@ -302,6 +350,7 @@ public class PreferenceController { public void onResume() { checkItemVisibility(); + setParallelDownloadsText(UserPreferences.getParallelDownloads()); setEpisodeCacheSizeText(UserPreferences.getEpisodeCacheSize()); setDataFolderText(); updateGpodnetPreferenceScreen(); @@ -381,6 +430,15 @@ public class PreferenceController { .setEnabled(UserPreferences.isEnableAutodownload()); } + + private void setParallelDownloadsText(int downloads) { + final Resources res = ui.getActivity().getResources(); + + String s = Integer.toString(downloads) + + res.getString(R.string.parallel_downloads_suffix); + ui.findPreference(UserPreferences.PREF_PARALLEL_DOWNLOADS).setSummary(s); + } + private void setEpisodeCacheSizeText(int cacheSize) { final Resources res = ui.getActivity().getResources(); diff --git a/app/src/main/java/de/danoeh/antennapod/receiver/PowerConnectionReceiver.java b/app/src/main/java/de/danoeh/antennapod/receiver/PowerConnectionReceiver.java index 0e7784381..f050e031d 100644 --- a/app/src/main/java/de/danoeh/antennapod/receiver/PowerConnectionReceiver.java +++ b/app/src/main/java/de/danoeh/antennapod/receiver/PowerConnectionReceiver.java @@ -13,16 +13,19 @@ import de.danoeh.antennapod.core.storage.DownloadRequester; // modified from http://developer.android.com/training/monitoring-device-state/battery-monitoring.html // and ConnectivityActionReceiver.java +// Updated based on http://stackoverflow.com/questions/20833241/android-charge-intent-has-no-extra-data +// Since the intent doesn't have the EXTRA_STATUS like the android.com article says it does +// (though it used to) public class PowerConnectionReceiver extends BroadcastReceiver { private static final String TAG = "PowerConnectionReceiver"; @Override public void onReceive(Context context, Intent intent) { - int status = intent.getIntExtra(BatteryManager.EXTRA_STATUS, -1); - boolean isCharging = status == BatteryManager.BATTERY_STATUS_CHARGING || - status == BatteryManager.BATTERY_STATUS_FULL; + final String action = intent.getAction(); - if (isCharging) { + Log.d(TAG, "charging intent: " + action); + + if (Intent.ACTION_POWER_CONNECTED.equals(action)) { Log.d(TAG, "charging, starting auto-download"); // we're plugged in, this is a great time to auto-download if everything else is // right. So, even if the user allows auto-dl on battery, let's still start diff --git a/app/src/main/java/de/danoeh/antennapod/receiver/SPAReceiver.java b/app/src/main/java/de/danoeh/antennapod/receiver/SPAReceiver.java index 359a546f6..d15108bfe 100644 --- a/app/src/main/java/de/danoeh/antennapod/receiver/SPAReceiver.java +++ b/app/src/main/java/de/danoeh/antennapod/receiver/SPAReceiver.java @@ -5,15 +5,17 @@ import android.content.Context; import android.content.Intent; import android.util.Log; import android.widget.Toast; + +import org.apache.commons.lang3.StringUtils; + +import java.util.Arrays; +import java.util.Date; + import de.danoeh.antennapod.BuildConfig; import de.danoeh.antennapod.R; import de.danoeh.antennapod.core.feed.Feed; import de.danoeh.antennapod.core.storage.DownloadRequestException; import de.danoeh.antennapod.core.storage.DownloadRequester; -import org.apache.commons.lang3.StringUtils; - -import java.util.Arrays; -import java.util.Date; /** * Receives intents from AntennaPod Single Purpose apps @@ -34,7 +36,7 @@ public class SPAReceiver extends BroadcastReceiver{ if (feedUrls != null) { if (BuildConfig.DEBUG) Log.d(TAG, "Received feeds list: " + Arrays.toString(feedUrls)); for (String url : feedUrls) { - Feed f = new Feed(url, new Date()); + Feed f = new Feed(url, new Date(0)); try { DownloadRequester.getInstance().downloadFeed(context, f); } catch (DownloadRequestException e) { diff --git a/app/src/main/res/layout/addfeed.xml b/app/src/main/res/layout/addfeed.xml index 09502eb7b..b7babbafa 100644 --- a/app/src/main/res/layout/addfeed.xml +++ b/app/src/main/res/layout/addfeed.xml @@ -1,100 +1,107 @@ <?xml version="1.0" encoding="utf-8"?> -<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" - android:layout_width="fill_parent" - android:layout_height="fill_parent" - android:orientation="vertical"> +<ScrollView + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:scrollbars="vertical"> - <ScrollView + <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" - android:layout_alignParentTop="true" - android:scrollbars="vertical"> + android:paddingTop="8dp" + android:paddingLeft="16dp" + android:paddingRight="16dp" + android:paddingBottom="8dp" + android:orientation="vertical"> - <RelativeLayout + <TextView + android:id="@+id/txtvFeedurl" android:layout_width="match_parent" - android:layout_height="wrap_content"> + android:layout_height="wrap_content" + style="@style/AntennaPod.TextView.Heading" + android:text="@string/txtvfeedurl_label"/> - <TextView - android:id="@+id/txtvFeedurl" - android:layout_width="fill_parent" - android:layout_height="wrap_content" - android:layout_alignParentTop="true" - android:layout_margin="16dp" - style="@style/AntennaPod.TextView.Heading" - android:text="@string/txtvfeedurl_label"/> + <EditText + android:id="@+id/etxtFeedurl" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="4dp" + android:hint="@string/etxtFeedurlHint" + android:inputType="textUri"/> - <EditText - android:id="@+id/etxtFeedurl" - android:layout_width="fill_parent" - android:layout_height="wrap_content" - android:layout_below="@id/txtvFeedurl" - android:layout_margin="8dp" - android:hint="@string/etxtFeedurlHint" - android:inputType="textUri"/> + <Button + android:id="@+id/butConfirm" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="8dp" + android:text="@string/confirm_label"/> - <Button - android:id="@+id/butConfirm" - android:layout_width="fill_parent" - android:layout_height="wrap_content" - android:layout_below="@id/etxtFeedurl" - android:layout_margin="8dp" - android:text="@string/confirm_label"/> + <View + android:id="@+id/divider1" + android:layout_width="match_parent" + android:layout_height="1dp" + android:layout_margin="16dp" + android:background="?android:attr/listDivider"/> - <TextView - android:id="@+id/txtvPodcastDirectories" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_below="@id/butConfirm" - android:layout_margin="8dp" - style="@style/AntennaPod.TextView.Heading" - android:text="@string/podcastdirectories_label"/> + <TextView + android:id="@+id/txtvPodcastDirectories" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_below="@id/divider1" + style="@style/AntennaPod.TextView.Heading" + android:text="@string/podcastdirectories_label"/> - <TextView - android:id="@+id/txtvPodcastDirectoriesDescr" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:text="@string/podcastdirectories_descr" - android:textSize="@dimen/text_size_medium" - android:layout_below="@id/txtvPodcastDirectories" - android:layout_margin="8dp"/> + <TextView + android:id="@+id/txtvPodcastDirectoriesDescr" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="@string/podcastdirectories_descr" + android:textSize="@dimen/text_size_medium" + android:layout_marginTop="4dp"/> - <Button - android:id="@+id/butBrowseGpoddernet" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_below="@id/txtvPodcastDirectoriesDescr" - android:layout_margin="8dp" - android:text="@string/browse_gpoddernet_label"/> + <Button + android:id="@+id/butBrowseGpoddernet" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="8dp" + android:text="@string/browse_gpoddernet_label"/> + <Button + android:id="@+id/butSearchItunes" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="8dp" + android:text="@string/search_itunes_label"/> - <TextView - android:id="@+id/txtvOpmlImport" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_below="@id/butBrowseGpoddernet" - android:layout_margin="8dp" - style="@style/AntennaPod.TextView.Heading" - android:text="@string/opml_import_label"/> + <View + android:id="@+id/divider2" + android:layout_width="match_parent" + android:layout_height="1dp" + android:layout_margin="16dp" + android:background="?android:attr/listDivider"/> + + <TextView + android:id="@+id/txtvOpmlImport" + android:layout_width="match_parent" + android:layout_height="wrap_content" + style="@style/AntennaPod.TextView.Heading" + android:text="@string/opml_import_label"/> - <TextView - android:id="@+id/txtvOpmlImportExpl" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_below="@id/txtvOpmlImport" - android:layout_margin="8dp" - android:textSize="@dimen/text_size_medium" - android:text="@string/opml_import_txtv_button_lable"/> + <TextView + android:id="@+id/txtvOpmlImportExpl" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="4dp" + android:textSize="@dimen/text_size_medium" + android:text="@string/opml_import_txtv_button_lable"/> + + <Button + android:id="@+id/butOpmlImport" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="8dp" + android:text="@string/opml_import_label"/> - <Button - android:id="@+id/butOpmlImport" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_below="@id/txtvOpmlImportExpl" - android:layout_marginBottom="8dp" - android:layout_marginLeft="8dp" - android:layout_marginRight="8dp" - android:text="@string/opml_import_label"/> - </RelativeLayout> - </ScrollView> + </LinearLayout> -</RelativeLayout>
\ No newline at end of file +</ScrollView> diff --git a/app/src/main/res/layout/cover_fragment.xml b/app/src/main/res/layout/cover_fragment.xml index 7d86346e3..18540aa1f 100644 --- a/app/src/main/res/layout/cover_fragment.xml +++ b/app/src/main/res/layout/cover_fragment.xml @@ -13,7 +13,7 @@ android:layout_height="match_parent" android:layout_gravity="center" android:adjustViewBounds="true" - android:scaleType="centerCrop" + android:scaleType="centerInside" tools:src="@android:drawable/sym_def_app_icon" /> </RelativeLayout>
\ No newline at end of file diff --git a/app/src/main/res/layout/feeditem_fragment_header.xml b/app/src/main/res/layout/feeditem_fragment_header.xml index 5956ae062..a21488306 100644 --- a/app/src/main/res/layout/feeditem_fragment_header.xml +++ b/app/src/main/res/layout/feeditem_fragment_header.xml @@ -16,7 +16,7 @@ android:layout_height="wrap_content" android:layout_gravity="center_horizontal" android:orientation="horizontal" - android:paddingBottom="8dp"> + android:paddingBottom="0dp"> <ImageView android:id="@+id/imgvCover" @@ -59,6 +59,29 @@ android:maxLines="5" tools:text="Podcast title" tools:background="@android:color/holo_green_dark" /> + + <TextView + android:id="@+id/txtvDuration" + style="@style/AntennaPod.TextView.ListItemSecondaryTitle" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_toRightOf="@id/imgvCover" + android:layout_below="@id/txtvTitle" + android:layout_marginLeft="16dp" + tools:text="00:42:23" + tools:background="@android:color/holo_green_dark"/> + + <TextView + android:id="@+id/txtvPublished" + style="@style/AntennaPod.TextView.ListItemSecondaryTitle" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_toLeftOf="@id/butMoreActions" + android:layout_marginRight="8dp" + tools:text="Jan 23" + tools:background="@android:color/holo_green_dark" + android:layout_below="@+id/txtvTitle"/> + </RelativeLayout> <ProgressBar diff --git a/app/src/main/res/layout/fragment_itunes_search.xml b/app/src/main/res/layout/fragment_itunes_search.xml new file mode 100644 index 000000000..17ffe349b --- /dev/null +++ b/app/src/main/res/layout/fragment_itunes_search.xml @@ -0,0 +1,26 @@ +<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" +xmlns:tools="http://schemas.android.com/tools" +android:layout_width="match_parent" +android:layout_height="match_parent" +tools:context="de.danoeh.antennapod.activity.ITunesSearchActivity"> +<android.support.v7.widget.SearchView + android:id="@+id/itunes_search_view" + android:layout_height="wrap_content" + android:layout_width="match_parent" + /> +<GridView + android:id="@+id/gridView" + android:layout_below="@id/itunes_search_view" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:clipToPadding="false" + android:columnWidth="200dp" + android:gravity="center" + android:horizontalSpacing="8dp" + android:numColumns="auto_fit" + android:paddingBottom="@dimen/list_vertical_padding" + android:paddingTop="@dimen/list_vertical_padding" + android:stretchMode="columnWidth" + android:verticalSpacing="8dp" + tools:listitem="@layout/gpodnet_podcast_listitem" /> +</RelativeLayout> diff --git a/app/src/main/res/layout/gpodnet_podcast_listitem.xml b/app/src/main/res/layout/gpodnet_podcast_listitem.xml index 2ade8e478..84c6c280e 100644 --- a/app/src/main/res/layout/gpodnet_podcast_listitem.xml +++ b/app/src/main/res/layout/gpodnet_podcast_listitem.xml @@ -23,16 +23,60 @@ tools:src="@drawable/ic_stat_antenna_default" tools:background="@android:color/holo_green_dark" /> + <LinearLayout + android:id="@+id/subscribers_container" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignTop="@id/txtvTitle" + android:layout_alignParentRight="true" + android:layout_marginRight="@dimen/listitem_threeline_horizontalpadding" + android:orientation="horizontal"> + + <ImageView + android:id="@+id/imgFeed" + android:layout_width="wrap_content" + android:layout_height="match_parent" + android:layout_marginRight="-4dp" + android:src="?attr/feed" /> + + <TextView + android:id="@+id/txtvSubscribers" + style="@style/AntennaPod.TextView.ListItemSecondaryTitle" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:lines="1" + tools:text="150" + tools:background="@android:color/holo_green_dark" /> + + </LinearLayout> + <TextView android:id="@+id/txtvTitle" style="@style/AntennaPod.TextView.ListItemPrimaryTitle" - android:layout_width="match_parent" + android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_centerVertical="true" - android:layout_marginBottom="@dimen/listitem_threeline_verticalpadding" + android:layout_marginBottom="@dimen/list_vertical_padding" android:layout_marginRight="@dimen/listitem_threeline_horizontalpadding" android:layout_toRightOf="@id/imgvCover" - android:maxLines="1" - tools:text="Podcast title" + android:layout_toLeftOf="@id/subscribers_container" + android:layout_alignTop="@id/imgvCover" + android:lines="1" + tools:text="Title" tools:background="@android:color/holo_green_dark" /> + + <TextView + android:id="@+id/txtvUrl" + style="android:style/TextAppearance.Small" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginRight="@dimen/listitem_threeline_horizontalpadding" + android:layout_toRightOf="@id/imgvCover" + android:layout_below="@id/txtvTitle" + android:textSize="14sp" + android:textColor="?android:attr/textColorSecondary" + android:ellipsize="middle" + android:maxLines="2" + tools:text="http://www.example.com/feed" + tools:background="@android:color/holo_green_dark"/> + </RelativeLayout> diff --git a/app/src/main/res/layout/gpodnet_tag_listitem.xml b/app/src/main/res/layout/gpodnet_tag_listitem.xml new file mode 100644 index 000000000..9e545e59d --- /dev/null +++ b/app/src/main/res/layout/gpodnet_tag_listitem.xml @@ -0,0 +1,34 @@ +<?xml version="1.0" encoding="utf-8"?> + +<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:gravity="center_vertical" + tools:background="@android:color/darker_gray"> + + <TextView + android:id="@+id/txtvTitle" + style="@style/AntennaPod.TextView.ListItemPrimaryTitle" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginLeft="@dimen/listitem_threeline_horizontalpadding" + android:layout_marginTop="@dimen/listitem_threeline_verticalpadding" + android:layout_marginBottom="@dimen/listitem_threeline_verticalpadding" + android:lines="1" + tools:text="Tag Title" + tools:background="@android:color/holo_green_dark" /> + + <TextView + android:id="@+id/txtvUsage" + style="@style/AntennaPod.TextView.ListItemSecondaryTitle" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentRight="true" + android:layout_marginRight="@dimen/listitem_threeline_horizontalpadding" + android:layout_marginTop="@dimen/listitem_threeline_verticalpadding" + android:layout_marginBottom="@dimen/listitem_threeline_verticalpadding" + tools:text="301" + tools:background="@android:color/holo_green_dark"/> + +</RelativeLayout> diff --git a/app/src/main/res/layout/itunes_podcast_listitem.xml b/app/src/main/res/layout/itunes_podcast_listitem.xml new file mode 100644 index 000000000..41b1f495f --- /dev/null +++ b/app/src/main/res/layout/itunes_podcast_listitem.xml @@ -0,0 +1,38 @@ +<?xml version="1.0" encoding="utf-8"?> + +<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" +xmlns:tools="http://schemas.android.com/tools" +android:layout_width="match_parent" +android:layout_height="@dimen/listitem_threeline_height" +tools:background="@android:color/darker_gray"> + +<ImageView + android:id="@+id/imgvCover" + android:layout_width="@dimen/thumbnail_length_itemlist" + android:layout_height="@dimen/thumbnail_length_itemlist" + android:layout_alignParentLeft="true" + android:layout_centerVertical="true" + android:layout_marginBottom="@dimen/listitem_threeline_verticalpadding" + android:layout_marginLeft="@dimen/listitem_threeline_horizontalpadding" + android:layout_marginRight="8dp" + android:layout_marginTop="@dimen/listitem_threeline_verticalpadding" + android:adjustViewBounds="true" + android:contentDescription="@string/cover_label" + android:cropToPadding="true" + android:scaleType="fitXY" + tools:src="@drawable/ic_stat_antenna_default" + tools:background="@android:color/holo_green_dark" /> + +<TextView + android:id="@+id/txtvTitle" + style="@style/AntennaPod.TextView.ListItemPrimaryTitle" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_centerVertical="true" + android:layout_marginBottom="@dimen/listitem_threeline_verticalpadding" + android:layout_marginRight="@dimen/listitem_threeline_horizontalpadding" + android:layout_toRightOf="@id/imgvCover" + android:maxLines="1" + tools:text="Podcast title" + tools:background="@android:color/holo_green_dark" /> +</RelativeLayout> diff --git a/app/src/main/res/layout/new_episodes_fragment.xml b/app/src/main/res/layout/new_episodes_fragment.xml index 19db02f1d..e90171630 100644 --- a/app/src/main/res/layout/new_episodes_fragment.xml +++ b/app/src/main/res/layout/new_episodes_fragment.xml @@ -16,7 +16,8 @@ android:paddingBottom="@dimen/list_vertical_padding" android:clipToPadding="false" dslv:collapsed_height="2dp" - dslv:drag_enabled="false" + dslv:drag_enabled="true" + dslv:drag_handle_id="@id/drag_handle" dslv:drag_scroll_start="0.33" dslv:float_alpha="0.6" dslv:max_drag_scroll_speed="0.5" @@ -49,4 +50,18 @@ tools:layout_height="64dp" tools:background="@android:color/holo_red_light"/> + <LinearLayout + android:id="@+id/undobar" + style="@style/UndoBar"> + + <TextView + android:id="@+id/undobar_message" + style="@style/UndoBarMessage"/> + + <Button + android:id="@+id/undobar_button" + style="@style/UndoBarButton"/> + + </LinearLayout> + </FrameLayout>
\ No newline at end of file diff --git a/app/src/main/res/layout/opml_import.xml b/app/src/main/res/layout/opml_import.xml index 3e45a0400..5ece4f09f 100644 --- a/app/src/main/res/layout/opml_import.xml +++ b/app/src/main/res/layout/opml_import.xml @@ -1,32 +1,92 @@ <?xml version="1.0" encoding="utf-8"?> -<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" + android:paddingTop="8dp" + android:paddingLeft="16dp" + android:paddingRight="16dp" + android:paddingBottom="8dp" tools:background="@android:color/darker_gray"> <TextView + android:id="@+id/txtvHeadingExplanation1" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_margin="8dp" - android:text="@string/opml_import_explanation" + style="@style/AntennaPod.TextView.Heading" + android:text="@string/txtvfeedurl_label"/> + + <TextView + android:id="@+id/txtvExplanation1" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="@string/opml_import_explanation_1" + android:textSize="@dimen/text_size_medium" + android:layout_marginTop="4dp" tools:background="@android:color/holo_green_dark" /> + <Button + android:id="@+id/butChooseFileFromFilesystem" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_gravity="center_horizontal" + android:layout_marginTop="8dp" + android:text="@string/choose_file_from_filesystem" /> + + <View + android:id="@+id/divider1" + android:layout_width="fill_parent" + android:layout_height="1dp" + android:layout_margin="16dp" + android:background="?android:attr/listDivider"/> + <TextView - android:id="@+id/txtvPath" + android:id="@+id/txtvHeadingExplanation2" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_margin="8dp" - tools:text="Path" + style="@style/AntennaPod.TextView.Heading" + android:text="@string/txtvfeedurl_label"/> + + <TextView + android:id="@+id/txtvExplanation2" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="@string/opml_import_explanation_2" + android:textSize="@dimen/text_size_medium" + android:layout_marginTop="4dp" tools:background="@android:color/holo_green_dark" /> <Button - android:id="@+id/butStartImport" - android:layout_width="wrap_content" + android:id="@+id/butChooseFileFromExternal" + android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_gravity="center_horizontal" - android:layout_margin="8dp" - android:text="@string/start_import_label" /> + android:layout_marginTop="8dp" + android:text="@string/choose_file_from_external_application" /> + + <View + android:id="@+id/divider2" + android:layout_width="fill_parent" + android:layout_height="1dp" + android:layout_margin="16dp" + android:background="?android:attr/listDivider"/> + + <TextView + android:id="@+id/txtvHeadingExplanation3" + android:layout_width="match_parent" + android:layout_height="wrap_content" + style="@style/AntennaPod.TextView.Heading" + android:text="@string/txtvfeedurl_label"/> + + <TextView + android:id="@+id/txtvExplanation3" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="@string/opml_import_explanation_3" + android:textSize="@dimen/text_size_medium" + android:layout_marginTop="4dp" + tools:background="@android:color/holo_green_dark" /> </LinearLayout>
\ No newline at end of file diff --git a/app/src/main/res/layout/queue_fragment.xml b/app/src/main/res/layout/queue_fragment.xml index d184eb28d..307d95a8d 100644 --- a/app/src/main/res/layout/queue_fragment.xml +++ b/app/src/main/res/layout/queue_fragment.xml @@ -20,7 +20,8 @@ dslv:float_alpha="0.6" dslv:float_background_color="?attr/dragview_float_background" dslv:max_drag_scroll_speed="0.5" - dslv:remove_enabled="false" + dslv:remove_enabled="true" + dslv:remove_mode="flingRemove" dslv:slide_shuffle_speed="0.3" dslv:sort_enabled="true" dslv:track_drag_sort="true" @@ -42,4 +43,18 @@ android:indeterminateOnly="true" android:visibility="gone" /> -</FrameLayout>
\ No newline at end of file + <LinearLayout + android:id="@+id/undobar" + style="@style/UndoBar"> + + <TextView + android:id="@+id/undobar_message" + style="@style/UndoBarMessage"/> + + <Button + android:id="@+id/undobar_button" + style="@style/UndoBarButton"/> + + </LinearLayout> + +</FrameLayout> diff --git a/app/src/main/res/layout/queue_listitem.xml b/app/src/main/res/layout/queue_listitem.xml index 74c6ed785..bc5b951a2 100644 --- a/app/src/main/res/layout/queue_listitem.xml +++ b/app/src/main/res/layout/queue_listitem.xml @@ -9,11 +9,12 @@ <ImageView android:id="@+id/drag_handle" - android:layout_width="24dp" + android:layout_width="100dp" android:layout_height="match_parent" - android:layout_margin="8dp" + android:layout_marginLeft="8dp" + android:layout_marginRight="-64dp" android:contentDescription="@string/drag_handle_content_description" - android:scaleType="center" + android:scaleType="fitXY" android:src="?attr/dragview_background" tools:src="@drawable/ic_drag_handle" tools:background="@android:color/holo_green_dark" /> @@ -32,7 +33,7 @@ <RelativeLayout android:layout_width="0dp" - android:layout_height="match_parent" + android:layout_height="wrap_content" android:layout_marginBottom="@dimen/listitem_threeline_verticalpadding" android:layout_marginLeft="@dimen/listitem_threeline_textleftpadding" android:layout_marginRight="@dimen/listitem_threeline_textrightpadding" @@ -40,46 +41,72 @@ android:layout_weight="1" tools:background="@android:color/holo_red_dark"> + <!-- order is important, pubDate first! --> + <TextView + android:id="@+id/txtvPubDate" + style="@style/AntennaPod.TextView.ListItemSecondaryTitle" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:lines="2" + android:layout_alignParentRight="true" + android:layout_alignParentTop="true" + android:layout_marginLeft="8dp" + android:gravity="right|bottom" + android:text="Feb\n12" + tools:background="@android:color/holo_blue_light" /> + <TextView android:id="@+id/txtvTitle" style="@style/AntennaPod.TextView.ListItemPrimaryTitle" - android:layout_width="0dp" + android:layout_width="wrap_content" android:layout_height="wrap_content" + android:layout_toLeftOf="@id/txtvPubDate" android:layout_alignParentLeft="true" - android:layout_alignParentRight="true" android:layout_alignParentTop="true" android:text="Queue item title" + android:ellipsize="end" tools:background="@android:color/holo_blue_light" /> <RelativeLayout android:id="@+id/bottom_bar" android:layout_width="0dp" android:layout_height="wrap_content" + android:layout_below="@id/txtvTitle" android:layout_alignParentBottom="true" android:layout_alignParentLeft="true" - android:layout_alignParentRight="true" - android:layout_marginTop="16dp"> + android:layout_alignParentRight="true"> <TextView - android:id="@+id/txtvPosition" + android:id="@+id/txtvProgressLeft" style="@style/AntennaPod.TextView.ListItemSecondaryTitle" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentLeft="true" + android:layout_marginBottom="0dp" android:text="00:42:23" - tools:background="@android:color/holo_blue_light" /> + tools:background="@android:color/holo_blue_light"/> + + <TextView + android:id="@+id/txtvProgressRight" + style="@style/AntennaPod.TextView.ListItemSecondaryTitle" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentRight="true" + android:layout_marginBottom="0dp" + tools:text="Jan 23" + tools:background="@android:color/holo_green_dark" /> <ProgressBar - android:id="@+id/pbar_download_progress" + android:id="@+id/progressBar" style="?android:attr/progressBarStyleHorizontal" - android:layout_width="0dp" + android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_alignParentRight="true" - android:layout_marginLeft="8dp" - android:layout_toRightOf="@id/txtvPosition" + android:layout_below="@id/txtvProgressLeft" + android:layout_marginTop="-2dp" android:max="100" tools:background="@android:color/holo_blue_light" /> + </RelativeLayout> </RelativeLayout> diff --git a/app/src/main/res/menu/feedlist.xml b/app/src/main/res/menu/feedlist.xml index 8d2d9e367..b6512e828 100644 --- a/app/src/main/res/menu/feedlist.xml +++ b/app/src/main/res/menu/feedlist.xml @@ -7,7 +7,7 @@ android:icon="?attr/navigation_refresh" android:menuCategory="container" android:title="@string/refresh_label" - custom:showAsAction="ifRoom"> + custom:showAsAction="always"> </item> <item android:id="@+id/refresh_complete_item" diff --git a/app/src/main/res/menu/new_episodes.xml b/app/src/main/res/menu/new_episodes.xml index d74e70b3b..72661a17e 100644 --- a/app/src/main/res/menu/new_episodes.xml +++ b/app/src/main/res/menu/new_episodes.xml @@ -7,7 +7,7 @@ android:id="@+id/refresh_item" android:title="@string/refresh_label" android:menuCategory="container" - custom:showAsAction="ifRoom" + custom:showAsAction="always" android:icon="?attr/navigation_refresh"/> <item diff --git a/app/src/main/res/menu/queue.xml b/app/src/main/res/menu/queue.xml index b85279e5a..c7dd4d371 100644 --- a/app/src/main/res/menu/queue.xml +++ b/app/src/main/res/menu/queue.xml @@ -7,10 +7,17 @@ android:id="@+id/refresh_item" android:title="@string/refresh_label" android:menuCategory="container" - custom:showAsAction="ifRoom" + custom:showAsAction="always" android:icon="?attr/navigation_refresh"/> <item + android:id="@+id/clear_queue" + android:title="Clear Queue" + android:menuCategory="container" + custom:showAsAction="collapseActionView" + android:icon="?attr/navigation_accept"/> + + <item android:id="@+id/queue_sort" android:title="@string/sort"> diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index 5169eac5a..6d14349d5 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -22,6 +22,17 @@ android:summary="@string/pref_persistNotify_sum" android:title="@string/pref_persistNotify_title"/> </PreferenceCategory> + + <PreferenceCategory android:title="@string/queue_label"> + <CheckBoxPreference + android:defaultValue="false" + android:enabled="true" + android:key="prefQueueAddToFront" + android:summary="@string/pref_queueAddToFront_sum" + android:title="@string/pref_queueAddToFront_title"/> + /> + </PreferenceCategory> + <PreferenceCategory android:title="@string/playback_pref"> <CheckBoxPreference android:defaultValue="true" @@ -77,13 +88,17 @@ android:key="prefAutoUpdateIntervall" android:summary="@string/pref_autoUpdateIntervall_sum" android:title="@string/pref_autoUpdateIntervall_title"/> - <CheckBoxPreference android:defaultValue="false" android:enabled="true" android:key="prefMobileUpdate" android:summary="@string/pref_mobileUpdate_sum" android:title="@string/pref_mobileUpdate_title"/> + <de.danoeh.antennapod.preferences.CustomEditTextPreference + android:defaultValue="6" + android:inputType="number" + android:key="prefParallelDownloads" + android:title="@string/pref_parallel_downloads_title"/> <ListPreference android:defaultValue="20" android:entries="@array/episode_cache_size_entries" @@ -111,6 +126,7 @@ </PreferenceScreen> </PreferenceCategory> + <PreferenceCategory android:title="@string/services_label"> <PreferenceScreen android:key="prefFlattrSettings" diff --git a/core/build.gradle b/core/build.gradle index dfe0fb133..710378a18 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -44,4 +44,5 @@ 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' -}
\ No newline at end of file + compile 'com.nineoldandroids:library:2.4.0' +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/EventDistributor.java b/core/src/main/java/de/danoeh/antennapod/core/feed/EventDistributor.java index f8815dcf0..5a2cfa40e 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/feed/EventDistributor.java +++ b/core/src/main/java/de/danoeh/antennapod/core/feed/EventDistributor.java @@ -31,6 +31,7 @@ public class EventDistributor extends Observable { public static final int PLAYBACK_HISTORY_UPDATE = 16; public static final int DOWNLOAD_QUEUED = 32; public static final int DOWNLOAD_HANDLED = 64; + public static final int PLAYER_STATUS_UPDATE = 128; private Handler handler; private AbstractQueue<Integer> events; @@ -124,6 +125,10 @@ public class EventDistributor extends Observable { addEvent(DOWNLOAD_HANDLED); } + public void sendPlayerStatusUpdateBroadcast() { + addEvent(PLAYER_STATUS_UPDATE); + } + public static abstract class EventListener implements Observer { @Override diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedImage.java b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedImage.java index b01747f7f..c6f24367e 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedImage.java +++ b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedImage.java @@ -2,10 +2,10 @@ package de.danoeh.antennapod.core.feed; import android.net.Uri; -import de.danoeh.antennapod.core.asynctask.PicassoImageResource; - import java.io.File; +import de.danoeh.antennapod.core.asynctask.PicassoImageResource; + public class FeedImage extends FeedFile implements PicassoImageResource { public static final int FEEDFILETYPE_FEEDIMAGE = 1; @@ -65,6 +65,8 @@ public class FeedImage extends FeedFile implements PicassoImageResource { public Uri getImageUri() { if (file_url != null && downloaded) { return Uri.fromFile(new File(file_url)); + } else if(download_url != null) { + return Uri.parse(download_url); } else { return null; } diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedItem.java b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedItem.java index c63b61f55..5a4d869e7 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedItem.java +++ b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedItem.java @@ -135,8 +135,8 @@ public class FeedItem extends FeedComponent implements ShownotesProvider, Flattr if (other.media != null) { if (media == null) { setMedia(other.media); - } else if (media.compareWithOther(other)) { - media.updateFromOther(other); + } else if (media.compareWithOther(other.media)) { + media.updateFromOther(other.media); } } if (other.paymentLink != null) { diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedMedia.java b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedMedia.java index 2434ee0cf..69e96c503 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedMedia.java +++ b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedMedia.java @@ -127,6 +127,25 @@ public class FeedMedia extends FeedFile implements Playable { && PlaybackPreferences.getCurrentlyPlayingFeedMediaId() == id; } + /** + * Reads playback preferences to determine whether this FeedMedia object is + * currently being played and the current player status is playing. + */ + public boolean isCurrentlyPlaying() { + return isPlaying() && + ((PlaybackPreferences.getCurrentPlayerStatus() == PlaybackPreferences.PLAYER_STATUS_PLAYING)); + } + + /** + * Reads playback preferences to determine whether this FeedMedia object is + * currently being played and the current player status is paused. + */ + public boolean isCurrentlyPaused() { + return isPlaying() && + ((PlaybackPreferences.getCurrentPlayerStatus() == PlaybackPreferences.PLAYER_STATUS_PAUSED)); + } + + @Override public int getTypeAsInt() { return FEEDFILETYPE_FEEDMEDIA; diff --git a/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/GpodnetService.java b/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/GpodnetService.java index 5ee40186f..a353c984a 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/GpodnetService.java +++ b/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/GpodnetService.java @@ -1,5 +1,8 @@ package de.danoeh.antennapod.core.gpoddernet; +import android.os.Build; +import android.util.Log; + import com.squareup.okhttp.Credentials; import com.squareup.okhttp.MediaType; import com.squareup.okhttp.OkHttpClient; @@ -18,16 +21,27 @@ import org.json.JSONObject; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; -import java.net.CookieManager; -import java.net.CookiePolicy; import java.net.MalformedURLException; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; +import java.security.KeyStore; +import java.security.Principal; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Collection; +import java.util.HashMap; import java.util.LinkedList; import java.util.List; +import java.util.Map; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509TrustManager; +import javax.security.auth.x500.X500Principal; import de.danoeh.antennapod.core.ClientConfig; import de.danoeh.antennapod.core.gpoddernet.model.GpodnetDevice; @@ -43,6 +57,8 @@ import de.danoeh.antennapod.core.service.download.AntennapodHttpClient; */ public class GpodnetService { + private static final String TAG = "GpodnetService"; + private static final String BASE_SCHEME = "https"; public static final String DEFAULT_BASE_HOST = "gpodder.net"; @@ -56,9 +72,84 @@ public class GpodnetService { public GpodnetService() { httpClient = AntennapodHttpClient.getHttpClient(); + if (Build.VERSION.SDK_INT <= 10) { + Log.d(TAG, "Use custom SSL factory"); + SSLSocketFactory factory = getCustomSslSocketFactory(); + httpClient.setSslSocketFactory(factory); + } BASE_HOST = GpodnetPreferences.getHostname(); } + private synchronized static SSLSocketFactory getCustomSslSocketFactory() { + try { + TrustManagerFactory defaultTrustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + defaultTrustManagerFactory.init((KeyStore) null); // use system keystore + final X509TrustManager defaultTrustManager = (X509TrustManager) defaultTrustManagerFactory.getTrustManagers()[0]; + TrustManager[] customTrustManagers = new TrustManager[]{new X509TrustManager() { + @Override + public java.security.cert.X509Certificate[] getAcceptedIssuers() { + return null; + } + @Override + public void checkClientTrusted(java.security.cert.X509Certificate[] chain, String authType) throws CertificateException { + } + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) + throws CertificateException { + // chain may out of order - construct data structures to walk from server certificate to root certificate + Map<Principal, X509Certificate> certificates = new HashMap<Principal, X509Certificate>(chain.length - 1); + X509Certificate subject = null; + for (X509Certificate cert : chain) { + cert.checkValidity(); + if (cert.getSubjectDN().toString().startsWith("CN=" + DEFAULT_BASE_HOST)) { + subject = cert; + } else { + certificates.put(cert.getSubjectDN(), cert); + } + } + if (subject == null) { + throw new CertificateException("Chain does not contain a certificate for " + DEFAULT_BASE_HOST); + } + // follow chain to root CA + while (certificates.get(subject.getIssuerDN()) != null) { + subject.checkValidity(); + X509Certificate issuer = certificates.get(subject.getIssuerDN()); + try { + subject.verify(issuer.getPublicKey()); + } catch (Exception e) { + Log.d(TAG, "failed: " + issuer.getSubjectDN() + " -> " + subject.getSubjectDN()); + throw new CertificateException("Could not verify certificate"); + } + subject = issuer; + } + X500Principal rootAuthority = subject.getIssuerX500Principal(); + boolean accepted = false; + for (X509Certificate cert : + defaultTrustManager.getAcceptedIssuers()) { + if (cert.getSubjectX500Principal().equals(rootAuthority)) { + try { + subject.verify(cert.getPublicKey()); + accepted = true; + } catch (Exception e) { + Log.d(TAG, "failed: " + cert.getSubjectDN() + " -> " + subject.getSubjectDN()); + throw new CertificateException("Could not verify root certificate"); + } + } + } + if (accepted == false) { + throw new CertificateException("Could not verify root certificate"); + } + } + }}; + SSLContext sslContext = SSLContext.getInstance("TLS"); + sslContext.init(null, customTrustManagers, null); + return sslContext.getSocketFactory(); + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + /** * Returns the [count] most used tags. */ @@ -81,9 +172,10 @@ public class GpodnetService { jsonTagList.length()); for (int i = 0; i < jsonTagList.length(); i++) { JSONObject jObj = jsonTagList.getJSONObject(i); - String name = jObj.getString("tag"); + String title = jObj.getString("title"); + String tag = jObj.getString("tag"); int usage = jObj.getInt("usage"); - tagList.add(new GpodnetTag(name, usage)); + tagList.add(new GpodnetTag(title, tag, usage)); } return tagList; } catch (JSONException e) { @@ -103,7 +195,7 @@ public class GpodnetService { try { URL url = new URI(BASE_SCHEME, BASE_HOST, String.format( - "/api/2/tag/%s/%d.json", tag.getName(), count), null).toURL(); + "/api/2/tag/%s/%d.json", tag.getTag(), count), null).toURL(); Request.Builder request = new Request.Builder().url(url); String response = executeRequest(request); diff --git a/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetTag.java b/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetTag.java index 7178f4be5..cd865731b 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetTag.java +++ b/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetTag.java @@ -1,46 +1,60 @@ package de.danoeh.antennapod.core.gpoddernet.model; -import org.apache.commons.lang3.Validate; +import android.os.Parcel; +import android.os.Parcelable; -import java.util.Comparator; +import org.apache.commons.lang3.Validate; -public class GpodnetTag { +public class GpodnetTag implements Parcelable { - private String name; - private int usage; + private final String title; + private final String tag; + private final int usage; - public GpodnetTag(String name, int usage) { - Validate.notNull(name); + public GpodnetTag(String title, String tag, int usage) { + Validate.notNull(title); + Validate.notNull(tag); - this.name = name; + this.title = title; + this.tag = tag; this.usage = usage; } - public GpodnetTag(String name) { - super(); - this.name = name; + public static GpodnetTag createFromParcel(Parcel in) { + final String title = in.readString(); + final String tag = in.readString(); + final int usage = in.readInt(); + return new GpodnetTag(title, tag, usage); } @Override public String toString() { - return "GpodnetTag [name=" + name + ", usage=" + usage + "]"; + return "GpodnetTag [title="+title+", tag=" + tag + ", usage=" + usage + "]"; } - public String getName() { - return name; + public String getTitle() { + return title; + } + + public String getTag() { + return tag; } public int getUsage() { return usage; } - public static class UsageComparator implements Comparator<GpodnetTag> { - - @Override - public int compare(GpodnetTag o1, GpodnetTag o2) { - return o1.usage - o2.usage; - } + @Override + public int describeContents() { + return 0; + } + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(title); + dest.writeString(tag); + dest.writeInt(usage); } + } diff --git a/core/src/main/java/de/danoeh/antennapod/core/preferences/PlaybackPreferences.java b/core/src/main/java/de/danoeh/antennapod/core/preferences/PlaybackPreferences.java index d88543f73..714f1b051 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/preferences/PlaybackPreferences.java +++ b/core/src/main/java/de/danoeh/antennapod/core/preferences/PlaybackPreferences.java @@ -8,6 +8,7 @@ import android.util.Log; import org.apache.commons.lang3.Validate; import de.danoeh.antennapod.core.BuildConfig; +import de.danoeh.antennapod.core.feed.EventDistributor; /** * Provides access to preferences set by the playback service. A private @@ -43,14 +44,27 @@ public class PlaybackPreferences implements /** True if last played media was a video. */ public static final String PREF_CURRENT_EPISODE_IS_VIDEO = "de.danoeh.antennapod.preferences.lastIsVideo"; + /** The current player status as int. */ + public static final String PREF_CURRENT_PLAYER_STATUS = "de.danoeh.antennapod.preferences.currentPlayerStatus"; + /** Value of PREF_CURRENTLY_PLAYING_MEDIA if no media is playing. */ public static final long NO_MEDIA_PLAYING = -1; + /** Value of PREF_CURRENT_PLAYER_STATUS if media player status is playing. */ + public static final int PLAYER_STATUS_PLAYING = 1; + + /** Value of PREF_CURRENT_PLAYER_STATUS if media player status is paused. */ + public static final int PLAYER_STATUS_PAUSED = 2; + + /** Value of PREF_CURRENT_PLAYER_STATUS if media player status is neither playing nor paused. */ + public static final int PLAYER_STATUS_OTHER = 3; + private long currentlyPlayingFeedId; private long currentlyPlayingFeedMediaId; private long currentlyPlayingMedia; private boolean currentEpisodeIsStream; private boolean currentEpisodeIsVideo; + private int currentPlayerStatus; private static PlaybackPreferences instance; private Context context; @@ -87,6 +101,8 @@ public class PlaybackPreferences implements NO_MEDIA_PLAYING); currentEpisodeIsStream = sp.getBoolean(PREF_CURRENT_EPISODE_IS_STREAM, true); currentEpisodeIsVideo = sp.getBoolean(PREF_CURRENT_EPISODE_IS_VIDEO, false); + currentPlayerStatus = sp.getInt(PREF_CURRENT_PLAYER_STATUS, + PLAYER_STATUS_OTHER); } @Override @@ -109,6 +125,11 @@ public class PlaybackPreferences implements currentlyPlayingFeedMediaId = sp.getLong( PREF_CURRENTLY_PLAYING_FEEDMEDIA_ID, NO_MEDIA_PLAYING); } + else if (key.equals(PREF_CURRENT_PLAYER_STATUS)) { + currentPlayerStatus = sp.getInt(PREF_CURRENT_PLAYER_STATUS, + PLAYER_STATUS_OTHER); + EventDistributor.getInstance().sendPlayerStatusUpdateBroadcast(); + } } private static void instanceAvailable() { @@ -143,4 +164,10 @@ public class PlaybackPreferences implements return instance.currentEpisodeIsVideo; } + public static int getCurrentPlayerStatus() { + instanceAvailable(); + return instance.currentPlayerStatus; + } + + } diff --git a/core/src/main/java/de/danoeh/antennapod/core/preferences/UserPreferences.java b/core/src/main/java/de/danoeh/antennapod/core/preferences/UserPreferences.java index a3b9f6049..6cb2faba5 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/preferences/UserPreferences.java +++ b/core/src/main/java/de/danoeh/antennapod/core/preferences/UserPreferences.java @@ -20,7 +20,6 @@ import java.util.LinkedList; import java.util.List; import java.util.concurrent.TimeUnit; -import de.danoeh.antennapod.core.ApplicationCallbacks; import de.danoeh.antennapod.core.BuildConfig; import de.danoeh.antennapod.core.ClientConfig; import de.danoeh.antennapod.core.R; @@ -42,6 +41,7 @@ public class UserPreferences implements public static final String PREF_FOLLOW_QUEUE = "prefFollowQueue"; public static final String PREF_DOWNLOAD_MEDIA_ON_WIFI_ONLY = "prefDownloadMediaOnWifiOnly"; public static final String PREF_UPDATE_INTERVAL = "prefAutoUpdateIntervall"; + public static final String PREF_PARALLEL_DOWNLOADS = "prefParallelDownloads"; 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"; @@ -60,6 +60,7 @@ public class UserPreferences implements private static final String PREF_SEEK_DELTA_SECS = "prefSeekDeltaSecs"; private static final String PREF_EXPANDED_NOTIFICATION = "prefExpandNotify"; private static final String PREF_PERSISTENT_NOTIFICATION = "prefPersistNotify"; + public static final String PREF_QUEUE_ADD_TO_FRONT = "prefQueueAddToFront"; // TODO: Make this value configurable private static final float PREF_AUTO_FLATTR_PLAYED_DURATION_THRESHOLD_DEFAULT = 0.8f; @@ -85,6 +86,7 @@ public class UserPreferences implements private boolean enableAutodownloadWifiFilter; private boolean enableAutodownloadOnBattery; private String[] autodownloadSelectedNetworks; + private int parallelDownloads; private int episodeCacheSize; private String playbackSpeed; private String[] playbackSpeedArray; @@ -143,6 +145,7 @@ public class UserPreferences implements PREF_ENABLE_AUTODL_WIFI_FILTER, false); autodownloadSelectedNetworks = StringUtils.split( sp.getString(PREF_AUTODL_SELECTED_NETWORKS, ""), ','); + parallelDownloads = Integer.valueOf(sp.getString(PREF_PARALLEL_DOWNLOADS, "6")); episodeCacheSize = readEpisodeCacheSizeInternal(sp.getString( PREF_EPISODE_CACHE_SIZE, "20")); enableAutodownload = sp.getBoolean(PREF_ENABLE_AUTODL, false); @@ -313,6 +316,11 @@ public class UserPreferences implements return instance.autodownloadSelectedNetworks; } + public static int getParallelDownloads() { + instanceAvailable(); + return instance.parallelDownloads; + } + public static int getEpisodeCacheSizeUnlimited() { return EPISODE_CACHE_SIZE_UNLIMITED; } @@ -398,6 +406,8 @@ public class UserPreferences implements } else if (key.equals(PREF_AUTODL_SELECTED_NETWORKS)) { autodownloadSelectedNetworks = StringUtils.split( sp.getString(PREF_AUTODL_SELECTED_NETWORKS, ""), ','); + } else if(key.equals(PREF_PARALLEL_DOWNLOADS)) { + parallelDownloads = Integer.valueOf(sp.getString(PREF_PARALLEL_DOWNLOADS, "6")); } else if (key.equals(PREF_EPISODE_CACHE_SIZE)) { episodeCacheSize = readEpisodeCacheSizeInternal(sp.getString( PREF_EPISODE_CACHE_SIZE, "20")); diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/GpodnetSyncService.java b/core/src/main/java/de/danoeh/antennapod/core/service/GpodnetSyncService.java index 0f2a81dfb..fed0d3bc8 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/GpodnetSyncService.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/GpodnetSyncService.java @@ -140,7 +140,7 @@ public class GpodnetSyncService extends Service { private synchronized void processSubscriptionChanges(List<String> localSubscriptions, GpodnetSubscriptionChange changes) throws DownloadRequestException { for (String downloadUrl : changes.getAdded()) { if (!localSubscriptions.contains(downloadUrl)) { - Feed feed = new Feed(downloadUrl, new Date()); + Feed feed = new Feed(downloadUrl, new Date(0)); DownloadRequester.getInstance().downloadFeed(this, feed); } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadRequest.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadRequest.java index 75d6570b2..41bbd5ba6 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadRequest.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadRequest.java @@ -6,6 +6,9 @@ import android.os.Parcelable; import org.apache.commons.lang3.Validate; +import de.danoeh.antennapod.core.feed.FeedFile; +import de.danoeh.antennapod.core.util.URLChecker; + public class DownloadRequest implements Parcelable { private final String destination; @@ -13,6 +16,7 @@ public class DownloadRequest implements Parcelable { private final String title; private String username; private String password; + private long ifModifiedSince; private boolean deleteOnFailure; private final long feedfileId; private final int feedfileType; @@ -45,12 +49,26 @@ public class DownloadRequest implements Parcelable { this(destination, source, title, feedfileId, feedfileType, null, null, true, null); } + public DownloadRequest(Builder builder) { + this.destination = builder.destination; + this.source = builder.source; + this.title = builder.title; + this.feedfileId = builder.feedfileId; + this.feedfileType = builder.feedfileType; + this.username = builder.username; + this.password = builder.password; + this.ifModifiedSince = builder.ifModifiedSince; + this.deleteOnFailure = builder.deleteOnFailure; + this.arguments = (builder.arguments != null) ? builder.arguments : new Bundle(); + } + private DownloadRequest(Parcel in) { destination = in.readString(); source = in.readString(); title = in.readString(); feedfileId = in.readLong(); feedfileType = in.readInt(); + ifModifiedSince = in.readLong(); deleteOnFailure = (in.readByte() > 0); arguments = in.readBundle(); if (in.dataAvail() > 0) { @@ -77,6 +95,7 @@ public class DownloadRequest implements Parcelable { dest.writeString(title); dest.writeLong(feedfileId); dest.writeInt(feedfileType); + dest.writeLong(ifModifiedSince); dest.writeByte((deleteOnFailure) ? (byte) 1 : 0); dest.writeBundle(arguments); if (username != null) { @@ -105,6 +124,7 @@ public class DownloadRequest implements Parcelable { DownloadRequest that = (DownloadRequest) o; + if (ifModifiedSince != that.ifModifiedSince) return false; if (deleteOnFailure != that.deleteOnFailure) return false; if (feedfileId != that.feedfileId) return false; if (feedfileType != that.feedfileType) return false; @@ -131,6 +151,7 @@ public class DownloadRequest implements Parcelable { result = 31 * result + (title != null ? title.hashCode() : 0); result = 31 * result + (username != null ? username.hashCode() : 0); result = 31 * result + (password != null ? password.hashCode() : 0); + result = 31 * result + (int)ifModifiedSince; result = 31 * result + (deleteOnFailure ? 1 : 0); result = 31 * result + (int) (feedfileId ^ (feedfileId >>> 32)); result = 31 * result + feedfileType; @@ -210,6 +231,15 @@ public class DownloadRequest implements Parcelable { this.password = password; } + public DownloadRequest setIfModifiedSince(long time) { + this.ifModifiedSince = time; + return this; + } + + public long getIfModifiedSince() { + return this.ifModifiedSince; + } + public boolean isDeleteOnFailure() { return deleteOnFailure; } @@ -217,4 +247,54 @@ public class DownloadRequest implements Parcelable { public Bundle getArguments() { return arguments; } + + public static class Builder { + private String destination; + private String source; + private String title; + private String username; + private String password; + private long ifModifiedSince; + private boolean deleteOnFailure = false; + private long feedfileId; + private int feedfileType; + private Bundle arguments; + + public Builder(String destination, FeedFile item) { + this.destination = destination; + this.source = URLChecker.prepareURL(item.getDownload_url()); + this.title = item.getHumanReadableIdentifier(); + this.feedfileId = item.getId(); + this.feedfileType = item.getTypeAsInt(); + } + + public Builder deleteOnFailure(boolean deleteOnFailure) { + this.deleteOnFailure = deleteOnFailure; + return this; + } + + public Builder ifModifiedSince(long time) { + this.ifModifiedSince = time; + return this; + } + + public Builder withAuthentication(String username, String password) { + this.username = username; + this.password = password; + return this; + } + + public DownloadRequest build() { + Validate.notNull(destination); + Validate.notNull(source); + Validate.notNull(title); + return new DownloadRequest(this); + } + + public Builder withArguments(Bundle args) { + this.arguments = args; + return this; + } + + } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadService.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadService.java index 02a6aecbd..60d463178 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadService.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadService.java @@ -52,7 +52,6 @@ import java.util.concurrent.atomic.AtomicInteger; import javax.xml.parsers.ParserConfigurationException; -import de.danoeh.antennapod.core.BuildConfig; import de.danoeh.antennapod.core.ClientConfig; import de.danoeh.antennapod.core.R; import de.danoeh.antennapod.core.feed.EventDistributor; @@ -61,6 +60,7 @@ import de.danoeh.antennapod.core.feed.FeedImage; import de.danoeh.antennapod.core.feed.FeedItem; import de.danoeh.antennapod.core.feed.FeedMedia; import de.danoeh.antennapod.core.feed.FeedPreferences; +import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.storage.DBReader; import de.danoeh.antennapod.core.storage.DBTasks; import de.danoeh.antennapod.core.storage.DBWriter; @@ -172,12 +172,11 @@ public class DownloadService extends Service { @Override public void run() { - if (BuildConfig.DEBUG) Log.d(TAG, "downloadCompletionThread was started"); + Log.d(TAG, "downloadCompletionThread was started"); while (!isInterrupted()) { try { Downloader downloader = downloadExecutor.take().get(); - if (BuildConfig.DEBUG) - Log.d(TAG, "Received 'Download Complete' - message."); + Log.d(TAG, "Received 'Download Complete' - message."); removeDownload(downloader); DownloadStatus status = downloader.getResult(); boolean successful = status.isSuccessful(); @@ -213,13 +212,13 @@ public class DownloadService extends Service { queryDownloadsAsync(); } } catch (InterruptedException e) { - if (BuildConfig.DEBUG) Log.d(TAG, "DownloadCompletionThread was interrupted"); + Log.d(TAG, "DownloadCompletionThread was interrupted"); } catch (ExecutionException e) { e.printStackTrace(); numberOfDownloads.decrementAndGet(); } } - if (BuildConfig.DEBUG) Log.d(TAG, "End of downloadCompletionThread"); + Log.d(TAG, "End of downloadCompletionThread"); } }; @@ -236,8 +235,7 @@ public class DownloadService extends Service { @SuppressLint("NewApi") @Override public void onCreate() { - if (BuildConfig.DEBUG) - Log.d(TAG, "Service started"); + Log.d(TAG, "Service started"); isRunning = true; handler = new Handler(); newMediaFiles = Collections.synchronizedList(new ArrayList<Long>()); @@ -258,8 +256,9 @@ public class DownloadService extends Service { return t; } }); + Log.d(TAG, "parallel downloads: " + UserPreferences.getParallelDownloads()); downloadExecutor = new ExecutorCompletionService<Downloader>( - Executors.newFixedThreadPool(NUM_PARALLEL_DOWNLOADS, + Executors.newFixedThreadPool(UserPreferences.getParallelDownloads(), new ThreadFactory() { @Override @@ -304,8 +303,7 @@ public class DownloadService extends Service { @Override public void onDestroy() { - if (BuildConfig.DEBUG) - Log.d(TAG, "Service shutting down"); + Log.d(TAG, "Service shutting down"); isRunning = false; if (ClientConfig.downloadServiceCallbacks.shouldCreateReport()) { @@ -346,8 +344,7 @@ public class DownloadService extends Service { .setLargeIcon(icon) .setSmallIcon(R.drawable.stat_notify_sync); } - if (BuildConfig.DEBUG) - Log.d(TAG, "Notification set up"); + Log.d(TAG, "Notification set up"); } /** @@ -427,8 +424,7 @@ public class DownloadService extends Service { String url = intent.getStringExtra(EXTRA_DOWNLOAD_URL); Validate.notNull(url, "ACTION_CANCEL_DOWNLOAD intent needs download url extra"); - if (BuildConfig.DEBUG) - Log.d(TAG, "Cancelling download with url " + url); + Log.d(TAG, "Cancelling download with url " + url); Downloader d = getDownloader(url); if (d != null) { d.cancel(); @@ -439,8 +435,7 @@ public class DownloadService extends Service { } else if (StringUtils.equals(intent.getAction(), ACTION_CANCEL_ALL_DOWNLOADS)) { for (Downloader d : downloads) { d.cancel(); - if (BuildConfig.DEBUG) - Log.d(TAG, "Cancelled all downloads"); + Log.d(TAG, "Cancelled all downloads"); } sendBroadcast(new Intent(ACTION_DOWNLOADS_CONTENT_CHANGED)); @@ -451,8 +446,7 @@ public class DownloadService extends Service { }; private void onDownloadQueued(Intent intent) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Received enqueue request"); + Log.d(TAG, "Received enqueue request"); DownloadRequest request = intent.getParcelableExtra(EXTRA_REQUEST); if (request == null) { throw new IllegalArgumentException( @@ -462,7 +456,12 @@ public class DownloadService extends Service { Downloader downloader = getDownloader(request); if (downloader != null) { numberOfDownloads.incrementAndGet(); - downloads.add(downloader); + // smaller rss feeds before bigger media files + if(request.getFeedfileId() == Feed.FEEDFILETYPE_FEED) { + downloads.add(0, downloader); + } else { + downloads.add(downloader); + } downloadExecutor.submit(downloader); sendBroadcast(new Intent(ACTION_DOWNLOADS_CONTENT_CHANGED)); } @@ -490,12 +489,10 @@ public class DownloadService extends Service { handler.post(new Runnable() { @Override public void run() { - if (BuildConfig.DEBUG) - Log.d(TAG, "Removing downloader: " - + d.getDownloadRequest().getSource()); + Log.d(TAG, "Removing downloader: " + + d.getDownloadRequest().getSource()); boolean rc = downloads.remove(d); - if (BuildConfig.DEBUG) - Log.d(TAG, "Result of downloads.remove: " + rc); + Log.d(TAG, "Result of downloads.remove: " + rc); DownloadRequester.getInstance().removeDownload(d.getDownloadRequest()); sendBroadcast(new Intent(ACTION_DOWNLOADS_CONTENT_CHANGED)); } @@ -544,8 +541,7 @@ public class DownloadService extends Service { } if (createReport) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Creating report"); + Log.d(TAG, "Creating report"); // create notification object Notification notification = new NotificationCompat.Builder(this) .setTicker( @@ -569,8 +565,7 @@ public class DownloadService extends Service { NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); nm.notify(REPORT_ID, notification); } else { - if (BuildConfig.DEBUG) - Log.d(TAG, "No report is created"); + Log.d(TAG, "No report is created"); } reportQueue.clear(); } @@ -592,13 +587,10 @@ public class DownloadService extends Service { * Check if there's something else to download, otherwise stop */ void queryDownloads() { - if (BuildConfig.DEBUG) { - Log.d(TAG, numberOfDownloads.get() + " downloads left"); - } + Log.d(TAG, numberOfDownloads.get() + " downloads left"); if (numberOfDownloads.get() <= 0 && DownloadRequester.getInstance().hasNoDownloads()) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Number of downloads is " + numberOfDownloads.get() + ", attempting shutdown"); + Log.d(TAG, "Number of downloads is " + numberOfDownloads.get() + ", attempting shutdown"); stopSelf(); } else { setupNotificationUpdater(); @@ -634,8 +626,7 @@ public class DownloadService extends Service { * Is called whenever a Feed is downloaded */ private void handleCompletedFeedDownload(DownloadRequest request) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Handling completed Feed Download"); + Log.d(TAG, "Handling completed Feed Download"); feedSyncThread.submitCompletedDownload(request); } @@ -644,8 +635,7 @@ public class DownloadService extends Service { * Is called whenever a Feed-Image is downloaded */ private void handleCompletedImageDownload(DownloadStatus status, DownloadRequest request) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Handling completed Image Download"); + Log.d(TAG, "Handling completed Image Download"); syncExecutor.execute(new ImageHandlerThread(status, request)); } @@ -653,13 +643,12 @@ public class DownloadService extends Service { * Is called whenever a FeedMedia is downloaded. */ private void handleCompletedFeedMediaDownload(DownloadStatus status, DownloadRequest request) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Handling completed FeedMedia Download"); + Log.d(TAG, "Handling completed FeedMedia Download"); syncExecutor.execute(new MediaHandlerThread(status, request)); } private void handleFailedDownload(DownloadStatus status, DownloadRequest request) { - if (BuildConfig.DEBUG) Log.d(TAG, "Handling failed download"); + Log.d(TAG, "Handling failed download"); syncExecutor.execute(new FailedDownloadHandler(status, request)); } @@ -709,12 +698,10 @@ public class DownloadService extends Service { long currentTime = startTime; while (requester.isDownloadingFeeds() && (currentTime - startTime) < WAIT_TIMEOUT) { try { - if (BuildConfig.DEBUG) - Log.d(TAG, "Waiting for " + (startTime + WAIT_TIMEOUT - currentTime) + " ms"); + Log.d(TAG, "Waiting for " + (startTime + WAIT_TIMEOUT - currentTime) + " ms"); sleep(startTime + WAIT_TIMEOUT - currentTime); } catch (InterruptedException e) { - if (BuildConfig.DEBUG) - Log.d(TAG, "interrupted while waiting for more downloads"); + Log.d(TAG, "interrupted while waiting for more downloads"); tasks += pollCompletedDownloads(); } finally { currentTime = System.currentTimeMillis(); @@ -762,7 +749,7 @@ public class DownloadService extends Service { continue; } - if (BuildConfig.DEBUG) Log.d(TAG, "Bundling " + results.size() + " feeds"); + Log.d(TAG, "Bundling " + results.size() + " feeds"); for (Pair<DownloadRequest, FeedHandlerResult> result : results) { removeDuplicateImages(result.second.feed); // duplicate images have to removed because the DownloadRequester does not accept two downloads with the same download URL yet. @@ -789,8 +776,7 @@ public class DownloadService extends Service { // Download Feed Image if provided and not downloaded if (savedFeed.getImage() != null && savedFeed.getImage().isDownloaded() == false) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Feed has image; Downloading...."); + Log.d(TAG, "Feed has image; Downloading...."); savedFeed.getImage().setOwner(savedFeed); final Feed savedFeedRef = savedFeed; try { @@ -856,7 +842,7 @@ public class DownloadService extends Service { } - if (BuildConfig.DEBUG) Log.d(TAG, "Shutting down"); + Log.d(TAG, "Shutting down"); } @@ -902,8 +888,7 @@ public class DownloadService extends Service { FeedHandlerResult result = null; try { result = feedHandler.parseFeed(feed); - if (BuildConfig.DEBUG) - Log.d(TAG, feed.getTitle() + " parsed"); + Log.d(TAG, feed.getTitle() + " parsed"); if (checkFeedData(feed) == false) { throw new InvalidFeedException(); } @@ -1008,13 +993,13 @@ public class DownloadService extends Service { */ private void cleanup(Feed feed) { if (feed.getFile_url() != null) { - if (new File(feed.getFile_url()).delete()) - if (BuildConfig.DEBUG) - Log.d(TAG, "Successfully deleted cache file."); - else - Log.e(TAG, "Failed to delete cache file."); + if (new File(feed.getFile_url()).delete()) { + Log.d(TAG, "Successfully deleted cache file."); + } else { + Log.e(TAG, "Failed to delete cache file."); + } feed.setFile_url(null); - } else if (BuildConfig.DEBUG) { + } else { Log.d(TAG, "Didn't delete cache file: File url is not set."); } } @@ -1056,7 +1041,7 @@ public class DownloadService extends Service { @Override public void run() { if (request.isDeleteOnFailure()) { - if (BuildConfig.DEBUG) Log.d(TAG, "Ignoring failed download, deleteOnFailure=true"); + Log.d(TAG, "Ignoring failed download, deleteOnFailure=true"); } else { File dest = new File(request.getDestination()); if (dest.exists() && request.getFeedfileType() == FeedMedia.FEEDFILETYPE_FEEDMEDIA) { @@ -1144,8 +1129,7 @@ public class DownloadService extends Service { mmr.setDataSource(media.getFile_url()); String durationStr = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION); media.setDuration(Integer.parseInt(durationStr)); - if (BuildConfig.DEBUG) - Log.d(TAG, "Duration of file is " + media.getDuration()); + Log.d(TAG, "Duration of file is " + media.getDuration()); } catch (NumberFormatException e) { e.printStackTrace(); } catch (RuntimeException e) { @@ -1191,8 +1175,7 @@ public class DownloadService extends Service { * Schedules the notification updater task if it hasn't been scheduled yet. */ private void setupNotificationUpdater() { - if (BuildConfig.DEBUG) - Log.d(TAG, "Setting up notification updater"); + Log.d(TAG, "Setting up notification updater"); if (notificationUpdater == null) { notificationUpdater = new NotificationUpdater(); notificationUpdaterFuture = schedExecutor.scheduleAtFixedRate( diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/HttpDownloader.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/HttpDownloader.java index 6bfd9b4a0..7abb6df5e 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/download/HttpDownloader.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/HttpDownloader.java @@ -7,6 +7,7 @@ import com.squareup.okhttp.OkHttpClient; import com.squareup.okhttp.Request; import com.squareup.okhttp.Response; import com.squareup.okhttp.ResponseBody; +import com.squareup.okhttp.internal.http.HttpDate; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; @@ -21,6 +22,7 @@ import java.net.HttpURLConnection; import java.net.SocketTimeoutException; import java.net.URI; import java.net.UnknownHostException; +import java.util.Date; import de.danoeh.antennapod.core.BuildConfig; import de.danoeh.antennapod.core.ClientConfig; @@ -64,6 +66,15 @@ public class HttpDownloader extends Downloader { final URI uri = URIUtil.getURIFromRequestUrl(request.getSource()); Request.Builder httpReq = new Request.Builder().url(uri.toURL()) .header("User-Agent", ClientConfig.USER_AGENT); + if(request.getIfModifiedSince() > 0) { + long threeDaysAgo = System.currentTimeMillis() - 1000*60*60*24*3; + if(request.getIfModifiedSince() > threeDaysAgo) { + Date date = new Date(request.getIfModifiedSince()); + String httpDate = HttpDate.format(date); + Log.d(TAG, "addHeader(\"If-Modified-Since\", \"" + httpDate + "\")"); + httpReq.addHeader("If-Modified-Since", httpDate); + } + } // add authentication information String userInfo = uri.getUserInfo(); @@ -83,7 +94,7 @@ public class HttpDownloader extends Downloader { request.setSoFar(destination.length()); httpReq.addHeader("Range", "bytes=" + request.getSoFar() + "-"); - if (BuildConfig.DEBUG) Log.d(TAG, "Adding range header: " + request.getSoFar()); + Log.d(TAG, "Adding range header: " + request.getSoFar()); } Response response = httpClient.newCall(httpReq.build()).execute(); @@ -96,6 +107,12 @@ public class HttpDownloader extends Downloader { if (BuildConfig.DEBUG) Log.d(TAG, "Response code is " + response.code()); + if(!response.isSuccessful() && response.code() == HttpURLConnection.HTTP_NOT_MODIFIED) { + Log.d(TAG, "Feed '" + request.getSource() + "' not modified since last update, Download canceled"); + onCancelled(); + return; + } + if (!response.isSuccessful() || response.body() == null) { final DownloadError error; final String details; diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java index aabbcc185..6f3eedcb2 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java @@ -101,6 +101,18 @@ public class PlaybackService extends Service { public static final String ACTION_SKIP_CURRENT_EPISODE = "action.de.danoeh.antennapod.core.service.skipCurrentEpisode"; /** + * If the PlaybackService receives this action, it will pause playback. + */ + public static final String ACTION_PAUSE_PLAY_CURRENT_EPISODE = "action.de.danoeh.antennapod.core.service.pausePlayCurrentEpisode"; + + + /** + * If the PlaybackService receives this action, it will resume playback. + */ + public static final String ACTION_RESUME_PLAY_CURRENT_EPISODE = "action.de.danoeh.antennapod.core.service.resumePlayCurrentEpisode"; + + + /** * Used in NOTIFICATION_TYPE_RELOAD. */ public static final int EXTRA_CODE_AUDIO = 1; @@ -216,6 +228,10 @@ public class PlaybackService extends Service { AudioManager.ACTION_AUDIO_BECOMING_NOISY)); registerReceiver(skipCurrentEpisodeReceiver, new IntentFilter( ACTION_SKIP_CURRENT_EPISODE)); + registerReceiver(pausePlayCurrentEpisodeReceiver, new IntentFilter( + ACTION_PAUSE_PLAY_CURRENT_EPISODE)); + registerReceiver(pauseResumeCurrentEpisodeReceiver, new IntentFilter( + ACTION_RESUME_PLAY_CURRENT_EPISODE)); remoteControlClient = setupRemoteControlClient(); taskManager = new PlaybackServiceTaskManager(this, taskManagerCallback); mediaPlayer = new PlaybackServiceMediaPlayer(this, mediaPlayerCallback); @@ -427,6 +443,7 @@ public class PlaybackService extends Service { // remove notifcation on pause stopForeground(true); } + writePlayerStatusPlaybackPreferences(); break; @@ -443,9 +460,11 @@ public class PlaybackService extends Service { taskManager.startPositionSaver(); taskManager.startWidgetUpdater(); + writePlayerStatusPlaybackPreferences(); setupNotification(newInfo); started = true; break; + case ERROR: writePlaybackPreferencesNoMediaPlaying(); break; @@ -634,9 +653,26 @@ public class PlaybackService extends Service { editor.putLong( PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEEDMEDIA_ID, PlaybackPreferences.NO_MEDIA_PLAYING); + editor.putInt( + PlaybackPreferences.PREF_CURRENT_PLAYER_STATUS, + PlaybackPreferences.PLAYER_STATUS_OTHER); editor.commit(); } + private int getCurrentPlayerStatusAsInt(PlayerStatus playerStatus) { + int playerStatusAsInt; + switch (playerStatus) { + case PLAYING: + playerStatusAsInt = PlaybackPreferences.PLAYER_STATUS_PLAYING; + break; + case PAUSED: + playerStatusAsInt = PlaybackPreferences.PLAYER_STATUS_PAUSED; + break; + default: + playerStatusAsInt = PlaybackPreferences.PLAYER_STATUS_OTHER; + } + return playerStatusAsInt; + } private void writePlaybackPreferences() { if (BuildConfig.DEBUG) @@ -647,6 +683,7 @@ public class PlaybackService extends Service { PlaybackServiceMediaPlayer.PSMPInfo info = mediaPlayer.getPSMPInfo(); MediaType mediaType = mediaPlayer.getCurrentMediaType(); boolean stream = mediaPlayer.isStreaming(); + int playerStatus = getCurrentPlayerStatusAsInt(info.playerStatus); if (info.playable != null) { editor.putLong(PlaybackPreferences.PREF_CURRENTLY_PLAYING_MEDIA, @@ -683,6 +720,23 @@ public class PlaybackService extends Service { PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEEDMEDIA_ID, PlaybackPreferences.NO_MEDIA_PLAYING); } + editor.putInt( + PlaybackPreferences.PREF_CURRENT_PLAYER_STATUS, playerStatus); + + editor.commit(); + } + + private void writePlayerStatusPlaybackPreferences() { + if (BuildConfig.DEBUG) + Log.d(TAG, "Writing player status playback preferences"); + + SharedPreferences.Editor editor = PreferenceManager + .getDefaultSharedPreferences(getApplicationContext()).edit(); + PlaybackServiceMediaPlayer.PSMPInfo info = mediaPlayer.getPSMPInfo(); + int playerStatus = getCurrentPlayerStatusAsInt(info.playerStatus); + + editor.putInt( + PlaybackPreferences.PREF_CURRENT_PLAYER_STATUS, playerStatus); editor.commit(); } @@ -1101,6 +1155,28 @@ public class PlaybackService extends Service { } }; + private BroadcastReceiver pauseResumeCurrentEpisodeReceiver = new BroadcastReceiver() { + @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"); + mediaPlayer.resume(); + } + } + }; + + private BroadcastReceiver pausePlayCurrentEpisodeReceiver = new BroadcastReceiver() { + @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"); + mediaPlayer.pause(false, false); + } + } + }; + public static MediaType getCurrentMediaType() { return currentMediaType; } diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceMediaPlayer.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceMediaPlayer.java index c143d7f2c..b7c02011d 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceMediaPlayer.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceMediaPlayer.java @@ -169,7 +169,8 @@ public class PlaybackServiceMediaPlayer { if (media != null) { - if (!forceReset && media.getIdentifier().equals(playable.getIdentifier())) { + 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."); @@ -179,6 +180,10 @@ public class PlaybackServiceMediaPlayer { if (playerStatus == PlayerStatus.PAUSED || playerStatus == PlayerStatus.PLAYING || playerStatus == PlayerStatus.PREPARED) { mediaPlayer.stop(); } + // set temporarily to pause in order to update list with current position + if (playerStatus == PlayerStatus.PLAYING) { + setPlayerStatus(PlayerStatus.PAUSED, media); + } setPlayerStatus(PlayerStatus.INDETERMINATE, null); } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/DBTasks.java b/core/src/main/java/de/danoeh/antennapod/core/storage/DBTasks.java index e73f9599d..e0e370b0d 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/storage/DBTasks.java +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/DBTasks.java @@ -298,7 +298,8 @@ public final class DBTasks { } /** - * Updates a specific Feed. + * Refresh a specific Feed. The refresh may get canceled if the feed does not seem to be modified + * and the last update was only few days ago. * * @param context Used for requesting the download. * @param feed The Feed object. @@ -311,9 +312,9 @@ public final class DBTasks { private static void refreshFeed(Context context, Feed feed, boolean loadAllPages) throws DownloadRequestException { Feed f; if (feed.getPreferences() == null) { - f = new Feed(feed.getDownload_url(), new Date(), feed.getTitle()); + f = new Feed(feed.getDownload_url(), feed.getLastUpdate(), feed.getTitle()); } else { - f = new Feed(feed.getDownload_url(), new Date(), feed.getTitle(), + f = new Feed(feed.getDownload_url(), feed.getLastUpdate(), feed.getTitle(), feed.getPreferences().getUsername(), feed.getPreferences().getPassword()); } f.setId(feed.getId()); diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/DBWriter.java b/core/src/main/java/de/danoeh/antennapod/core/storage/DBWriter.java index 87bbdf455..c5bf89533 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/storage/DBWriter.java +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/DBWriter.java @@ -7,7 +7,6 @@ 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; @@ -35,6 +34,7 @@ import de.danoeh.antennapod.core.feed.FeedMedia; import de.danoeh.antennapod.core.feed.FeedPreferences; 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; @@ -239,6 +239,26 @@ public class DBWriter { } /** + * Deletes the entire download log. + * + * @param context A context that is used for opening a database connection. + */ + public static Future<?> clearDownloadLog(final Context context) { + return dbExec.submit(new Runnable() { + @Override + public void run() { + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + adapter.clearDownloadLog(); + adapter.close(); + EventDistributor.getInstance() + .sendDownloadLogUpdateBroadcast(); + } + }); + } + + + /** * Adds a FeedMedia object to the playback history. A FeedMedia object is in the playback history if * its playback completion date is set to a non-null value. This method will set the playback completion date to the * current date regardless of the current value. @@ -386,7 +406,16 @@ public class DBWriter { context, itemIds[i]); if (item != null) { - queue.add(item); + // add item to either front ot back of queue + boolean addToFront = PreferenceManager.getDefaultSharedPreferences(context) + .getBoolean(UserPreferences.PREF_QUEUE_ADD_TO_FRONT, false); + + if(addToFront){ + queue.add(0, item); + }else{ + queue.add(item); + } + queueModified = true; if (!item.isRead()) { item.setRead(true); diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/DownloadRequester.java b/core/src/main/java/de/danoeh/antennapod/core/storage/DownloadRequester.java index d0cdad649..ca6aa0178 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/storage/DownloadRequester.java +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/DownloadRequester.java @@ -91,57 +91,53 @@ public class DownloadRequester { } private void download(Context context, FeedFile item, FeedFile container, File dest, - boolean overwriteIfExists, String username, String password, boolean deleteOnFailure, Bundle arguments) { + boolean overwriteIfExists, String username, String password, + long ifModifiedSince, boolean deleteOnFailure, Bundle arguments) { final boolean partiallyDownloadedFileExists = item.getFile_url() != null; - if (!isDownloadingFile(item)) { - if (!isFilenameAvailable(dest.toString()) || (!partiallyDownloadedFileExists && dest.exists())) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Filename already used."); - if (isFilenameAvailable(dest.toString()) && overwriteIfExists) { - boolean result = dest.delete(); - if (BuildConfig.DEBUG) - Log.d(TAG, "Deleting file. Result: " + result); - } else { - // find different name - File newDest = null; - for (int i = 1; i < Integer.MAX_VALUE; i++) { - String newName = FilenameUtils.getBaseName(dest - .getName()) - + "-" - + i - + FilenameUtils.EXTENSION_SEPARATOR - + FilenameUtils.getExtension(dest.getName()); - if (BuildConfig.DEBUG) - Log.d(TAG, "Testing filename " + newName); - newDest = new File(dest.getParent(), newName); - if (!newDest.exists() - && isFilenameAvailable(newDest.toString())) { - if (BuildConfig.DEBUG) - Log.d(TAG, "File doesn't exist yet. Using " - + newName); - break; - } - } - if (newDest != null) { - dest = newDest; + if (isDownloadingFile(item)) { + Log.e(TAG, "URL " + item.getDownload_url() + + " is already being downloaded"); + return; + } + if (!isFilenameAvailable(dest.toString()) || (!partiallyDownloadedFileExists && dest.exists())) { + Log.d(TAG, "Filename already used."); + if (isFilenameAvailable(dest.toString()) && overwriteIfExists) { + boolean result = dest.delete(); + Log.d(TAG, "Deleting file. Result: " + result); + } else { + // find different name + File newDest = null; + for (int i = 1; i < Integer.MAX_VALUE; i++) { + String newName = FilenameUtils.getBaseName(dest + .getName()) + + "-" + + i + + FilenameUtils.EXTENSION_SEPARATOR + + FilenameUtils.getExtension(dest.getName()); + Log.d(TAG, "Testing filename " + newName); + newDest = new File(dest.getParent(), newName); + if (!newDest.exists() + && isFilenameAvailable(newDest.toString())) { + Log.d(TAG, "File doesn't exist yet. Using " + newName); + break; } } + if (newDest != null) { + dest = newDest; + } } - if (BuildConfig.DEBUG) - Log.d(TAG, - "Requesting download of url " + item.getDownload_url()); - String baseUrl = (container != null) ? container.getDownload_url() : null; - item.setDownload_url(URLChecker.prepareURL(item.getDownload_url(), baseUrl)); - - DownloadRequest request = new DownloadRequest(dest.toString(), - URLChecker.prepareURL(item.getDownload_url()), item.getHumanReadableIdentifier(), - item.getId(), item.getTypeAsInt(), username, password, deleteOnFailure, arguments); - - download(context, request); - } else { - Log.e(TAG, "URL " + item.getDownload_url() - + " is already being downloaded"); } + Log.d(TAG, "Requesting download of url " + item.getDownload_url()); + String baseUrl = (container != null) ? container.getDownload_url() : null; + item.setDownload_url(URLChecker.prepareURL(item.getDownload_url(), baseUrl)); + + DownloadRequest.Builder builder = new DownloadRequest.Builder(dest.toString(), item) + .withAuthentication(username, password) + .ifModifiedSince(ifModifiedSince) + .deleteOnFailure(deleteOnFailure) + .withArguments(arguments); + DownloadRequest request = builder.build(); + download(context, request); } /** @@ -163,18 +159,26 @@ public class DownloadRequester { return true; } + /** + * Downloads a feed + * + * @param context The application's environment. + * @param feed Feed to download + * @param loadAllPages Set to true to download all pages + */ public synchronized void downloadFeed(Context context, Feed feed, boolean loadAllPages) throws DownloadRequestException { if (feedFileValid(feed)) { String username = (feed.getPreferences() != null) ? feed.getPreferences().getUsername() : null; String password = (feed.getPreferences() != null) ? feed.getPreferences().getPassword() : null; + long ifModifiedSince = feed.getLastUpdate().getTime(); Bundle args = new Bundle(); args.putInt(REQUEST_ARG_PAGE_NR, feed.getPageNr()); args.putBoolean(REQUEST_ARG_LOAD_ALL_PAGES, loadAllPages); download(context, feed, null, new File(getFeedfilePath(context), - getFeedfileName(feed)), true, username, password, true, args); + getFeedfileName(feed)), true, username, password, ifModifiedSince, true, args); } } @@ -187,7 +191,7 @@ public class DownloadRequester { if (feedFileValid(image)) { FeedFile container = (image.getOwner() instanceof FeedFile) ? (FeedFile) image.getOwner() : null; download(context, image, container, new File(getImagefilePath(context), - getImagefileName(image)), false, null, null, false, null); + getImagefileName(image)), false, null, null, 0, false, null); } } @@ -213,7 +217,7 @@ public class DownloadRequester { getMediafilename(feedmedia)); } download(context, feedmedia, feed, - dest, false, username, password, false, null); + dest, false, username, password, 0, false, null); } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/PodDBAdapter.java b/core/src/main/java/de/danoeh/antennapod/core/storage/PodDBAdapter.java index ce41147e1..f72858adc 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/storage/PodDBAdapter.java +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/PodDBAdapter.java @@ -889,6 +889,10 @@ public class PodDBAdapter { db.update(TABLE_NAME_FEED_MEDIA, values, null, null); } + public void clearDownloadLog() { + db.delete(TABLE_NAME_DOWNLOAD_LOG, null, null); + } + /** * Get all Feeds from the Feed Table. * diff --git a/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/SyndHandler.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/SyndHandler.java index 1dda24944..47503dee4 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/SyndHandler.java +++ b/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/SyndHandler.java @@ -1,14 +1,23 @@ package de.danoeh.antennapod.core.syndication.handler; import android.util.Log; -import de.danoeh.antennapod.core.BuildConfig; -import de.danoeh.antennapod.core.feed.Feed; -import de.danoeh.antennapod.core.syndication.namespace.*; -import de.danoeh.antennapod.core.syndication.namespace.atom.NSAtom; + import org.xml.sax.Attributes; import org.xml.sax.SAXException; import org.xml.sax.helpers.DefaultHandler; +import de.danoeh.antennapod.core.BuildConfig; +import de.danoeh.antennapod.core.feed.Feed; +import de.danoeh.antennapod.core.syndication.namespace.NSContent; +import de.danoeh.antennapod.core.syndication.namespace.NSDublinCore; +import de.danoeh.antennapod.core.syndication.namespace.NSITunes; +import de.danoeh.antennapod.core.syndication.namespace.NSMedia; +import de.danoeh.antennapod.core.syndication.namespace.NSRSS20; +import de.danoeh.antennapod.core.syndication.namespace.NSSimpleChapters; +import de.danoeh.antennapod.core.syndication.namespace.Namespace; +import de.danoeh.antennapod.core.syndication.namespace.SyndElement; +import de.danoeh.antennapod.core.syndication.namespace.atom.NSAtom; + /** Superclass for all SAX Handlers which process Syndication formats */ public class SyndHandler extends DefaultHandler { private static final String TAG = "SyndHandler"; @@ -100,7 +109,12 @@ public class SyndHandler extends DefaultHandler { state.namespaces.put(uri, new NSMedia()); if (BuildConfig.DEBUG) Log.d(TAG, "Recognized media namespace"); - } + } else if (uri.equals(NSDublinCore.NSURI) + && prefix.equals(NSDublinCore.NSTAG)) { + state.namespaces.put(uri, new NSDublinCore()); + if (BuildConfig.DEBUG) + Log.d(TAG, "Recognized DublinCore namespace"); + } } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSDublinCore.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSDublinCore.java new file mode 100644 index 000000000..099593eed --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSDublinCore.java @@ -0,0 +1,37 @@ +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; + +public class NSDublinCore extends Namespace { + private static final String TAG = "NSDublinCore"; + public static final String NSTAG = "dc"; + public static final String NSURI = "http://purl.org/dc/elements/1.1/"; + + private static final String ITEM = "item"; + private static final String DATE = "date"; + + @Override + public SyndElement handleElementStart(String localName, HandlerState state, + Attributes attributes) { + return new SyndElement(localName, this); + } + + @Override + public void handleElementEnd(String localName, HandlerState state) { + if(state.getTagstack().size() >= 2 + && state.getContentBuf() != null) { + String content = state.getContentBuf().toString(); + SyndElement topElement = state.getTagstack().peek(); + String top = topElement.getName(); + SyndElement secondElement = state.getSecondTag(); + String second = secondElement.getName(); + if (top.equals(DATE) && second.equals(ITEM)) { + state.getCurrentItem().setPubDate( + SyndDateUtils.parseISO8601Date(content)); + } + } + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/syndication/util/SyndDateUtils.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/util/SyndDateUtils.java index 1ac389f08..a9929d7b1 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/syndication/util/SyndDateUtils.java +++ b/core/src/main/java/de/danoeh/antennapod/core/syndication/util/SyndDateUtils.java @@ -28,6 +28,8 @@ public class SyndDateUtils { */ 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() { @@ -44,6 +46,14 @@ public class SyndDateUtils { }; + 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")) { @@ -123,6 +133,23 @@ public class SyndDateUtils { } + 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. diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/gui/UndoBarController.java b/core/src/main/java/de/danoeh/antennapod/core/util/gui/UndoBarController.java new file mode 100644 index 000000000..0e03bc8b4 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/gui/UndoBarController.java @@ -0,0 +1,121 @@ +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; + +import com.nineoldandroids.animation.Animator; +import com.nineoldandroids.animation.AnimatorListenerAdapter; +import com.nineoldandroids.view.ViewHelper; +import com.nineoldandroids.view.ViewPropertyAnimator; + +import de.danoeh.antennapod.core.R; + +import static com.nineoldandroids.view.ViewPropertyAnimator.animate; + +public class UndoBarController { + private View mBarView; + private TextView mMessageView; + private ViewPropertyAnimator mBarAnimator; + private Handler mHideHandler = new Handler(); + + private UndoListener mUndoListener; + + // State objects + private Parcelable mUndoToken; + private CharSequence mUndoMessage; + + public interface UndoListener { + void onUndo(Parcelable token); + } + + public UndoBarController(View undoBarView, UndoListener undoListener) { + mBarView = undoBarView; + mBarAnimator = animate(mBarView); + mUndoListener = undoListener; + + mMessageView = (TextView) mBarView.findViewById(R.id.undobar_message); + mBarView.findViewById(R.id.undobar_button) + .setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + hideUndoBar(false); + mUndoListener.onUndo(mUndoToken); + } + }); + + hideUndoBar(true); + } + + public void showUndoBar(boolean immediate, CharSequence message, Parcelable undoToken) { + mUndoToken = undoToken; + mUndoMessage = message; + mMessageView.setText(mUndoMessage); + + mHideHandler.removeCallbacks(mHideRunnable); + mHideHandler.postDelayed(mHideRunnable, + mBarView.getResources().getInteger(R.integer.undobar_hide_delay)); + + mBarView.setVisibility(View.VISIBLE); + if (immediate) { + ViewHelper.setAlpha(mBarView, 1); + } else { + mBarAnimator.cancel(); + mBarAnimator + .alpha(1) + .setDuration( + mBarView.getResources() + .getInteger(android.R.integer.config_shortAnimTime)) + .setListener(null); + } + } + + public void hideUndoBar(boolean immediate) { + mHideHandler.removeCallbacks(mHideRunnable); + if (immediate) { + mBarView.setVisibility(View.GONE); + ViewHelper.setAlpha(mBarView, 0); + mUndoMessage = null; + } else { + mBarAnimator.cancel(); + mBarAnimator + .alpha(0) + .setDuration(mBarView.getResources() + .getInteger(android.R.integer.config_shortAnimTime)) + .setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mBarView.setVisibility(View.GONE); + mUndoMessage = null; + mUndoToken = null; + } + }); + } + } + + public void onSaveInstanceState(Bundle outState) { + outState.putCharSequence("undo_message", mUndoMessage); + outState.putParcelable("undo_token", mUndoToken); + } + + public void onRestoreInstanceState(Bundle savedInstanceState) { + if (savedInstanceState != null) { + mUndoMessage = savedInstanceState.getCharSequence("undo_message"); + mUndoToken = savedInstanceState.getParcelable("undo_token"); + + if (mUndoToken != null || !TextUtils.isEmpty(mUndoMessage)) { + showUndoBar(true, mUndoMessage, mUndoToken); + } + } + } + + private Runnable mHideRunnable = new Runnable() { + @Override + public void run() { + hideUndoBar(false); + } + }; +} diff --git a/core/src/main/res/drawable-hdpi/ic_drag_handle.9.png b/core/src/main/res/drawable-hdpi/ic_drag_handle.9.png Binary files differnew file mode 100644 index 000000000..939454989 --- /dev/null +++ b/core/src/main/res/drawable-hdpi/ic_drag_handle.9.png diff --git a/core/src/main/res/drawable-hdpi/ic_drag_handle.png b/core/src/main/res/drawable-hdpi/ic_drag_handle.png Binary files differdeleted file mode 100755 index 38ec201de..000000000 --- a/core/src/main/res/drawable-hdpi/ic_drag_handle.png +++ /dev/null diff --git a/core/src/main/res/drawable-hdpi/ic_drag_handle_dark.9.png b/core/src/main/res/drawable-hdpi/ic_drag_handle_dark.9.png Binary files differnew file mode 100644 index 000000000..65b9ec1fa --- /dev/null +++ b/core/src/main/res/drawable-hdpi/ic_drag_handle_dark.9.png diff --git a/core/src/main/res/drawable-hdpi/ic_drag_handle_dark.png b/core/src/main/res/drawable-hdpi/ic_drag_handle_dark.png Binary files differdeleted file mode 100755 index e96d23252..000000000 --- a/core/src/main/res/drawable-hdpi/ic_drag_handle_dark.png +++ /dev/null diff --git a/core/src/main/res/drawable-hdpi/ic_feed_grey600_24dp.png b/core/src/main/res/drawable-hdpi/ic_feed_grey600_24dp.png Binary files differnew file mode 100755 index 000000000..46be3e14e --- /dev/null +++ b/core/src/main/res/drawable-hdpi/ic_feed_grey600_24dp.png diff --git a/core/src/main/res/drawable-hdpi/ic_feed_white_24dp.png b/core/src/main/res/drawable-hdpi/ic_feed_white_24dp.png Binary files differnew file mode 100755 index 000000000..3d57127f5 --- /dev/null +++ b/core/src/main/res/drawable-hdpi/ic_feed_white_24dp.png diff --git a/core/src/main/res/drawable-mdpi/ic_drag_handle.9.png b/core/src/main/res/drawable-mdpi/ic_drag_handle.9.png Binary files differnew file mode 100644 index 000000000..8de13a08b --- /dev/null +++ b/core/src/main/res/drawable-mdpi/ic_drag_handle.9.png diff --git a/core/src/main/res/drawable-mdpi/ic_drag_handle.png b/core/src/main/res/drawable-mdpi/ic_drag_handle.png Binary files differdeleted file mode 100755 index 4afbdc67d..000000000 --- a/core/src/main/res/drawable-mdpi/ic_drag_handle.png +++ /dev/null diff --git a/core/src/main/res/drawable-mdpi/ic_drag_handle_dark.9.png b/core/src/main/res/drawable-mdpi/ic_drag_handle_dark.9.png Binary files differnew file mode 100644 index 000000000..e24681d12 --- /dev/null +++ b/core/src/main/res/drawable-mdpi/ic_drag_handle_dark.9.png diff --git a/core/src/main/res/drawable-mdpi/ic_drag_handle_dark.png b/core/src/main/res/drawable-mdpi/ic_drag_handle_dark.png Binary files differdeleted file mode 100755 index 2b25c4101..000000000 --- a/core/src/main/res/drawable-mdpi/ic_drag_handle_dark.png +++ /dev/null diff --git a/core/src/main/res/drawable-mdpi/ic_feed_grey600_24dp.png b/core/src/main/res/drawable-mdpi/ic_feed_grey600_24dp.png Binary files differnew file mode 100755 index 000000000..79f082610 --- /dev/null +++ b/core/src/main/res/drawable-mdpi/ic_feed_grey600_24dp.png diff --git a/core/src/main/res/drawable-mdpi/ic_feed_white_24dp.png b/core/src/main/res/drawable-mdpi/ic_feed_white_24dp.png Binary files differnew file mode 100755 index 000000000..15a4b16bf --- /dev/null +++ b/core/src/main/res/drawable-mdpi/ic_feed_white_24dp.png diff --git a/core/src/main/res/drawable-xhdpi/ic_drag_handle.9.png b/core/src/main/res/drawable-xhdpi/ic_drag_handle.9.png Binary files differnew file mode 100644 index 000000000..46b8a5ad8 --- /dev/null +++ b/core/src/main/res/drawable-xhdpi/ic_drag_handle.9.png diff --git a/core/src/main/res/drawable-xhdpi/ic_drag_handle.png b/core/src/main/res/drawable-xhdpi/ic_drag_handle.png Binary files differdeleted file mode 100755 index 5bdcac342..000000000 --- a/core/src/main/res/drawable-xhdpi/ic_drag_handle.png +++ /dev/null diff --git a/core/src/main/res/drawable-xhdpi/ic_drag_handle_dark.9.png b/core/src/main/res/drawable-xhdpi/ic_drag_handle_dark.9.png Binary files differnew file mode 100644 index 000000000..864fae9e8 --- /dev/null +++ b/core/src/main/res/drawable-xhdpi/ic_drag_handle_dark.9.png diff --git a/core/src/main/res/drawable-xhdpi/ic_drag_handle_dark.png b/core/src/main/res/drawable-xhdpi/ic_drag_handle_dark.png Binary files differdeleted file mode 100755 index d341c7c82..000000000 --- a/core/src/main/res/drawable-xhdpi/ic_drag_handle_dark.png +++ /dev/null diff --git a/core/src/main/res/drawable-xhdpi/ic_feed_grey600_24dp.png b/core/src/main/res/drawable-xhdpi/ic_feed_grey600_24dp.png Binary files differnew file mode 100755 index 000000000..5cb0262ee --- /dev/null +++ b/core/src/main/res/drawable-xhdpi/ic_feed_grey600_24dp.png diff --git a/core/src/main/res/drawable-xhdpi/ic_feed_white_24dp.png b/core/src/main/res/drawable-xhdpi/ic_feed_white_24dp.png Binary files differnew file mode 100755 index 000000000..5f34b0492 --- /dev/null +++ b/core/src/main/res/drawable-xhdpi/ic_feed_white_24dp.png diff --git a/core/src/main/res/drawable-xxhdpi/ic_drag_handle.9.png b/core/src/main/res/drawable-xxhdpi/ic_drag_handle.9.png Binary files differnew file mode 100644 index 000000000..0e99bde9e --- /dev/null +++ b/core/src/main/res/drawable-xxhdpi/ic_drag_handle.9.png diff --git a/core/src/main/res/drawable-xxhdpi/ic_drag_handle.png b/core/src/main/res/drawable-xxhdpi/ic_drag_handle.png Binary files differdeleted file mode 100755 index f834699c6..000000000 --- a/core/src/main/res/drawable-xxhdpi/ic_drag_handle.png +++ /dev/null diff --git a/core/src/main/res/drawable-xxhdpi/ic_drag_handle_dark.9.png b/core/src/main/res/drawable-xxhdpi/ic_drag_handle_dark.9.png Binary files differnew file mode 100644 index 000000000..0da191a69 --- /dev/null +++ b/core/src/main/res/drawable-xxhdpi/ic_drag_handle_dark.9.png diff --git a/core/src/main/res/drawable-xxhdpi/ic_drag_handle_dark.png b/core/src/main/res/drawable-xxhdpi/ic_drag_handle_dark.png Binary files differdeleted file mode 100755 index a9408bc9d..000000000 --- a/core/src/main/res/drawable-xxhdpi/ic_drag_handle_dark.png +++ /dev/null diff --git a/core/src/main/res/drawable-xxhdpi/ic_feed_grey600_24dp.png b/core/src/main/res/drawable-xxhdpi/ic_feed_grey600_24dp.png Binary files differnew file mode 100755 index 000000000..01ef2ee4d --- /dev/null +++ b/core/src/main/res/drawable-xxhdpi/ic_feed_grey600_24dp.png diff --git a/core/src/main/res/drawable-xxhdpi/ic_feed_white_24dp.png b/core/src/main/res/drawable-xxhdpi/ic_feed_white_24dp.png Binary files differnew file mode 100755 index 000000000..6dd465852 --- /dev/null +++ b/core/src/main/res/drawable-xxhdpi/ic_feed_white_24dp.png diff --git a/core/src/main/res/values-az/strings.xml b/core/src/main/res/values-az/strings.xml index adb983e9e..9640412c1 100644 --- a/core/src/main/res/values-az/strings.xml +++ b/core/src/main/res/values-az/strings.xml @@ -173,7 +173,6 @@ <string name="search_label">Axtar</string> <string name="found_in_title_label">Başlığda tapıldı</string> <!--OPML import and export--> - <string name="opml_import_explanation">OPML faylın idxalı üçün onu aşağıdakı qovluqa yerləşdirin və idxal prosesini başlamaq üçün düyməyi basın.</string> <string name="start_import_label">İdxalı başla</string> <string name="opml_import_label">OPML idxalı</string> <string name="opml_directory_error">XƏTA!</string> @@ -182,11 +181,9 @@ <string name="opml_import_error_dir_empty">İdxal qovliqu boşdur.</string> <string name="select_all_label">Hamısını seç</string> <string name="deselect_all_label">Seçimi ləğv et</string> - <string name="choose_file_to_import_label">İdxal üçün fayl seç</string> <string name="opml_export_label">OPML ixraçı</string> <string name="exporting_label">İxrac...</string> <string name="export_error_label">İxracın xətası</string> - <string name="opml_export_success_title">OPML ixracı uğurlu keçdi</string> <string name="opml_export_success_sum">OPML fayl:\u0020 yazılıb</string> <!--Sleep timer--> <string name="set_sleeptimer_label">Yuxu taymerini qoy</string> diff --git a/core/src/main/res/values-ca/strings.xml b/core/src/main/res/values-ca/strings.xml index 7f8aeebae..0fe3ae415 100644 --- a/core/src/main/res/values-ca/strings.xml +++ b/core/src/main/res/values-ca/strings.xml @@ -224,6 +224,7 @@ <string name="pref_autodl_wifi_filter_title">Activa el filtre de la xarxa sense fils</string> <string name="pref_autodl_wifi_filter_sum">Permet les baixades automàtiques només per a les xarxes sense fils seleccionades.</string> <string name="pref_automatic_download_on_battery_title">Baixa mentre no es carrega</string> + <string name="pref_automatic_download_on_battery_sum">Permet les baixades automàtiques mentre la bateria no es carrega</string> <string name="pref_episode_cache_title">Memòria d\'episodis</string> <string name="pref_theme_title_light">Clar</string> <string name="pref_theme_title_dark">Fosc</string> @@ -243,6 +244,11 @@ <string name="pref_seek_delta_sum">Salta aquesta quantitat de segons en rebobinar o en avançar ràpidament</string> <string name="pref_gpodnet_sethostname_title">Definex nom del servidor</string> <string name="pref_gpodnet_sethostname_use_default_host">Utilitza el servidor per defecte</string> + <string name="pref_expandNotify_title">Amplia la notificació</string> + <string name="pref_expandNotify_sum">Amplia sempre les notificacions per mostrar els botons de reproducció.</string> + <string name="pref_persistNotify_title">Botons de reproducció persistents</string> + <string name="pref_persistNotify_sum">Mantén els controls a l\'àrea de notificacions i pantalla de bloqueig quan la reproducció estigui aturada</string> + <string name="pref_expand_notify_unsupport_toast">Les versions d\'Android anteriors a la 4.1 no suporten les notificacions ampliades.</string> <!--Auto-Flattr dialog--> <string name="auto_flattr_enable">Activa la compartició automàtica per Flattr</string> <string name="auto_flattr_after_percent">Comparteix per Flattr l\'episodi en haver-ne reproduït el %d per cent</string> @@ -257,7 +263,6 @@ <string name="found_in_title_label">Trobat al títol</string> <!--OPML import and export--> <string name="opml_import_txtv_button_lable">Els fitxers OPML us permeten moure els podcasts d\'un gestor de podcasts a un altre.</string> - <string name="opml_import_explanation">Per importar un fitxer OPML, ubiqueu-lo al següent directori i premeu el botó de sota per iniciar el procés. </string> <string name="start_import_label">Inicia la importació</string> <string name="opml_import_label">Importació OPML</string> <string name="opml_directory_error">Error!</string> @@ -266,7 +271,6 @@ <string name="opml_import_error_dir_empty">El directori d\'importacions és buit.</string> <string name="select_all_label">Selecciona-ho tot</string> <string name="deselect_all_label">Deselecciona-ho tot</string> - <string name="choose_file_to_import_label">Seleccioneu el fitxer a importar</string> <string name="opml_export_label">Exportació OPML</string> <string name="exporting_label">S\'està exportant...</string> <string name="export_error_label">Error d\'exportació</string> @@ -290,6 +294,7 @@ <string name="gpodnetauth_login_title">Inici de sessió</string> <string name="gpodnetauth_login_descr">Benvingut al procés d\'inici de sessió a gpodder.net. Primerament, introduïu la informació d\'accés:</string> <string name="gpodnetauth_login_butLabel">Entra</string> + <string name="gpodnetauth_login_register">Si no teniu compte, podeu crear-ne un aquí:\nhttps://gpodder.net/register/</string> <string name="username_label">Nom d\'usuari</string> <string name="password_label">Contrasenya</string> <string name="gpodnetauth_device_title">Selecció de dispositiu</string> @@ -345,6 +350,7 @@ <string name="new_episodes_count_label">Nombre d\'episodis nous</string> <string name="in_progress_episodes_count_label">Nombre d\'episodis que heu començat a escoltar</string> <string name="drag_handle_content_description">Arrossegueu l\'element per canviar-ne la posició</string> + <string name="load_next_page_label">Carrega la següent pàgina</string> <!--Feed information screen--> <string name="authentication_label">Autenticació</string> <string name="authentication_descr">Canvieu el nom d\'usuari i contrasenya per a aquest podcast i els seus episodis.</string> diff --git a/core/src/main/res/values-cs-rCZ/strings.xml b/core/src/main/res/values-cs-rCZ/strings.xml index 71a064d48..a67a6d10b 100644 --- a/core/src/main/res/values-cs-rCZ/strings.xml +++ b/core/src/main/res/values-cs-rCZ/strings.xml @@ -256,7 +256,6 @@ <string name="found_in_title_label">Nalezeno v názvu</string> <!--OPML import and export--> <string name="opml_import_txtv_button_lable">OPML soubory umožňují přenést vaše podcasty z jednoho přehrávače na jiný.</string> - <string name="opml_import_explanation">Pro načtení OPML souboru je třeba ho nejdříve umístit do následujícího adresáře a poté stisknout tlačítko pro zahájení procesu importu. </string> <string name="start_import_label">Importovat</string> <string name="opml_import_label">OPML import</string> <string name="opml_directory_error">CHYBA!</string> @@ -265,7 +264,6 @@ <string name="opml_import_error_dir_empty">Adresář importu je prázdný.</string> <string name="select_all_label">Označit vše</string> <string name="deselect_all_label">Zrušit výběr</string> - <string name="choose_file_to_import_label">Vyberte soubor k importování</string> <string name="opml_export_label">OPML export</string> <string name="exporting_label">Exportuji...</string> <string name="export_error_label">Chyba exportu</string> diff --git a/core/src/main/res/values-da/strings.xml b/core/src/main/res/values-da/strings.xml index b3ba5006b..d31c65614 100644 --- a/core/src/main/res/values-da/strings.xml +++ b/core/src/main/res/values-da/strings.xml @@ -263,7 +263,6 @@ <string name="found_in_title_label">Fundet i titel</string> <!--OPML import and export--> <string name="opml_import_txtv_button_lable">OPML filer lader dig flytte dine podcasts fra en podcastafspiller til en anden.</string> - <string name="opml_import_explanation">For at importere en OPML fil, skal du først placere den i følgende mappe og tryk på knappen nedenfor for at starte import-processen.</string> <string name="start_import_label">Start import</string> <string name="opml_import_label">OPML import</string> <string name="opml_directory_error">FEJL!</string> @@ -272,7 +271,6 @@ <string name="opml_import_error_dir_empty">Import mappen er tom.</string> <string name="select_all_label">Vælg alt</string> <string name="deselect_all_label">Fravælg alt</string> - <string name="choose_file_to_import_label">Vælg fil at importere</string> <string name="opml_export_label">OPML eksport</string> <string name="exporting_label">Eksporterer...</string> <string name="export_error_label">Eksport fejl</string> diff --git a/core/src/main/res/values-de/strings.xml b/core/src/main/res/values-de/strings.xml index e47b65820..1f1b519ab 100644 --- a/core/src/main/res/values-de/strings.xml +++ b/core/src/main/res/values-de/strings.xml @@ -58,16 +58,19 @@ <string name="close_label">Schließen</string> <string name="retry_label">Erneut versuchen</string> <string name="auto_download_label">Automatisch herunterladen</string> + <string name="parallel_downloads_suffix">\u0020gleichzeitige Downloads</string> <!--'Add Feed' Activity labels--> <string name="feedurl_label">Feed URL</string> <string name="etxtFeedurlHint">URL des Feeds oder der Webseite</string> <string name="txtvfeedurl_label">Podcast per URL hinzufügen</string> <string name="podcastdirectories_label">Podcast in Verzeichnis finden</string> - <string name="podcastdirectories_descr">Bei gpodder.net kannst du nach neuen Podcasts nach Name, Kategorie oder Popularität suchen.</string> + <string name="podcastdirectories_descr">Bei gpodder.net kannst du neue Podcasts nach Name, Kategorie oder Popularität suchen.</string> <string name="browse_gpoddernet_label">gpodder.net durchsuchen</string> <!--Actions on feeds--> - <string name="mark_all_read_label">Markiere alle als gelesen</string> - <string name="mark_all_read_msg">Alle Episoden als gelesen markieren</string> + <string name="mark_all_read_label">Alle als gespielt markieren</string> + <string name="mark_all_read_msg">Alle Episoden als gespielt markieren</string> + <string name="mark_all_read_confirmation_msg">Bitte bestätige, dass alle Episoden als gespielt markiert werden sollen.</string> + <string name="mark_all_read_feed_confirmation_msg">Bitte bestätige, dass alle Episoden in diesem Feed als gespielt markiert werden sollen.</string> <string name="show_info_label">Informationen anzeigen</string> <string name="remove_feed_label">Podcast entfernen</string> <string name="share_link_label">Webseiten-Link teilen</string> @@ -83,12 +86,13 @@ <string name="stream_label">Streamen</string> <string name="remove_label">Entfernen</string> <string name="remove_episode_lable">Episode entfernen</string> - <string name="mark_read_label">Als gelesen markieren</string> - <string name="mark_unread_label">Als ungelesen markieren</string> + <string name="mark_read_label">Als gespielt markieren</string> + <string name="mark_unread_label">Als ungespielt markieren</string> + <string name="marked_as_read_label">Als gespielt markiert</string> <string name="add_to_queue_label">Zur Abspielliste hinzufügen</string> <string name="remove_from_queue_label">Aus der Abspielliste entfernen</string> <string name="visit_website_label">Webseite besuchen</string> - <string name="support_label">Flattr this</string> + <string name="support_label">Flattrn</string> <string name="enqueue_all_new">Alle zur Abspielliste hinzufügen</string> <string name="download_all">Alle herunterladen</string> <string name="skip_episode_label">Episode überspringen</string> @@ -150,9 +154,10 @@ <string name="duration">Dauer</string> <string name="ascending">Aufsteigend</string> <string name="descending">Absteigend</string> + <string name="clear_queue_confirmation_msg">Bitte bestätige, dass alle Episoden in der Warteschlange gelöscht werden sollen</string> <!--Flattr--> <string name="flattr_auth_label">Flattr Anmeldung</string> - <string name="flattr_auth_explanation">Drücke den Button unten um den Authentifizierungsprozess zu starten. Du wirst dann zur Flattr-Anmeldeseite weitergeleitet, wo du gefragt wirst, AntennaPod die Erlaubnis zu geben, Dinge zu flattrn. Nachdem du die Erlaubnis erteilt hast, kehrst du automatisch zu diesem Bildschirm zurück.</string> + <string name="flattr_auth_explanation">Drücke den Button unten, um den Authentifizierungsprozess zu starten. Du wirst zur Flattr-Anmeldeseite weitergeleitet. Hier wirst du gefragt, AntennaPod die Erlaubnis zu geben, Dinge zu flattrn. Nachdem du die Erlaubnis erteilt hast, kehrst du automatisch zu diesem Bildschirm zurück.</string> <string name="authenticate_label">Authentifizieren</string> <string name="return_home_label">Zur Hauptseite zurückkehren</string> <string name="flattr_auth_success">Die Authentifizierung war erfolgreich! Du kannst nun in der Anwendung Flattr verwenden.</string> @@ -170,12 +175,12 @@ <string name="flattr_click_success_queue">Geflattrt: %s</string> <string name="flattr_click_failure_count">Flattrn von %d Sachen fehlgeschlagen!</string> <string name="flattr_click_failure">Nicht geflattrt: %s</string> - <string name="flattr_click_enqueued">Sache wird später gelfattrt</string> + <string name="flattr_click_enqueued">Sache wird später geflattrt</string> <string name="flattring_thing">Flattrt: %s</string> <string name="flattring_label">AntennaPod flattrt</string> <string name="flattrd_label">AntennaPod hat geflattrt</string> <string name="flattrd_failed_label">AntennaPod Flattrn fehlgeschlagen</string> - <string name="flattr_retrieving_status">Rufe geflatterte Sachen ab</string> + <string name="flattr_retrieving_status">Rufe geflattrte Sachen ab</string> <!--Variable Speed--> <string name="download_plugin_label">Plugin herunterladen</string> <string name="no_playback_plugin_title">Plugin nicht installiert</string> @@ -190,10 +195,10 @@ <string name="queue_label">Abspielliste</string> <string name="services_label">Dienste</string> <string name="flattr_label">Flattr</string> - <string name="pref_pauseOnHeadsetDisconnect_sum">Pausiere die Wiedergabe wenn der Kopfhörer entfernt worden ist.</string> - <string name="pref_unpauseOnHeadsetReconnect_sum">Wiedergabe fortsetzen wenn Kopfhörer wieder reingesteckt werden</string> - <string name="pref_followQueue_sum">Springe zur nächsten Episode wenn die vorherige Episode endet</string> - <string name="pref_auto_delete_sum">Episode löschen wenn Wiedergabe beendet</string> + <string name="pref_pauseOnHeadsetDisconnect_sum">Pausiere die Wiedergabe, wenn der Kopfhörer entfernt worden ist.</string> + <string name="pref_unpauseOnHeadsetReconnect_sum">Wiedergabe fortsetzen, wenn Kopfhörer wieder reingesteckt werden</string> + <string name="pref_followQueue_sum">Springe zur nächsten Episode, wenn die vorherige Episode endet</string> + <string name="pref_auto_delete_sum">Episode löschen, wenn die Wiedergabe endet</string> <string name="pref_auto_delete_title">Automatisches Löschen</string> <string name="playback_pref">Wiedergabe</string> <string name="network_pref">Netzwerk</string> @@ -203,7 +208,7 @@ <string name="pref_followQueue_title">Durchgehendes Abspielen</string> <string name="pref_downloadMediaOnWifiOnly_title">WiFi Medien-Download</string> <string name="pref_pauseOnHeadsetDisconnect_title">Kopfhörer-Trennung</string> - <string name="pref_unpauseOnHeadsetReconnect_title">Kopfhörer wieder reingesteckt</string> + <string name="pref_unpauseOnHeadsetReconnect_title">Kopfhörer wieder eingesteckt</string> <string name="pref_mobileUpdate_title">Mobile Aktualisierungen</string> <string name="pref_mobileUpdate_sum">Erlaube Aktualisierungen über die mobile Datenverbindung</string> <string name="refreshing_label">Aktualisiere</string> @@ -225,6 +230,7 @@ <string name="pref_autodl_wifi_filter_sum">Erlaube das automatische Herunterladen nur in ausgewählten W-LAN Netzwerken.</string> <string name="pref_automatic_download_on_battery_title">Automatischer Download im Batterie Modus</string> <string name="pref_automatic_download_on_battery_sum">Automatische Downloads auch erlauben, wenn die Batterie nicht geladen wird</string> + <string name="pref_parallel_downloads_title">Parallele Downloads</string> <string name="pref_episode_cache_title">Episodenspeicher</string> <string name="pref_theme_title_light">Hell</string> <string name="pref_theme_title_dark">Dunkel</string> @@ -233,11 +239,11 @@ <string name="pref_update_interval_hours_singular">Stunde</string> <string name="pref_update_interval_hours_manual">Manuell</string> <string name="pref_gpodnet_authenticate_title">Anmelden</string> - <string name="pref_gpodnet_authenticate_sum">Melde dich mit deinem gpodder.net profil an um deine Abonnements zu synchronisieren</string> + <string name="pref_gpodnet_authenticate_sum">Melde dich mit deinem gpodder.net Profil an, um deine Abonnements zu synchronisieren</string> <string name="pref_gpodnet_logout_title">Abmelden</string> <string name="pref_gpodnet_logout_toast">Abmeldung war erfolgreich</string> <string name="pref_gpodnet_setlogin_information_title">Anmeldeinformationen ändern</string> - <string name="pref_gpodnet_setlogin_information_sum">Ändere die Anmeldeinformationen deines gpodder.net profils</string> + <string name="pref_gpodnet_setlogin_information_sum">Ändere die Anmeldeinformationen deines gpodder.net Profils</string> <string name="pref_playback_speed_title">Wiedergabegeschwindigkeiten</string> <string name="pref_playback_speed_sum">Lege die verfügbaren Werte für die Veränderung der Wiedergabeschwindigkeit fest</string> <string name="pref_seek_delta_title">Spul-Zeit</string> @@ -249,9 +255,11 @@ <string name="pref_persistNotify_title">Persistente Wiedergabesteurung</string> <string name="pref_persistNotify_sum">Zeige Wiedergabebedienelemente in der Benachrichtigung und im Lockscreen an, während die Wiedergabe pausiert ist.</string> <string name="pref_expand_notify_unsupport_toast">Android-Versionen vor 4.1 unterstützen keine erweiterten Benachrichtigungen.</string> + <string name="pref_queueAddToFront_sum">Fügen Sie neue Folgen auf den Anfang der Warteschlange.</string> + <string name="pref_queueAddToFront_title">Vorne in Abspielliste einreihen</string> <!--Auto-Flattr dialog--> <string name="auto_flattr_enable">Automatisches Flattrn aktivieren</string> - <string name="auto_flattr_after_percent">Flattr eine Episode sobald %d Prozent gespielt worden sind</string> + <string name="auto_flattr_after_percent">Flattr eine Episode, sobald %d Prozent gespielt worden sind</string> <string name="auto_flattr_ater_beginning">Flattr Episode, sobald die Wiedergabe beginnt</string> <string name="auto_flattr_ater_end">Flattr Episode, sobald die Wiedergabe endet</string> <!--Search--> @@ -263,7 +271,9 @@ <string name="found_in_title_label">In Titel gefunden</string> <!--OPML import and export--> <string name="opml_import_txtv_button_lable">Mit OPML Dateien kannst du deine Podcasts von einem Podcatcher auf einen anderen übertragen</string> - <string name="opml_import_explanation">Um eine OPML Datei zu importieren, musst du diese im folgenden Ordner platzieren und den unteren Button antippen, um den Import Prozess zu starten.</string> + <string name="opml_import_explanation_1">Wähle einen bestimmten Dateipfad aus dem lokalen Dateisystem.</string> + <string name="opml_import_explanation_2">Verwende externe Anwendungen wie Dropbox, Google Drive oder deinen Lieblingsdateimanager, um eine OPML-Datei zu öffnen.</string> + <string name="opml_import_explanation_3">Viele Anwendungen wie Google Mail, Dropbox, Google Drive und die meisten Dateimanager können OPML-Dateien <i>mit</ i> AntennaPod <i>öffnen</i>.</string> <string name="start_import_label">Import starten</string> <string name="opml_import_label">OPML Import</string> <string name="opml_directory_error">FEHLER!</string> @@ -272,12 +282,13 @@ <string name="opml_import_error_dir_empty">Der Import-Ordner ist leer.</string> <string name="select_all_label">Alle auswählen</string> <string name="deselect_all_label">Auswahl zurücksetzen</string> - <string name="choose_file_to_import_label">Wähle eine Datei zum Importieren aus</string> + <string name="choose_file_from_filesystem">Vom lokalen Dateisystem</string> + <string name="choose_file_from_external_application">Verwende externe Anwendung</string> <string name="opml_export_label">OPML Export</string> <string name="exporting_label">Exportiere...</string> <string name="export_error_label">Exportfehler</string> <string name="opml_export_success_title">OPML Export erfolgreich</string> - <string name="opml_export_success_sum">Die .opml Datei wurde unter dem folgenden Pfad gespeichert:\u0020</string> + <string name="opml_export_success_sum">Die OPML Datei wurde unter dem folgenden Pfad gespeichert:\u0020</string> <!--Sleep timer--> <string name="set_sleeptimer_label">Schlummerfunktion</string> <string name="disable_sleeptimer_label">Schlummerfunktion deaktivieren</string> @@ -296,11 +307,11 @@ <string name="gpodnetauth_login_title">Anmeldung</string> <string name="gpodnetauth_login_descr">Willkommen beim gpodder.net Anmeldeprozess. Gib zuerst deine Anmeldeinformationen ein:</string> <string name="gpodnetauth_login_butLabel">Anmelden</string> - <string name="gpodnetauth_login_register">Falls du noch kein gpodder.net profil hast, kannst du hier eines erstellen: https://gpodder.net/register/</string> + <string name="gpodnetauth_login_register">Falls du noch kein gpodder.net Profil hast, kannst du hier eines erstellen: https://gpodder.net/register/</string> <string name="username_label">Benutzername</string> <string name="password_label">Passwort</string> <string name="gpodnetauth_device_title">Geräte-Auswahl</string> - <string name="gpodnetauth_device_descr">Erstelle ein neues Gerät für dein gpodder.net profil oder wähle ein bereits vorhandenes:</string> + <string name="gpodnetauth_device_descr">Erstelle ein neues Gerät für dein gpodder.net Profil oder wähle ein bereits vorhandenes:</string> <string name="gpodnetauth_device_deviceID">Geräte-ID:\u0020</string> <string name="gpodnetauth_device_caption">Beschreibung</string> <string name="gpodnetauth_device_butCreateNewDevice">Neues Gerät erstellen</string> @@ -309,7 +320,7 @@ <string name="gpodnetauth_device_errorAlreadyUsed">Geräte-ID wird bereits verwendet</string> <string name="gpodnetauth_device_butChoose">Auswählen</string> <string name="gpodnetauth_finish_title">Anmeldung erfolgreich!</string> - <string name="gpodnetauth_finish_descr">Glückwunsch! Dein gpodder.net profil ist jetzt mit deinem Gerät verbunden. Von nun an wird AntennaPod automatisch deine Abonnements mit deinem gpodder.net profil synchronisieren.</string> + <string name="gpodnetauth_finish_descr">Glückwunsch! Dein gpodder.net Profil ist jetzt mit deinem Gerät verbunden. Von nun an wird AntennaPod automatisch deine Abonnements mit deinem gpodder.net Profil synchronisieren.</string> <string name="gpodnetauth_finish_butsyncnow">Jetzt synchronisieren</string> <string name="gpodnetauth_finish_butgomainscreen">Zum Hauptbildschirm zurückkehren</string> <string name="gpodnetsync_auth_error_title">gpodder.net Anmeldefehler</string> @@ -328,7 +339,7 @@ <string name="folder_not_empty_dialog_title">Ordner ist nicht leer</string> <string name="folder_not_empty_dialog_msg">Der ausgewählte Ordner ist nicht leer. Medien-Downloads und andere Daten werden direkt in diesem Ordner gespeichert. Trotzdem fortfahren?</string> <string name="set_to_default_folder">Standardordner auswählen</string> - <string name="pref_pausePlaybackForFocusLoss_sum">Pausiere die Wiedergabe anstatt die Lautstärke zu reduzieren, wenn eine andere Anwendung Töne abspielt</string> + <string name="pref_pausePlaybackForFocusLoss_sum">Pausiere die Wiedergabe statt die Lautstärke zu reduzieren, wenn eine andere Anwendung Töne abspielt</string> <string name="pref_pausePlaybackForFocusLoss_title">Bei Unterbrechungen pausieren</string> <!--Online feed view--> <string name="subscribe_label">Abonnieren</string> @@ -348,7 +359,7 @@ <string name="status_downloading_label">Episode wird gerade heruntergeladen</string> <string name="status_downloaded_label">Episode ist heruntergeladen</string> <string name="status_unread_label">Eintrag ist neu</string> - <string name="in_queue_label">Episode befindet sich inder Abspielliste</string> + <string name="in_queue_label">Episode befindet sich in der Abspielliste</string> <string name="new_episodes_count_label">Anzahl neuer Episoden</string> <string name="in_progress_episodes_count_label">Anzahl der Episoden, die du angefangen hast zu hören</string> <string name="drag_handle_content_description">Ziehe, um die Position dieses Objekts zu verändern</string> @@ -358,4 +369,5 @@ <string name="authentication_descr">Ändere den Benutzernamen und das Passwort für diesen Podcast und dessen Episoden.</string> <!--AntennaPodSP--> <string name="sp_apps_importing_feeds_msg">Importiere Abonnements aus Single-Purpose Apps</string> + <string name="search_itunes_label">iTunes suchen</string> </resources> diff --git a/core/src/main/res/values-es-rES/strings.xml b/core/src/main/res/values-es-rES/strings.xml index cd4949530..48ff1570b 100644 --- a/core/src/main/res/values-es-rES/strings.xml +++ b/core/src/main/res/values-es-rES/strings.xml @@ -158,7 +158,6 @@ <string name="search_label">Buscar</string> <string name="found_in_title_label">Encontrado en el título</string> <!--OPML import and export--> - <string name="opml_import_explanation">Para importar un archivo OPML, debe copiarlo al siguiente directorio y pulsar el botón que se muestra abajo.</string> <string name="start_import_label">Comenzar la importación</string> <string name="opml_import_label">Importación de OPML</string> <string name="opml_directory_error">¡ERROR!</string> @@ -167,11 +166,9 @@ <string name="opml_import_error_dir_empty">El directorio de importación está vacío</string> <string name="select_all_label">Seleccionar todo</string> <string name="deselect_all_label">Deseleccionar todo</string> - <string name="choose_file_to_import_label">Elegir qué archivo importar</string> <string name="opml_export_label">Exportar a OPML</string> <string name="exporting_label">Exportando...</string> <string name="export_error_label">Error en la exportación</string> - <string name="opml_export_success_title">Exportación a OPML exitosa</string> <string name="opml_export_success_sum">El archivo OPML se ha escrito en:\u0020</string> <!--Sleep timer--> <string name="set_sleeptimer_label">Establecer un temporizador</string> diff --git a/core/src/main/res/values-es/strings.xml b/core/src/main/res/values-es/strings.xml index 00dbb628a..2dbe4e8d4 100644 --- a/core/src/main/res/values-es/strings.xml +++ b/core/src/main/res/values-es/strings.xml @@ -58,6 +58,7 @@ <string name="close_label">Cerrar</string> <string name="retry_label">Reintentar</string> <string name="auto_download_label">Incluir en descargas automáticas</string> + <string name="parallel_downloads_suffix">\u0020descargas paralelas</string> <!--'Add Feed' Activity labels--> <string name="feedurl_label">URL del canal</string> <string name="etxtFeedurlHint">URL del canal o del sitio web</string> @@ -68,6 +69,8 @@ <!--Actions on feeds--> <string name="mark_all_read_label">Marcar todo como leído</string> <string name="mark_all_read_msg">Se marcaron todos los episodios como leídos</string> + <string name="mark_all_read_confirmation_msg">Por favor, confirme que desea marcar todos los episodios como leídos.</string> + <string name="mark_all_read_feed_confirmation_msg">Por favor, confirme que desea marcar todos los episodios de este feed como leídos.</string> <string name="show_info_label">Información del programa</string> <string name="remove_feed_label">Eliminar podcast</string> <string name="share_link_label">Compartir el enlace de la web</string> @@ -85,6 +88,7 @@ <string name="remove_episode_lable">Quitar episodio</string> <string name="mark_read_label">Marcar como leído</string> <string name="mark_unread_label">Marcar como no leído</string> + <string name="marked_as_read_label">Marcado como leído</string> <string name="add_to_queue_label">Añadir a la cola</string> <string name="remove_from_queue_label">Quitar de la cola</string> <string name="visit_website_label">Visitar el sitio web</string> @@ -150,6 +154,7 @@ <string name="duration">Duración</string> <string name="ascending">Ascendente</string> <string name="descending">Descendente</string> + <string name="clear_queue_confirmation_msg">Por favor, confirme que desea borrar TODOS los episodios de la cola</string> <!--Flattr--> <string name="flattr_auth_label">Identificarse en Flattr</string> <string name="flattr_auth_explanation">Pulse el botón inferior para comenzar la autenticación. Su navegador abrirá la pantalla de identificación de Flattr y le preguntará si quiere conceder permiso a AntennaPod para valorar cosas. Tras concederlo, volverá a esta pantalla automáticamente.</string> @@ -225,6 +230,7 @@ <string name="pref_autodl_wifi_filter_sum">Permitir la descarga automática sólo para las redes WiFi marcadas.</string> <string name="pref_automatic_download_on_battery_title">Descargar cuando no se está cargando</string> <string name="pref_automatic_download_on_battery_sum">Permitir la descarga automática cuando la batería no está cargando</string> + <string name="pref_parallel_downloads_title">Descargas paralelas</string> <string name="pref_episode_cache_title">Caché de episodios</string> <string name="pref_theme_title_light">Claro</string> <string name="pref_theme_title_dark">Oscuro</string> @@ -249,6 +255,8 @@ <string name="pref_persistNotify_title">Controles de reproducción persistentes</string> <string name="pref_persistNotify_sum">Mantener la notificación y controles en pantalla de bloqueo cuando se pausa.</string> <string name="pref_expand_notify_unsupport_toast">Las versiones de Android anteriores a la 4.1 no soportan notificaciones expandidas</string> + <string name="pref_queueAddToFront_sum">Agregar nuevos episodios al principio de la cola.</string> + <string name="pref_queueAddToFront_title">Poner al principio de la cola.</string> <!--Auto-Flattr dialog--> <string name="auto_flattr_enable">Habilitar Flattr automático</string> <string name="auto_flattr_after_percent">Hacer Flattr del episodio en cuanto se haya reproducido el %d por ciento</string> @@ -263,7 +271,9 @@ <string name="found_in_title_label">Encontrado en el título</string> <!--OPML import and export--> <string name="opml_import_txtv_button_lable">Los archivos OPML le permiten migrar sus podcasts de una aplicación a otra.</string> - <string name="opml_import_explanation">Para importar un archivo OPML, debe copiarlo al siguiente directorio y pulsar el botón que se muestra abajo.</string> + <string name="opml_import_explanation_1">Elegir un una ruta del sistema de ficheros local.</string> + <string name="opml_import_explanation_2">Usar una aplicación externa tipo Dropbox, Google Drive or su gestor de ficheros favorito para abrir un archivo OPML.</string> + <string name="opml_import_explanation_3">Muchas aplicaciones como Google Mail, Dropbox, Google Drive y la mayoría de gestores de ficheros pueden <i>abrir</i> archivos OPML <i>con</i> AntennaPod.</string> <string name="start_import_label">Comenzar la importación</string> <string name="opml_import_label">Importación de OPML</string> <string name="opml_directory_error">¡ERROR!</string> @@ -272,7 +282,8 @@ <string name="opml_import_error_dir_empty">El directorio de importación está vacío.</string> <string name="select_all_label">Seleccionar todo</string> <string name="deselect_all_label">Deseleccionar todo</string> - <string name="choose_file_to_import_label">Elegir qué archivo importar</string> + <string name="choose_file_from_filesystem">Desde el sistema de ficheros local</string> + <string name="choose_file_from_external_application">Usar aplicación externa</string> <string name="opml_export_label">Exportar a OPML</string> <string name="exporting_label">Exportando...</string> <string name="export_error_label">Error en la exportación</string> @@ -358,4 +369,5 @@ <string name="authentication_descr">Cambiar nombre y contraseña de este podcast y sus episodios</string> <!--AntennaPodSP--> <string name="sp_apps_importing_feeds_msg">Importando subscripciones de aplicaciones de uso específico...</string> + <string name="search_itunes_label">Buscar en iTunes</string> </resources> diff --git a/core/src/main/res/values-fr/strings.xml b/core/src/main/res/values-fr/strings.xml index ada7e4e96..617df3f8f 100644 --- a/core/src/main/res/values-fr/strings.xml +++ b/core/src/main/res/values-fr/strings.xml @@ -58,6 +58,7 @@ <string name="close_label">Fermer</string> <string name="retry_label">Réessayer</string> <string name="auto_download_label">Télécharger automatiquement à l\'avenir</string> + <string name="parallel_downloads_suffix">\u0020téléchargements parallèles</string> <!--'Add Feed' Activity labels--> <string name="feedurl_label">URL du flux</string> <string name="etxtFeedurlHint">URL ou flux ou site web</string> @@ -68,6 +69,8 @@ <!--Actions on feeds--> <string name="mark_all_read_label">Tous marquer comme lus</string> <string name="mark_all_read_msg">Tous les épisodes ont été marqués comme lus</string> + <string name="mark_all_read_confirmation_msg">Veuillez confirmer que vous voulez bien marquer tous les épisodes comme lus</string> + <string name="mark_all_read_feed_confirmation_msg">Veuillez confirmer que vous voulez bien marquer tous les épisode de ce flux comme lus</string> <string name="show_info_label">Voir les détails</string> <string name="remove_feed_label">Supprimer le podcast</string> <string name="share_link_label">Partager un lien vers le site</string> @@ -85,6 +88,7 @@ <string name="remove_episode_lable">Supprimer cet épisode</string> <string name="mark_read_label">Marquer comme lu</string> <string name="mark_unread_label">Marquer comme non lu</string> + <string name="marked_as_read_label">Les épisodes ont été marqués comme lus</string> <string name="add_to_queue_label">Ajouter à la liste</string> <string name="remove_from_queue_label">Supprimer de la liste</string> <string name="visit_website_label">Visiter le site</string> @@ -150,6 +154,7 @@ <string name="duration">Durée</string> <string name="ascending">Ordre croissant</string> <string name="descending">Ordre décroissant</string> + <string name="clear_queue_confirmation_msg">Veuillez confirmer que vous voulez bien supprimer TOUS les épisodes de la file d\'attente</string> <!--Flattr--> <string name="flattr_auth_label">Connecter à Flattr</string> <string name="flattr_auth_explanation">Appuyez sur le bouton ci-dessous pour vous authentifier. Vous serez envoyés à l\'écran de connexion Flattr dans le navigateur, et il vous sera demandé de donner à AntennaPod la permission de flattr. Une fois ceci fait, vous reviendrez automatiquement à cet écran.</string> @@ -225,6 +230,7 @@ <string name="pref_autodl_wifi_filter_sum">Autoriser le téléchargement automatique uniquement sur les réseaux Wi-Fi sélectionnés.</string> <string name="pref_automatic_download_on_battery_title">Télécharger lorsque l\'appareil n\'est pas en charge</string> <string name="pref_automatic_download_on_battery_sum">Autoriser le téléchargement automatique quand l\'appareil n\'est pas en train de charger</string> + <string name="pref_parallel_downloads_title">Téléchargements simultanés</string> <string name="pref_episode_cache_title">Épisodes stockés localement</string> <string name="pref_theme_title_light">Clair</string> <string name="pref_theme_title_dark">Sombre</string> @@ -249,6 +255,8 @@ <string name="pref_persistNotify_title">Boutons de lecture permanents</string> <string name="pref_persistNotify_sum">Garder les notifications et les boutons de lecture sur l\'écran de verouillage quand la lecture est en pause</string> <string name="pref_expand_notify_unsupport_toast">Les versions d\'Android antérieures à 4.1 ne sont pas compatibles avec les notifications élargies</string> + <string name="pref_queueAddToFront_sum">Ajouter de nouveaux épisodes en tête de file</string> + <string name="pref_queueAddToFront_title">Mettre au début de la file d\'attente</string> <!--Auto-Flattr dialog--> <string name="auto_flattr_enable">Activer le paiement flattr automatique</string> <string name="auto_flattr_after_percent">Lancer un paiement flattr pour un épisode dès que %d de l\'épisode a été joué</string> @@ -263,7 +271,9 @@ <string name="found_in_title_label">Trouvé dans le titre</string> <!--OPML import and export--> <string name="opml_import_txtv_button_lable">Les fichiers OPML vous permettent de bouger vos podcasts d\'un logiciel à un autre.</string> - <string name="opml_import_explanation">Pour importer un fichier OPML, copiez-le dans le répertoire suivant, et appuyez sur le bouton ci-dessous pour l\'importer.</string> + <string name="opml_import_explanation_1">Choisir un chemin de fichier spécifique dans le système de fichiers local</string> + <string name="opml_import_explanation_2">Utiliser une application tierce comme Dropbox, Google Drive ou votre gestionnaire de fichier favori pour ouvrir un fichier OPML</string> + <string name="opml_import_explanation_3">De nombreuses applications comme Google Mail, Dropbox ou Google Drive et la plupart des gestionnaires de fichiers peuvent <i>ouvrir</i> les fichiers OPML <i>avec</i> AntennaPod.</string> <string name="start_import_label">Démarrer l\'importation</string> <string name="opml_import_label">Importation OPML</string> <string name="opml_directory_error">ERREUR !</string> @@ -272,7 +282,8 @@ <string name="opml_import_error_dir_empty">Le répertoire d\'importation est vide.</string> <string name="select_all_label">Tout choisir</string> <string name="deselect_all_label">Ne rien choisir</string> - <string name="choose_file_to_import_label">Choisir le fichier à importer</string> + <string name="choose_file_from_filesystem">Depuis le système de fichier local</string> + <string name="choose_file_from_external_application">Utiliser une application tierce</string> <string name="opml_export_label">Exportation OPML</string> <string name="exporting_label">Exportation en cours...</string> <string name="export_error_label">Erreur d\'exportation</string> @@ -358,4 +369,5 @@ <string name="authentication_descr">Modifier votre identifiant et mot de passe pour ce podcast et tous ses épisodes</string> <!--AntennaPodSP--> <string name="sp_apps_importing_feeds_msg">Importation des abonnements à partir d\'applications à usage unique...</string> + <string name="search_itunes_label">Chercher sur iTunes</string> </resources> diff --git a/core/src/main/res/values-hi-rIN/strings.xml b/core/src/main/res/values-hi-rIN/strings.xml index b1d6c33b1..7a43ba15b 100644 --- a/core/src/main/res/values-hi-rIN/strings.xml +++ b/core/src/main/res/values-hi-rIN/strings.xml @@ -208,7 +208,6 @@ <string name="found_in_title_label">शीर्षक में मिला</string> <!--OPML import and export--> <string name="opml_import_txtv_button_lable">OPML फ़ाइलें आपको एक podcatcher से दूसरे को अपने पॉडकास्ट स्थानांतरित करने के लिए अनुमति देते हैं.</string> - <string name="opml_import_explanation">एक OPML फ़ाइल आयात करने के लिए, आपको इसे निम्नलिखित निर्देशिका में डालना है और आयात की प्रक्रिया शुरू करने के लिए नीचे दिए गए बटन को प्रेस करना है.</string> <string name="start_import_label">आयात प्रारंभ</string> <string name="opml_import_label">OPML आयात</string> <string name="opml_directory_error">त्रुटि!</string> @@ -217,11 +216,9 @@ <string name="opml_import_error_dir_empty">आयात निर्देशिका खाली है.</string> <string name="select_all_label">सभी का चयन करें</string> <string name="deselect_all_label">सभी का चयन रद्द करें</string> - <string name="choose_file_to_import_label">आयात करने के लिए फ़ाइल चुनें</string> <string name="opml_export_label">OPML निर्यात</string> <string name="exporting_label">निर्यात ...</string> <string name="export_error_label">निर्यात त्रुटि</string> - <string name="opml_export_success_title">OPML निर्यात सफल.</string> <string name="opml_export_success_sum">.ompl फ़ाइल लिखा गया था:\u0020</string> <!--Sleep timer--> <string name="set_sleeptimer_label">स्लीप टाइमर सेट</string> diff --git a/core/src/main/res/values-it-rIT/strings.xml b/core/src/main/res/values-it-rIT/strings.xml index e91029724..69bab7326 100644 --- a/core/src/main/res/values-it-rIT/strings.xml +++ b/core/src/main/res/values-it-rIT/strings.xml @@ -60,7 +60,7 @@ <string name="auto_download_label">Includi nei download automatici</string> <!--'Add Feed' Activity labels--> <string name="feedurl_label">URL del feed</string> - <string name="etxtFeedurlHint">URL del feed o del sito web</string> + <string name="etxtFeedurlHint">www.example.com/feed</string> <string name="txtvfeedurl_label">Aggiungi un Podcast tramite URL</string> <string name="podcastdirectories_label">Trova un podcast nella directory</string> <string name="podcastdirectories_descr">Puoi cercare dei nuovi podcast in base al nome, alla categoria o alla popolarità nella directory di gpodder.net.</string> @@ -85,6 +85,7 @@ <string name="remove_episode_lable">Rimuovi l\'episodio</string> <string name="mark_read_label">Segna come letto</string> <string name="mark_unread_label">Segna come non letto</string> + <string name="marked_as_read_label">Segnato come letto</string> <string name="add_to_queue_label">Aggiungi alla coda</string> <string name="remove_from_queue_label">Rimuovi dalla coda</string> <string name="visit_website_label">Visita il sito</string> @@ -225,6 +226,7 @@ <string name="pref_autodl_wifi_filter_sum">Abilita il download automatico solo per alcune reti Wi-Fi selezionate.</string> <string name="pref_automatic_download_on_battery_title">Scarica quando la batteria non è in carica</string> <string name="pref_automatic_download_on_battery_sum">Permetti il download automatico quando la batteria non è in carica</string> + <string name="pref_parallel_downloads_title">Download paralleli</string> <string name="pref_episode_cache_title">Cache degli episodi</string> <string name="pref_theme_title_light">Light</string> <string name="pref_theme_title_dark">Dark</string> @@ -263,7 +265,6 @@ <string name="found_in_title_label">Trovato nel titolo</string> <!--OPML import and export--> <string name="opml_import_txtv_button_lable">I file OPML ti permettono di spostare i tuoi podcast da un programma ad un altro.</string> - <string name="opml_import_explanation">Per importare un file OPML devi posizionarlo nella directory indicata e premere il tasto seguente in modo da iniziare il processo di importazione.</string> <string name="start_import_label">Avvio importazione</string> <string name="opml_import_label">Importazione OPML</string> <string name="opml_directory_error">ERRORE!</string> @@ -272,7 +273,6 @@ <string name="opml_import_error_dir_empty">La directory di importazione è vuota.</string> <string name="select_all_label">Seleziona tutti</string> <string name="deselect_all_label">Deseleziona tutti</string> - <string name="choose_file_to_import_label">Scegli il file da importare</string> <string name="opml_export_label">Esportazione su OPML</string> <string name="exporting_label">Esportazione in corso...</string> <string name="export_error_label">Errore di esportazione</string> @@ -358,4 +358,5 @@ <string name="authentication_descr">Cambia il tuo nome utente e la tua password per questo podcast e i suoi episodi.</string> <!--AntennaPodSP--> <string name="sp_apps_importing_feeds_msg">Importazione di sottoscrizioni da applicazioni monouso in corso...</string> + <string name="search_itunes_label">Cerca su iTunes</string> </resources> diff --git a/core/src/main/res/values-iw-rIL/strings.xml b/core/src/main/res/values-iw-rIL/strings.xml index f014954e4..b52bb4144 100644 --- a/core/src/main/res/values-iw-rIL/strings.xml +++ b/core/src/main/res/values-iw-rIL/strings.xml @@ -58,6 +58,7 @@ <string name="close_label">סגור</string> <string name="retry_label">נסה שוב</string> <string name="auto_download_label">כלול בהורדות אוטומטיות</string> + <string name="parallel_downloads_suffix">\u0020הורדות במקביל</string> <!--'Add Feed' Activity labels--> <string name="feedurl_label">כתובת הזנה</string> <string name="etxtFeedurlHint">כתובת של הזנה או אתר אינטרנט</string> @@ -68,6 +69,8 @@ <!--Actions on feeds--> <string name="mark_all_read_label">סמן הכל כנקרא</string> <string name="mark_all_read_msg">סמן את כל הפרקים כנקרא</string> + <string name="mark_all_read_confirmation_msg">אנא אשר שאתה רוצה לסמן את כל פרקים כנקראים.</string> + <string name="mark_all_read_feed_confirmation_msg">אנא אשר שאתה רוצה לסמן את כל פרקים בהזנה זו כנקראים.</string> <string name="show_info_label">הצג מידע</string> <string name="remove_feed_label">הסר פודקאסט</string> <string name="share_link_label">שתף קישור אתר</string> @@ -85,6 +88,7 @@ <string name="remove_episode_lable">הסר פרק</string> <string name="mark_read_label">סמן כנקרא</string> <string name="mark_unread_label">סמן כלא נקרא</string> + <string name="marked_as_read_label">סומן כנקרא</string> <string name="add_to_queue_label">הוסף לתור</string> <string name="remove_from_queue_label">הסר מהתור</string> <string name="visit_website_label">בקר באתר</string> @@ -150,6 +154,7 @@ <string name="duration">משך</string> <string name="ascending">בסדר עולה</string> <string name="descending">בסדר יורד</string> + <string name="clear_queue_confirmation_msg">אנא אשר שאתה רוצה לנקות את התור מכל הפרקים שבו</string> <!--Flattr--> <string name="flattr_auth_label">כניסה ל-Fattr</string> <string name="flattr_auth_explanation">לחץ על הכפתור למטה כדי להתחיל את תהליך האימות. אתה תועבר למסך כניסת flattr בדפדפן שלך ותתבקש לתת לאנטנה-פוד רשות לתרום באמצעות flattr. לאחר שקבלת אישור, תוכל לחזור למסך זה באופן אוטומטי.</string> @@ -226,6 +231,7 @@ <string name="pref_autodl_wifi_filter_sum">אפשר הורדה אוטומטית דרך רשתות אלחוטייות נבחרות.</string> <string name="pref_automatic_download_on_battery_title">הורדה כשלא טוען</string> <string name="pref_automatic_download_on_battery_sum">אפשר הורדה אוטומטית כשהסוללה אינה נטענת</string> + <string name="pref_parallel_downloads_title">הורדות במקביל</string> <string name="pref_episode_cache_title">מטמון פרקים</string> <string name="pref_theme_title_light">בהיר</string> <string name="pref_theme_title_dark">כהה</string> @@ -250,6 +256,8 @@ <string name="pref_persistNotify_title">פקדי הפעלה קבועים</string> <string name="pref_persistNotify_sum">שמר בקרי הודעה ומסך נעילה בעת השהיית השמעה.</string> <string name="pref_expand_notify_unsupport_toast">גרסאות אנדרויד לפני 4.1 לא תומכות בהודעות מורחבות.</string> + <string name="pref_queueAddToFront_sum">הוסף פרקים חדשים לראש התור.</string> + <string name="pref_queueAddToFront_title">הוסף לראש התור.</string> <!--Auto-Flattr dialog--> <string name="auto_flattr_enable">הפעל תרומות flattr אוטומטיות</string> <string name="auto_flattr_after_percent">תרום באמצעות flattr כשנוגן %d אחוזים מהפרק</string> @@ -264,7 +272,9 @@ <string name="found_in_title_label">נמצא בכותרת</string> <!--OPML import and export--> <string name="opml_import_txtv_button_lable">קבצי OPML יאפשרו לכך לנייד פודקאסטים מלוכד פודקאסטים אחד למשנו.</string> - <string name="opml_import_explanation">לייבא קובץ OPML, אתה צריך למקם אותו בספרייה הבאה וללחוץ על הכפתור למטה כדי להתחיל את תהליך היבוא.</string> + <string name="opml_import_explanation_1">בחר נתיב קובץ ספציפי במערכת הקבצים המקומית.</string> + <string name="opml_import_explanation_2">השתמש ביישומים חיצוניים כמו Dropbox, Google Drive או מנהל הקבצים האהוב עליך לפתוח קובץ OPML.</string> + <string name="opml_import_explanation_3">יישומים רבים כמו Google Mail, Dropbox, Google Drive ורוב מנהלי הקבצים יכולים <i>לפתוח</i> קבצי OPML <i>עם</i> אנטנה-פוד.</string> <string name="start_import_label">התחל יבוא</string> <string name="opml_import_label">יבוא OPML</string> <string name="opml_directory_error">שגיאה!</string> @@ -273,7 +283,8 @@ <string name="opml_import_error_dir_empty">ספריית היבוא ריקה.</string> <string name="select_all_label">בחר הכל</string> <string name="deselect_all_label">בטל בחירות</string> - <string name="choose_file_to_import_label">בחר קובץ ליבוא</string> + <string name="choose_file_from_filesystem">ממערכת הקבצים המקומית</string> + <string name="choose_file_from_external_application">השתמש באפליקציה חיצונית</string> <string name="opml_export_label">יצוא OPML</string> <string name="exporting_label">מייצא...</string> <string name="export_error_label">שגיאת יצוא</string> @@ -359,4 +370,5 @@ <string name="authentication_descr">שנה את שם המשתמש והסיסמה שלך לפודקאסט ופרקים שלו.</string> <!--AntennaPodSP--> <string name="sp_apps_importing_feeds_msg">מייבא רישום מאפליקציות יעודיות...</string> + <string name="search_itunes_label">חפש בiTunes</string> </resources> diff --git a/core/src/main/res/values-ja/strings.xml b/core/src/main/res/values-ja/strings.xml index f17e1cf61..df73db23e 100644 --- a/core/src/main/res/values-ja/strings.xml +++ b/core/src/main/res/values-ja/strings.xml @@ -58,6 +58,7 @@ <string name="close_label">閉じる</string> <string name="retry_label">再試行</string> <string name="auto_download_label">自動ダウンロードに含む</string> + <string name="parallel_downloads_suffix">\u0020パラレル ダウンロード</string> <!--'Add Feed' Activity labels--> <string name="feedurl_label">フィードURL</string> <string name="etxtFeedurlHint">フィードまたはWebサイトのURL</string> @@ -68,6 +69,8 @@ <!--Actions on feeds--> <string name="mark_all_read_label">既読としてマーク</string> <string name="mark_all_read_msg">すべてのエピソードを既読にしました</string> + <string name="mark_all_read_confirmation_msg">既読としてマークするすべてのエピソードを確認してください。</string> + <string name="mark_all_read_feed_confirmation_msg">既読としてマークするこのフィードのすべてのエピソードを確認してください。</string> <string name="show_info_label">情報を表示</string> <string name="remove_feed_label">ポッドキャストを削除</string> <string name="share_link_label">Webサイトのリンクを共有</string> @@ -85,6 +88,7 @@ <string name="remove_episode_lable">エピソードを削除</string> <string name="mark_read_label">既読にする</string> <string name="mark_unread_label">未読にする</string> + <string name="marked_as_read_label">既読</string> <string name="add_to_queue_label">キューに追加</string> <string name="remove_from_queue_label">キューから削除</string> <string name="visit_website_label">Webサイトを訪問</string> @@ -150,6 +154,7 @@ <string name="duration">継続時間</string> <string name="ascending">昇順</string> <string name="descending">降順</string> + <string name="clear_queue_confirmation_msg">クリアする、キューに含まれるすべてのエピソードを確認してください。</string> <!--Flattr--> <string name="flattr_auth_label">Flattrにサインイン</string> <string name="flattr_auth_explanation">認証処理を開始するには、下のボタンを押します。お使いのブラウザでflattrのログイン画面に転送され、AntennaPodにflattrする許可を与えるように求められます。あなたが許可を与えた後、自動的にこの画面に戻ります。</string> @@ -225,6 +230,7 @@ <string name="pref_autodl_wifi_filter_sum">選択したWi-Fiネットワークに対してのみ自動ダウンロードを許可します。</string> <string name="pref_automatic_download_on_battery_title">充電中以外の時にダウンロード</string> <string name="pref_automatic_download_on_battery_sum">バッテリーを充電していない時に自動ダウンロードを許可します</string> + <string name="pref_parallel_downloads_title">パラレル ダウンロード</string> <string name="pref_episode_cache_title">エピソードキャッシュ</string> <string name="pref_theme_title_light">ライト</string> <string name="pref_theme_title_dark">ダーク</string> @@ -249,6 +255,8 @@ <string name="pref_persistNotify_title">永続再生コントロール</string> <string name="pref_persistNotify_sum">再生が一時停止された時に、通知およびロック画面のコントロールを保持します。</string> <string name="pref_expand_notify_unsupport_toast">Androidバージョン4.1以前では、拡張通知をサポートしていません。</string> + <string name="pref_queueAddToFront_sum">新しいエピソードをキューの先頭に追加します。</string> + <string name="pref_queueAddToFront_title">キューの先頭に入れる</string> <!--Auto-Flattr dialog--> <string name="auto_flattr_enable">自動Flattrを有効にする</string> <string name="auto_flattr_after_percent">%d %再生したらエピソードをFlattr </string> @@ -263,7 +271,9 @@ <string name="found_in_title_label">タイトルで見つかりました</string> <!--OPML import and export--> <string name="opml_import_txtv_button_lable">OPMLファイルで、あるポッドキャッチャーから別のものにポッドキャストを移動することができます。</string> - <string name="opml_import_explanation">OPMLファイルをインポートするには、次のディレクトリにファイルを置き、以下のボタンを押してインポート処理を開始します。</string> + <string name="opml_import_explanation_1">ローカルのファイルシステムから指定するファイルパスを選択してください。</string> + <string name="opml_import_explanation_2">Dropbox、Google ドライブ、またはお好みのファイルマネージャなどの外部アプリケーションを使用して、OPML ファイルを開いてください。</string> + <string name="opml_import_explanation_3">Google メール、Dropbox、Google ドライブ、および多くのファイルマネージャーなどのアプリケーションで、AntennaPod <i>の</i> OPML ファイルを <i>開く</i> ことができます。</string> <string name="start_import_label">インポート開始</string> <string name="opml_import_label">OPMLインポート</string> <string name="opml_directory_error">エラー!</string> @@ -272,7 +282,8 @@ <string name="opml_import_error_dir_empty">インポートディレクトリが空です。</string> <string name="select_all_label">すべてを選択</string> <string name="deselect_all_label">選択解除</string> - <string name="choose_file_to_import_label">インポートするファイルを選択してください</string> + <string name="choose_file_from_filesystem">ローカル ファイルシステムから</string> + <string name="choose_file_from_external_application">外部アプリケーションを使用する</string> <string name="opml_export_label">OPMLエクスポート</string> <string name="exporting_label">エクスポート中...</string> <string name="export_error_label">エクスポートエラー</string> @@ -358,4 +369,5 @@ <string name="authentication_descr">このポッドキャストとそのエピソード用のあなたのユーザー名とパスワードを変更します。</string> <!--AntennaPodSP--> <string name="sp_apps_importing_feeds_msg">単一目的のアプリから購読をインポート中…</string> + <string name="search_itunes_label">iTunes を検索</string> </resources> diff --git a/core/src/main/res/values-ko/strings.xml b/core/src/main/res/values-ko/strings.xml index b4fa23711..7132031e4 100644 --- a/core/src/main/res/values-ko/strings.xml +++ b/core/src/main/res/values-ko/strings.xml @@ -58,16 +58,19 @@ <string name="close_label">닫기</string> <string name="retry_label">다시 시도</string> <string name="auto_download_label">자동 다운로드에 포함</string> + <string name="parallel_downloads_suffix">\u0020동시 다운로드</string> <!--'Add Feed' Activity labels--> <string name="feedurl_label">피드 URL</string> <string name="etxtFeedurlHint">피드의 URL 또는 홈페이지</string> <string name="txtvfeedurl_label">URL로 팟캐스트를 추가</string> <string name="podcastdirectories_label">디렉터리에서 팟캐스트 찾기</string> - <string name="podcastdirectories_descr">gpodder.net 디렉터리에서 이름, 분류, 인기에 따라 새 팟캐스트를 검색할 수 있습니다</string> + <string name="podcastdirectories_descr">gpodder.net 디렉터리에서 이름, 분류, 인기에 따라 새 팟캐스트를 검색할 수 있고, iTunes 스토어에서 검색할 수도 있습니다.</string> <string name="browse_gpoddernet_label">gpodder.net 둘러보기</string> <!--Actions on feeds--> <string name="mark_all_read_label">모두 읽은 것으로 표시</string> <string name="mark_all_read_msg">모든 에피소드 읽은 것으로 표시</string> + <string name="mark_all_read_confirmation_msg">에피소드 모두를 읽은 것으로 표시하는지 확인하십시오.</string> + <string name="mark_all_read_feed_confirmation_msg">이 피드의 에피소드 모두를 읽은 것으로 표시하는지 확인하십시오.</string> <string name="show_info_label">정보 표시</string> <string name="remove_feed_label">팟캐스트 제거</string> <string name="share_link_label">홈페이지 링크 공유</string> @@ -85,6 +88,7 @@ <string name="remove_episode_lable">에피소드 제거</string> <string name="mark_read_label">읽은 것으로 표시</string> <string name="mark_unread_label">읽지 않은 것으로 표시</string> + <string name="marked_as_read_label">읽은 것으로 표시</string> <string name="add_to_queue_label">대기열에 추가</string> <string name="remove_from_queue_label">대기열에서 제거</string> <string name="visit_website_label">홈페이지 보기</string> @@ -150,6 +154,7 @@ <string name="duration">기간</string> <string name="ascending">오름차순</string> <string name="descending">내림차순</string> + <string name="clear_queue_confirmation_msg">내부의 모든 에피소드 대기열을 지울지 확인하십시오.</string> <!--Flattr--> <string name="flattr_auth_label">Flattr 로그인</string> <string name="flattr_auth_explanation">인증 절차를 시작하려면 아래 버튼을 누르십시오. 브라우저의 Flattr 로그인 화면으로 이동하고, 안테나팟에 Flattr를 사용을 허락 여부를 물어봅니다. 허락을 하면 자동으로 이 화면으로 돌아옵니다.</string> @@ -225,6 +230,7 @@ <string name="pref_autodl_wifi_filter_sum">선택한 Wi-Fi 네트워크에 대해서만 자동 다운로드를 허용합니다.</string> <string name="pref_automatic_download_on_battery_title">충전하지 않을 때 다운로드</string> <string name="pref_automatic_download_on_battery_sum">배터리 충전 중이 아닐 때 자동 다운로드 허용</string> + <string name="pref_parallel_downloads_title">동시 다운로드</string> <string name="pref_episode_cache_title">에피소드 임시 저장</string> <string name="pref_theme_title_light">밝게</string> <string name="pref_theme_title_dark">어둡게</string> @@ -249,6 +255,8 @@ <string name="pref_persistNotify_title">재생 조작 고정</string> <string name="pref_persistNotify_sum">재생이 일시 중지했을 때에도 알림과 잠금 화면의 조작 기능 유지</string> <string name="pref_expand_notify_unsupport_toast">안드로이드 4.1 전 버전에서는 알림 확장을 지원하지 않습니다.</string> + <string name="pref_queueAddToFront_sum">새 에피소드를 대기열 앞에 추가합니다.</string> + <string name="pref_queueAddToFront_title">대기열 앞에 추가</string> <!--Auto-Flattr dialog--> <string name="auto_flattr_enable">자동 flattr 사용</string> <string name="auto_flattr_after_percent">%d 퍼센트를 재생하면 에피소드에 flattr합니다</string> @@ -263,7 +271,9 @@ <string name="found_in_title_label">제목에서 발견</string> <!--OPML import and export--> <string name="opml_import_txtv_button_lable">OPML 파일을 이용하면 팟캐스트 목록을 한 팟캐스트 프로그램에서 다른 팟캐스트 프로그램으로 옮길 수 있습니다.</string> - <string name="opml_import_explanation">OPML 파일을 가져오려면, 다음 디렉터리에 파일을 저장하고 아래 버튼을 누르면 가져오기 처리를 시작합니다.</string> + <string name="opml_import_explanation_1">로컬 파일시스템의 특정 파일 경로를 선택하십시오.</string> + <string name="opml_import_explanation_2">OPML 파일을 여는데 Dropbox, Google Drive, 또는 파일 관리자 와 같은 외부 앱을 사용합니다.</string> + <string name="opml_import_explanation_3">Google Mail, Dropbox, Google Drive 및 대부분의 파일 관리자는 OPML 파일을 안테나팟<i>으로</i> <i>열 수</i> 있습니다.</string> <string name="start_import_label">가져오기 시작</string> <string name="opml_import_label">OPML 가져오기</string> <string name="opml_directory_error">오류!</string> @@ -272,7 +282,8 @@ <string name="opml_import_error_dir_empty">가져오기 디렉터리가 비어 있습니다.</string> <string name="select_all_label">모두 선택</string> <string name="deselect_all_label">모두 선택 해제</string> - <string name="choose_file_to_import_label">가져올 파일을 고르십시오</string> + <string name="choose_file_from_filesystem">로컬 파일시스템에서</string> + <string name="choose_file_from_external_application">외부 앱 사용</string> <string name="opml_export_label">OPML 내보내기</string> <string name="exporting_label">내보내는 중...</string> <string name="export_error_label">내보내기 오류</string> @@ -358,4 +369,5 @@ <string name="authentication_descr">이 팟캐스트와 에피소드에 대한 사용자 이름과 비밀번호를 바꿉니다.</string> <!--AntennaPodSP--> <string name="sp_apps_importing_feeds_msg">단일 용도 앱에서 구독 정보를 가져옵니다...</string> + <string name="search_itunes_label">iTunes 검색</string> </resources> diff --git a/core/src/main/res/values-nl/strings.xml b/core/src/main/res/values-nl/strings.xml index 9df199324..66ffaaf87 100644 --- a/core/src/main/res/values-nl/strings.xml +++ b/core/src/main/res/values-nl/strings.xml @@ -213,7 +213,6 @@ <string name="found_in_title_label">Gevonden in de titel</string> <!--OPML import and export--> <string name="opml_import_txtv_button_lable">Met OPML-bestanden kan je podcasts van de ene naar de andere podcatcher verplaatsen.</string> - <string name="opml_import_explanation">Om een OPML-bestand te importeren moet je het in de volgende map zetten en op onderstaande knop drukken.</string> <string name="start_import_label">Start importeren</string> <string name="opml_import_label">OPML import</string> <string name="opml_directory_error">FOUT!</string> @@ -222,11 +221,9 @@ <string name="opml_import_error_dir_empty">De import map is leeg.</string> <string name="select_all_label">Selecteer alles</string> <string name="deselect_all_label">Deselecteer alles</string> - <string name="choose_file_to_import_label">Kies het te importeren bestand</string> <string name="opml_export_label">OPML export</string> <string name="exporting_label">Aan het exporteren...</string> <string name="export_error_label">Export fout</string> - <string name="opml_export_success_title">OPML export succesvol.</string> <string name="opml_export_success_sum"> Het OPML-bestand is in \u0020 geplaatst</string> <!--Sleep timer--> <string name="set_sleeptimer_label">Slaap timer instellen</string> diff --git a/core/src/main/res/values-pl-rPL/strings.xml b/core/src/main/res/values-pl-rPL/strings.xml index bf9a640ce..6e5c2ce44 100644 --- a/core/src/main/res/values-pl-rPL/strings.xml +++ b/core/src/main/res/values-pl-rPL/strings.xml @@ -261,7 +261,6 @@ <string name="found_in_title_label">Znaleziono w tytułach</string> <!--OPML import and export--> <string name="opml_import_txtv_button_lable">Pliki OPML pozwalają na przenoszenie podcastów między aplikacjami.</string> - <string name="opml_import_explanation">W celu importu pliku OPML musisz umieścić go w poniższym folderze i nacisnąć przycisk poniżej w celu rozpoczęcia importu.</string> <string name="start_import_label">Rozpocznij import</string> <string name="opml_import_label">Import OPML</string> <string name="opml_directory_error">BŁĄD!</string> @@ -270,7 +269,6 @@ <string name="opml_import_error_dir_empty">Katalog importowania jest pusty.</string> <string name="select_all_label">Zaznacz wszystko</string> <string name="deselect_all_label">Odznacz wszystko</string> - <string name="choose_file_to_import_label">Wybierz plik do importu</string> <string name="opml_export_label">Eksport OPML</string> <string name="exporting_label">Eksportowanie...</string> <string name="export_error_label">Błąd eksportu</string> diff --git a/core/src/main/res/values-pt-rBR/strings.xml b/core/src/main/res/values-pt-rBR/strings.xml index ef63e718e..aba186c1a 100644 --- a/core/src/main/res/values-pt-rBR/strings.xml +++ b/core/src/main/res/values-pt-rBR/strings.xml @@ -198,7 +198,6 @@ <string name="found_in_title_label">Encontrado no título</string> <!--OPML import and export--> <string name="opml_import_txtv_button_lable">Arquivos OPML permitem que você mova seus podcasts de um programa de podcasts para outro.</string> - <string name="opml_import_explanation">Para importar um arquivo OPML, você precisa armazená-lo neste diretório e pressionar o botão abaixo para iniciar o processo de importação.</string> <string name="start_import_label">Iniciar importação</string> <string name="opml_import_label">Importação de OPML</string> <string name="opml_directory_error">ERRO!</string> @@ -207,11 +206,9 @@ <string name="opml_import_error_dir_empty">O diretório de importação está vazio.</string> <string name="select_all_label">Selecionar todos</string> <string name="deselect_all_label">Remover seleção</string> - <string name="choose_file_to_import_label">Escolher arquivo para importar</string> <string name="opml_export_label">Exportar OPML</string> <string name="exporting_label">Exportando...</string> <string name="export_error_label">Erro na exportação</string> - <string name="opml_export_success_title">OMPL exportado com sucesso</string> <string name="opml_export_success_sum">O arquivo .opml foi gravado em:\u0020</string> <!--Sleep timer--> <string name="set_sleeptimer_label">Configura desligamento automático</string> diff --git a/core/src/main/res/values-pt/strings.xml b/core/src/main/res/values-pt/strings.xml index 7df1b35f4..9ef8474ee 100644 --- a/core/src/main/res/values-pt/strings.xml +++ b/core/src/main/res/values-pt/strings.xml @@ -58,16 +58,19 @@ <string name="close_label">Fechar</string> <string name="retry_label">Tentar novamente</string> <string name="auto_download_label">Incluir nas transferências automáticas</string> + <string name="parallel_downloads_suffix">\u0020transferências simultâneas</string> <!--'Add Feed' Activity labels--> <string name="feedurl_label">URL da fonte</string> <string name="etxtFeedurlHint">URL da fonte ou sítio web</string> <string name="txtvfeedurl_label">Adicionar podcast via URL</string> <string name="podcastdirectories_label">Localizar podcasts no diretório</string> - <string name="podcastdirectories_descr">Pode procurar os novos podcasts no gPodder.net por nome, categoria ou popularidade.</string> + <string name="podcastdirectories_descr">Pode procurar os novos podcasts no gPodder.net por nome, categoria ou popularidade e também na loja iTunes.</string> <string name="browse_gpoddernet_label">Procurar no gPodder.net</string> <!--Actions on feeds--> <string name="mark_all_read_label">Marcar tudo como lido</string> <string name="mark_all_read_msg">Marcar todos os episódios como lidos</string> + <string name="mark_all_read_confirmation_msg">Por favor confirme que deseja marcar todos os episódios como lidos.</string> + <string name="mark_all_read_feed_confirmation_msg">Por favor confirme que deseja marcar todos os episódios desta fonte como lidos.</string> <string name="show_info_label">Mostrar informações</string> <string name="remove_feed_label">Remover podcast</string> <string name="share_link_label">Partilhar ligação do sítio web</string> @@ -85,6 +88,7 @@ <string name="remove_episode_lable">Remover episódio</string> <string name="mark_read_label">Marcar como lido</string> <string name="mark_unread_label">Marcar como novo</string> + <string name="marked_as_read_label">Marcar como lido</string> <string name="add_to_queue_label">Adicionar à fila</string> <string name="remove_from_queue_label">Remover da fila</string> <string name="visit_website_label">Aceder ao sítio web</string> @@ -150,6 +154,7 @@ <string name="duration">Duração</string> <string name="ascending">Crescente</string> <string name="descending">Decrescente</string> + <string name="clear_queue_confirmation_msg">Por favor confirme que deseja limpar todos os episódios da fila de reprodução.</string> <!--Flattr--> <string name="flattr_auth_label">Sessão Flattr</string> <string name="flattr_auth_explanation">Prima o botão abaixo para iniciar a autenticação. O seu navegador web abrirá o ecrã da sessão flattr e ser-lhe-á solicitada a permissão para o AntennaPod efetuar as alterações. Após ser dada a permissão, voltará novamente a este ecrã.</string> @@ -225,6 +230,7 @@ <string name="pref_autodl_wifi_filter_sum">Apenas permitir transferências automáticas através de redes sem fios.</string> <string name="pref_automatic_download_on_battery_title">Transferência se não estiver a carregar</string> <string name="pref_automatic_download_on_battery_sum">Permitir transferência automática se a bateria não estiver a ser carregada</string> + <string name="pref_parallel_downloads_title">Transferências simultâneas</string> <string name="pref_episode_cache_title">Cache de episódios</string> <string name="pref_theme_title_light">Claro</string> <string name="pref_theme_title_dark">Escuro</string> @@ -249,6 +255,8 @@ <string name="pref_persistNotify_title">Controlos de reprodução persistentes</string> <string name="pref_persistNotify_sum">Manter controlos de notificação e ecrã de bloqueio ao colocar a reprodução em pausa.</string> <string name="pref_expand_notify_unsupport_toast">As versões Android anteriores à 4.1 não possuem suporte à expansão de notificações.</string> + <string name="pref_queueAddToFront_sum">Colocar novos episódios no inicio da fila.</string> + <string name="pref_queueAddToFront_title">Novos episódios no inicio</string> <!--Auto-Flattr dialog--> <string name="auto_flattr_enable">Ativar flattr automático</string> <string name="auto_flattr_after_percent">Flattr de episódios ao atingir %d porcento de reprodução</string> @@ -263,7 +271,9 @@ <string name="found_in_title_label">Encontrado no título</string> <!--OPML import and export--> <string name="opml_import_txtv_button_lable">Os ficheiros OPML permitem-lhe mover os podcasts entre aplicações.</string> - <string name="opml_import_explanation">Para importar um ficheiro OPML, tem que o colocar neste diretório e premir o botão abaixo para iniciar o processo.</string> + <string name="opml_import_explanation_1">Escolha um caminho especifico no sistema local de ficheiros.</string> + <string name="opml_import_explanation_2">Utilize aplicações externas como o Dropbox, Google Drive ou o seu gestor de ficheiros preferido para abrir o ficheiro OPML.</string> + <string name="opml_import_explanation_3">As aplicações como o Google Mail, Dropbox, Google Drive ou gestores de ficheiros podem <i>abrir</i> os ficheiros OPML <i>através</i> do AntennaPod.</string> <string name="start_import_label">Iniciar importação</string> <string name="opml_import_label">Importação OPML</string> <string name="opml_directory_error">Erro!</string> @@ -272,7 +282,8 @@ <string name="opml_import_error_dir_empty">O diretório de importação está vazio.</string> <string name="select_all_label">Marcar tudo</string> <string name="deselect_all_label">Desmarcar tudo</string> - <string name="choose_file_to_import_label">Escolha o ficheiro a importar</string> + <string name="choose_file_from_filesystem">Do sistema local de ficheiros</string> + <string name="choose_file_from_external_application">Utilizar aplicação externa</string> <string name="opml_export_label">Exportação OPML</string> <string name="exporting_label">Exportação...</string> <string name="export_error_label">Erro de exportação</string> @@ -358,4 +369,5 @@ <string name="authentication_descr">Altere o seu nome de utilizador e senha para este podcast e seus episódios.</string> <!--AntennaPodSP--> <string name="sp_apps_importing_feeds_msg">Importar subscrições de aplicações single-purpose...</string> + <string name="search_itunes_label">Procurar no iTunes</string> </resources> diff --git a/core/src/main/res/values-ro-rRO/strings.xml b/core/src/main/res/values-ro-rRO/strings.xml index 6cc93e4a9..7bfb99f9d 100644 --- a/core/src/main/res/values-ro-rRO/strings.xml +++ b/core/src/main/res/values-ro-rRO/strings.xml @@ -182,7 +182,6 @@ <string name="search_label">Caută</string> <string name="found_in_title_label">Găsit în titlu</string> <!--OPML import and export--> - <string name="opml_import_explanation">Pentru a importa un fișier OPML trebuie să-l salvați în următorul director și apăsați butonul de mai jos pentru a începe procesul.</string> <string name="start_import_label">Începe importarea</string> <string name="opml_import_label">OPML import</string> <string name="opml_directory_error">EROARE!</string> @@ -191,11 +190,9 @@ <string name="opml_import_error_dir_empty">Directorul de import este gol.</string> <string name="select_all_label">Selectează toate</string> <string name="deselect_all_label">Deselectează toate</string> - <string name="choose_file_to_import_label">Alege fișier pentru import</string> <string name="opml_export_label">Exportă OPML</string> <string name="exporting_label">Exportă...</string> <string name="export_error_label">Eroare exportare</string> - <string name="opml_export_success_title">Exportare opml cu succes.</string> <string name="opml_export_success_sum">Fișierul .opml a fost scris în:\u0020</string> <!--Sleep timer--> <string name="set_sleeptimer_label">Setează cronometru somn</string> diff --git a/core/src/main/res/values-ru/strings.xml b/core/src/main/res/values-ru/strings.xml index 140e7ea3a..e08e40e56 100644 --- a/core/src/main/res/values-ru/strings.xml +++ b/core/src/main/res/values-ru/strings.xml @@ -58,22 +58,26 @@ <string name="close_label">Закрыть</string> <string name="retry_label">Повторить</string> <string name="auto_download_label">Добавить в автозагрузки</string> + <string name="parallel_downloads_suffix">\u0020одновременных загрузок</string> <!--'Add Feed' Activity labels--> <string name="feedurl_label">URL канала</string> - <string name="etxtFeedurlHint">Адрес канала или сайта</string> + <string name="etxtFeedurlHint">www.example.com/feed</string> <string name="txtvfeedurl_label">Добавить подкаст по URL</string> <string name="podcastdirectories_label">Найти подкаст в каталоге</string> - <string name="podcastdirectories_descr">Вы можете искать новые подкасты в каталоге gpodder.net по имени, категории или популярности/</string> + <string name="podcastdirectories_descr">Вы можете искать новые подкасты по имени, категории или популярности в каталоге gpodder.net и в магазине iTunes.</string> <string name="browse_gpoddernet_label">Просмотр gpodder.net</string> <!--Actions on feeds--> <string name="mark_all_read_label">Отметить как прослушанное</string> <string name="mark_all_read_msg">Отметить все выпуски как прослушанные</string> + <string name="mark_all_read_confirmation_msg">Подтвердите, что хотите пометить все эпизоды как прослушанные.</string> + <string name="mark_all_read_feed_confirmation_msg">Подтвердите, что хотите пометить все эпизоды в этом канале как прослушанные.</string> <string name="show_info_label">Показать информацию</string> <string name="remove_feed_label">Удалить подкаст</string> <string name="share_link_label">Поделиться ссылкой на сайт</string> <string name="share_source_label">Ссылка на канал</string> <string name="feed_delete_confirmation_msg">Подтвердите удаление канала и всех выпусков, загруженных с этого канала.</string> <string name="feed_remover_msg">Удаление канала</string> + <string name="load_complete_feed">Обновить весь канал</string> <!--actions on feeditems--> <string name="download_label">Загрузить</string> <string name="play_label">Воспроизвести</string> @@ -84,6 +88,7 @@ <string name="remove_episode_lable">Удалить</string> <string name="mark_read_label">Отметить как прочитанное</string> <string name="mark_unread_label">Отметить как непрочитанное</string> + <string name="marked_as_read_label">Помечено как прослушанное</string> <string name="add_to_queue_label">Добавить в очередь</string> <string name="remove_from_queue_label">Удалить из очереди</string> <string name="visit_website_label">Посетить сайт</string> @@ -142,6 +147,13 @@ <string name="removed_from_queue">Удалено</string> <string name="move_to_top_label">Переместить вверх</string> <string name="move_to_bottom_label">Переместить вниз</string> + <string name="sort">Сортировать</string> + <string name="alpha">По алфавиту</string> + <string name="date">По дате</string> + <string name="duration">По продолжительности</string> + <string name="ascending">По возрастанию</string> + <string name="descending">По убыванию</string> + <string name="clear_queue_confirmation_msg">Подтвердите, что хотите очистить очередь от ВСЕХ эпизодов.</string> <!--Flattr--> <string name="flattr_auth_label">Авторизоваться в Flattr</string> <string name="flattr_auth_explanation">Нажмите кнопку, чтобы начать процесс авторизации. Вы будете перенаправлены на сайт Flattr, где нужно будет разрешить AntennaPod использовать ваш аккаунт. После этого вы автоматически будете перенаправлены обратно.</string> @@ -181,7 +193,10 @@ <string name="services_label">Сервисы</string> <string name="flattr_label">Flattr</string> <string name="pref_pauseOnHeadsetDisconnect_sum">Приостановить воспроизведение, когда наушники отсоединены</string> + <string name="pref_unpauseOnHeadsetReconnect_sum">Продолжать воспроизведение после подключения наушников</string> <string name="pref_followQueue_sum">После завершения воспроизведения перейти к следующему в очереди</string> + <string name="pref_auto_delete_sum">Удалять эпизод после завершения воспроизведения</string> + <string name="pref_auto_delete_title">Автоматическое удаление</string> <string name="playback_pref">Воспроизведение</string> <string name="network_pref">Сеть</string> <string name="pref_autoUpdateIntervall_title">Интервал обновлений</string> @@ -190,6 +205,7 @@ <string name="pref_followQueue_title">Непрерывное воспроизведение</string> <string name="pref_downloadMediaOnWifiOnly_title">Загрузка по Wi-Fi</string> <string name="pref_pauseOnHeadsetDisconnect_title">Наушники отсоединены</string> + <string name="pref_unpauseOnHeadsetReconnect_title">Наушники отсоединены</string> <string name="pref_mobileUpdate_title">Мобильные обновления</string> <string name="pref_mobileUpdate_sum">Позволить обновления через мобильное интернет-подключение</string> <string name="refreshing_label">Обновление</string> @@ -208,6 +224,9 @@ <string name="pref_automatic_download_sum">Настроить автоматическую загрузку выпусков.</string> <string name="pref_autodl_wifi_filter_title">Включить фильтр Wi-Fi</string> <string name="pref_autodl_wifi_filter_sum">Разрешать автоматическую загрузку только для выбранных сетей Wi-Fi.</string> + <string name="pref_automatic_download_on_battery_title">Загружать без зарядки</string> + <string name="pref_automatic_download_on_battery_sum">Разрешать автоматическую загрузку когда батарея не заряжается</string> + <string name="pref_parallel_downloads_title">Одновременные загрузки</string> <string name="pref_episode_cache_title">Кэш выпусков</string> <string name="pref_theme_title_light">Светлая</string> <string name="pref_theme_title_dark">Тёмная</string> @@ -223,6 +242,8 @@ <string name="pref_gpodnet_setlogin_information_sum">Изменить информацию авторизации для аккаунта gpodder.net</string> <string name="pref_playback_speed_title">Скорость воспроизведения</string> <string name="pref_playback_speed_sum">Настроить скорости воспроизведения</string> + <string name="pref_seek_delta_title">Перемотка</string> + <string name="pref_seek_delta_sum">Пропускать секунд при перемотке назад или вперед</string> <string name="pref_gpodnet_sethostname_title">Задать имя узла</string> <string name="pref_gpodnet_sethostname_use_default_host">Использовать узел по умолчанию</string> <string name="pref_expandNotify_title">Расширенное уведомление</string> @@ -231,6 +252,9 @@ <string name="pref_persistNotify_sum">Сохранять уведомление и кнопки воспроизведения на экране блокировки во время паузы.</string> <string name="pref_expand_notify_unsupport_toast">Версии Android ниже 4.1 не поддерживают расширенные уведомления.</string> <!--Auto-Flattr dialog--> + <string name="auto_flattr_after_percent">Поддерживать через Flattr эпизоды, прослушанные на %d процентов</string> + <string name="auto_flattr_ater_beginning">Поддерживать эпизод через Flattr в начале воспроизведения</string> + <string name="auto_flattr_ater_end">Поддерживать эпизод через Flattr в конце воспроизведения</string> <!--Search--> <string name="search_hint">Поиск каналов или выпусков</string> <string name="found_in_shownotes_label">Найдено в описании выпуска</string> @@ -240,7 +264,7 @@ <string name="found_in_title_label">Найдено в заголовке</string> <!--OPML import and export--> <string name="opml_import_txtv_button_lable">OPML файлы позволяют перемещать ваши подкасты из одного менеджера подкастов в другой.</string> - <string name="opml_import_explanation">Для импорта файла OPML его нужно поместить в указанный каталог и нажать кнопку внизу для запуска импорта.</string> + <string name="opml_import_explanation_1">Укажите путь к файлу на устройстве</string> <string name="start_import_label">Начать импорт</string> <string name="opml_import_label">Импорт OPML</string> <string name="opml_directory_error">Ошибка</string> @@ -249,11 +273,9 @@ <string name="opml_import_error_dir_empty">Каталог для импорта пуст.</string> <string name="select_all_label">Отметить все</string> <string name="deselect_all_label">Снять все отметки</string> - <string name="choose_file_to_import_label">Выбрать файл для импорта</string> <string name="opml_export_label">Экспорт в OPML</string> <string name="exporting_label">Экспортируется...</string> <string name="export_error_label">Ошибка экспорта</string> - <string name="opml_export_success_title">Экспорт OPML завершён.</string> <string name="opml_export_success_sum">Файл OPML был записан в:\u0020</string> <!--Sleep timer--> <string name="set_sleeptimer_label">Установить таймер сна</string> @@ -273,6 +295,7 @@ <string name="gpodnetauth_login_title">Войти</string> <string name="gpodnetauth_login_descr">Добро пожаловать в процесс авторизации на gpodder.net. Сначала введите вашу информацию для авторизации:</string> <string name="gpodnetauth_login_butLabel">Войти</string> + <string name="gpodnetauth_login_register">Если у вас ещё нет аккаунта, вы можете создать его здесь:\nhttps://gpodder.net/register/</string> <string name="username_label">Имя пользователя</string> <string name="password_label">Пароль</string> <string name="gpodnetauth_device_title">Выбор устройства</string> @@ -333,4 +356,5 @@ <string name="authentication_label">Авторизация</string> <!--AntennaPodSP--> <string name="sp_apps_importing_feeds_msg">Импорт подписок из одноцелевых приложений…</string> + <string name="search_itunes_label">Поиск в iTunes</string> </resources> diff --git a/core/src/main/res/values-sv-rSE/strings.xml b/core/src/main/res/values-sv-rSE/strings.xml index 888f08a1c..4e468c1e1 100644 --- a/core/src/main/res/values-sv-rSE/strings.xml +++ b/core/src/main/res/values-sv-rSE/strings.xml @@ -58,6 +58,7 @@ <string name="close_label">Stäng</string> <string name="retry_label">Försök igen</string> <string name="auto_download_label">Inkludera i automatiska nedladdningar</string> + <string name="parallel_downloads_suffix">\u0020parallella nedladdningar</string> <!--'Add Feed' Activity labels--> <string name="feedurl_label">Flödets URL</string> <string name="etxtFeedurlHint">URL till flöde eller webbsida</string> @@ -68,6 +69,8 @@ <!--Actions on feeds--> <string name="mark_all_read_label">Markera alla som lästa</string> <string name="mark_all_read_msg">Markera alla episoder som lästa</string> + <string name="mark_all_read_confirmation_msg">Bekräfta att du vill markera alla avsnitt som lästa.</string> + <string name="mark_all_read_feed_confirmation_msg">Bekräfta att du vill markera alla avsnitt i detta flöde som lästa.</string> <string name="show_info_label">Visa information</string> <string name="remove_feed_label">Ta bort podcast</string> <string name="share_link_label">Dela hemsidans länk</string> @@ -85,6 +88,7 @@ <string name="remove_episode_lable">Ta bort episod</string> <string name="mark_read_label">Markera som läst</string> <string name="mark_unread_label">Markera som oläst</string> + <string name="marked_as_read_label">Markerad som läst</string> <string name="add_to_queue_label">Lägg till i kön</string> <string name="remove_from_queue_label">Ta bort från Kön</string> <string name="visit_website_label">Besök websidan</string> @@ -150,6 +154,7 @@ <string name="duration">Längd</string> <string name="ascending">Stigande</string> <string name="descending">Fallande</string> + <string name="clear_queue_confirmation_msg">Bekräfta att du vill rensa kön från ALLA avsnitt.</string> <!--Flattr--> <string name="flattr_auth_label">Flattr inloggning</string> <string name="flattr_auth_explanation">Tryck på knappen nedan för att starta autentiseringen. Du kommer att vidarebefordras till Flattrs inloggningsskärm i din webbläsare och uppmanas att ge AntennaPod tillstånd att Flattra saker. Efter att du har gett tillstånd, kommer du automatiskt tillbaka till den här skärmen.</string> @@ -225,6 +230,7 @@ <string name="pref_autodl_wifi_filter_sum">Tillåt automatisk nedladdning endast för utvalda WiFi-nätverk.</string> <string name="pref_automatic_download_on_battery_title">Nedladdning vid batteridrift</string> <string name="pref_automatic_download_on_battery_sum">Tillåt automatisk nedladdning när batteriet inte laddas</string> + <string name="pref_parallel_downloads_title">Parallella nedladdningar</string> <string name="pref_episode_cache_title">Avsnittscache</string> <string name="pref_theme_title_light">Ljust</string> <string name="pref_theme_title_dark">Mörkt</string> @@ -249,6 +255,8 @@ <string name="pref_persistNotify_title">Bestående uppspelningskontroller</string> <string name="pref_persistNotify_sum">Behåll notifiering och kontroller på låsskärmen när uppspelningen pausas.</string> <string name="pref_expand_notify_unsupport_toast">Androidversioner före 4.1 har inte stöd för expanderade notifieringar.</string> + <string name="pref_queueAddToFront_sum">Lägg till avsnitt först i kön.</string> + <string name="pref_queueAddToFront_title">Köa först.</string> <!--Auto-Flattr dialog--> <string name="auto_flattr_enable">Aktivera automatisk Flattring</string> <string name="auto_flattr_after_percent">Flattra episoden så snart %d procent har spelats</string> @@ -263,7 +271,9 @@ <string name="found_in_title_label">Hittad i titeln</string> <!--OPML import and export--> <string name="opml_import_txtv_button_lable">OPML-filer låter dig flytta dina podcasts från en podcatcher till en annan.</string> - <string name="opml_import_explanation">Om du vill importera en OPML-fil, måste du placera den i följande katalog och tryck på knappen nedan för att starta importen.</string> + <string name="opml_import_explanation_1">Välj en specifik sökväg från det lokala filsystemet.</string> + <string name="opml_import_explanation_2">Använd en extern applikation som Dropbox, Google Drive eller ditt favoritval av filhanterare för att öppna en OPML fil.</string> + <string name="opml_import_explanation_3">Flera applikationer som Google Mail, Dropbox, Google Drive och de flesta filhanterare kan <i>öppna</i> OPML filer <i>med</i> AntennaPod.</string> <string name="start_import_label">Påbörja importering</string> <string name="opml_import_label">Importera OPML-fil</string> <string name="opml_directory_error">FEL! </string> @@ -272,7 +282,8 @@ <string name="opml_import_error_dir_empty">Katalogen är tom.</string> <string name="select_all_label">Välj alla</string> <string name="deselect_all_label">Avmarkera alla</string> - <string name="choose_file_to_import_label">Välj fil att importera</string> + <string name="choose_file_from_filesystem">Från lokalt filsystem</string> + <string name="choose_file_from_external_application">Använd extern applikation</string> <string name="opml_export_label">OPML export</string> <string name="exporting_label">Exporterar...</string> <string name="export_error_label">Exporteringsfel</string> @@ -358,4 +369,5 @@ <string name="authentication_descr">Byt ditt användarnamn och lösenord för den här podcasten och dess episoder.</string> <!--AntennaPodSP--> <string name="sp_apps_importing_feeds_msg">Importerar prenumerationer från appar gjorda för ett enda syfte...</string> + <string name="search_itunes_label">Leta i iTunes</string> </resources> diff --git a/core/src/main/res/values-tr/strings.xml b/core/src/main/res/values-tr/strings.xml index 4fe2c69cd..265a9025c 100644 --- a/core/src/main/res/values-tr/strings.xml +++ b/core/src/main/res/values-tr/strings.xml @@ -58,16 +58,19 @@ <string name="close_label">Kapat</string> <string name="retry_label">Yeniden dene</string> <string name="auto_download_label">Otomatik indirmelere dahil et</string> + <string name="parallel_downloads_suffix">\u0020paralel indirmeler</string> <!--'Add Feed' Activity labels--> <string name="feedurl_label">Besleme Adresi</string> - <string name="etxtFeedurlHint">Besleme veya web sayfası URLsi</string> + <string name="etxtFeedurlHint">www.example.com/feed</string> <string name="txtvfeedurl_label">URL ile cep yayını ekle</string> <string name="podcastdirectories_label">Dizinde cep yayını bul</string> - <string name="podcastdirectories_descr">gdpodder.net dizininde yeni cep yayınlarını isme, kategoriye veya popülerliğe göre arayabilirsiniz.</string> + <string name="podcastdirectories_descr">gdpodder.net dizininde yeni cep yayınlarını isme, kategoriye veya popülerliğe göre arayabilirsiniz veya iTunes mağazasında arama yapabilirsiniz.</string> <string name="browse_gpoddernet_label">gpodder.net\'e gözat</string> <!--Actions on feeds--> <string name="mark_all_read_label">Hepsini okundu olarak işaretle</string> <string name="mark_all_read_msg">Tüm bölümler okundu olarak işaretlendi</string> + <string name="mark_all_read_confirmation_msg">Lütfen tüm bölümleri okundu olarak işaretlemek istediğinizi onaylayın.</string> + <string name="mark_all_read_feed_confirmation_msg">Lütfen bu besleme içindeki tüm bölümleri okundu olarak işaretlemek istediğinizi onaylayın.</string> <string name="show_info_label">Bilgiyi göster</string> <string name="remove_feed_label">Cep yayını kaldır</string> <string name="share_link_label">Web sayfası bağlantısı paylaş</string> @@ -85,6 +88,7 @@ <string name="remove_episode_lable">Bölümü kaldır</string> <string name="mark_read_label">Okundu olarak işaretle</string> <string name="mark_unread_label">Okunmadı olarak işaretle</string> + <string name="marked_as_read_label">Okundu olarak işaretlendi</string> <string name="add_to_queue_label">Kuyruğa Ekle</string> <string name="remove_from_queue_label">Kuyruktan Kaldır</string> <string name="visit_website_label">Siteyi Ziyaret Et</string> @@ -144,6 +148,13 @@ <string name="removed_from_queue">Öge kaldırıldı</string> <string name="move_to_top_label">En üste taşı</string> <string name="move_to_bottom_label">En alta taşı</string> + <string name="sort">Sırala</string> + <string name="alpha">Alfabetik olarak</string> + <string name="date">Tarih</string> + <string name="duration">Süre</string> + <string name="ascending">Artan</string> + <string name="descending">Azalan</string> + <string name="clear_queue_confirmation_msg">Lütfen içerisindeki BÜTÜN ölümlerle birlikte kuyruğu temizleme isteğinizi onaylayın.</string> <!--Flattr--> <string name="flattr_auth_label">Flattr giriş</string> <string name="flattr_auth_explanation">Yetkilendirme işlemini başlatmak için aşağıdaki butona basın. Tarayıcınızda flattr giriş ekranına yönlendirileceksiniz ve AntennaPod\'un flattr ile etkileşime girebilmesi için izniniz istenecek. İzin verdikten sonra otomatik olarak bu ekrana döneceksiniz.</string> @@ -185,7 +196,10 @@ <string name="services_label">Servisler</string> <string name="flattr_label">Flattr</string> <string name="pref_pauseOnHeadsetDisconnect_sum">Kulaklıklar çıkarıldığında çalmayı duraklat</string> + <string name="pref_unpauseOnHeadsetReconnect_sum">Kulaklıklar yeniden bağlandığında çalmaya devam et</string> <string name="pref_followQueue_sum">Çalma tamamlandığında kuyruktaki diğer öğeye geç</string> + <string name="pref_auto_delete_sum">Çalma bittiğinde bölümü sil</string> + <string name="pref_auto_delete_title">Otomatik Silme</string> <string name="playback_pref">Çalma</string> <string name="network_pref">Ağ</string> <string name="pref_autoUpdateIntervall_title">Güncelleme aralığı</string> @@ -194,6 +208,7 @@ <string name="pref_followQueue_title">Devamlı çalma</string> <string name="pref_downloadMediaOnWifiOnly_title">Kablosuz medya indirmesi</string> <string name="pref_pauseOnHeadsetDisconnect_title">Kulaklık bağlı değil</string> + <string name="pref_unpauseOnHeadsetReconnect_title">Kulaklıklar yeniden bağlı</string> <string name="pref_mobileUpdate_title">Mobil güncellemeler</string> <string name="pref_mobileUpdate_sum">Mobil veri üzerinden güncellemelere izin ver</string> <string name="refreshing_label">Yenileniyor</string> @@ -213,6 +228,9 @@ <string name="pref_automatic_download_sum">Bölümlerin otomatik indirilmesini yapılandır.</string> <string name="pref_autodl_wifi_filter_title">Wi-Fi filtresini etkinleştir</string> <string name="pref_autodl_wifi_filter_sum">Seçilen kablosuz ağlar için otomatik indirmeye izin ver.</string> + <string name="pref_automatic_download_on_battery_title">Şarj olmuyorken indir</string> + <string name="pref_automatic_download_on_battery_sum">Pil şarj olmuyorken otomatik indirmeye izin ver</string> + <string name="pref_parallel_downloads_title">Paralel indirmeler</string> <string name="pref_episode_cache_title">Bölüm ön belleği</string> <string name="pref_theme_title_light">Aydınlık</string> <string name="pref_theme_title_dark">Karanlık</string> @@ -237,6 +255,8 @@ <string name="pref_persistNotify_title">Kalıcı oynatma kontrolleri</string> <string name="pref_persistNotify_sum">Çalma duraklatıldığında bildirim ve ekran kilidi ayarlarını sakla.</string> <string name="pref_expand_notify_unsupport_toast">Android 4.1 öncesi sürümler genişletilmiş bildirimleri desteklememektedir.</string> + <string name="pref_queueAddToFront_sum">Yeni bölümleri kuyruğun önüne ekle.</string> + <string name="pref_queueAddToFront_title">Kuyruğun önüne ekle.</string> <!--Auto-Flattr dialog--> <string name="auto_flattr_enable">Otomatik Flattr\'lamayı etkinleştir</string> <string name="auto_flattr_after_percent">Bölümün yüzde %d kısmı oynatıldığında Flattr\'la</string> @@ -251,7 +271,9 @@ <string name="found_in_title_label">Başlıkta bulundu</string> <!--OPML import and export--> <string name="opml_import_txtv_button_lable">OPML dosyaları cep yayınlarını bir cihazdan diğerine aktarmanıza yarar.</string> - <string name="opml_import_explanation">Bir OPML dosyasını içe aktarmak için onu aşağıdaki klasöre koyun ve aşağıdaki butona tıklayın.</string> + <string name="opml_import_explanation_1">Yerel dosya sisteminden belirli bir yol seçin.</string> + <string name="opml_import_explanation_2">OPML dosyasını açmak için harici uygulamalardan Dropbox, Google Drive veya kendi favori dosya yöneticinizi kullanın.</string> + <string name="opml_import_explanation_3">Google Mail, Dropbox, Google Drive gibi birçok uygulama ve çoğu dosya yöneticisi OPML dosyalarını AntennaPod <i>ile</i> <i>açabilir.</i></string> <string name="start_import_label">İçe aktarmayı başlat</string> <string name="opml_import_label">OPML içe aktar</string> <string name="opml_directory_error">HATA!</string> @@ -260,11 +282,12 @@ <string name="opml_import_error_dir_empty">İça aktarma dizini boş</string> <string name="select_all_label">Hepsini seç</string> <string name="deselect_all_label">Tüm seçimleri geri al</string> - <string name="choose_file_to_import_label">İçe aktarılacak dosyayı seç</string> + <string name="choose_file_from_filesystem">Yerel dosya sisteminden</string> + <string name="choose_file_from_external_application">Harici uygulama kullan</string> <string name="opml_export_label">OPML dışa aktar</string> <string name="exporting_label">Dışa aktarılıyor...</string> <string name="export_error_label">Dışa aktarma hatası</string> - <string name="opml_export_success_title">Opml dışa aktarma başarılı</string> + <string name="opml_export_success_title">Opml dışa aktarma başarılı.</string> <string name="opml_export_success_sum">.opml dosyasy yazıldı: \u0020</string> <!--Sleep timer--> <string name="set_sleeptimer_label">Zamanlayıcıyı ayarla</string> @@ -346,4 +369,5 @@ <string name="authentication_descr">Bu cep yayını ve içerdiği bölümler için kullanıcı adı şifreyi değiştir.</string> <!--AntennaPodSP--> <string name="sp_apps_importing_feeds_msg">Üyelikler tek-amaçlı uygulamalardan içe aktarılıyor...</string> + <string name="search_itunes_label">iTunes\'da Arama</string> </resources> diff --git a/core/src/main/res/values-uk-rUA/strings.xml b/core/src/main/res/values-uk-rUA/strings.xml index 08ce56f26..1602e6253 100644 --- a/core/src/main/res/values-uk-rUA/strings.xml +++ b/core/src/main/res/values-uk-rUA/strings.xml @@ -58,6 +58,7 @@ <string name="close_label">Закрити</string> <string name="retry_label">Повторити знову</string> <string name="auto_download_label">Включити до автозавантаження</string> + <string name="parallel_downloads_suffix">\u0020паралельні завантаження</string> <!--'Add Feed' Activity labels--> <string name="feedurl_label">Посилання на канал</string> <string name="etxtFeedurlHint">URL канала або сайта</string> @@ -68,6 +69,8 @@ <!--Actions on feeds--> <string name="mark_all_read_label">Позначити всі як переглянуті</string> <string name="mark_all_read_msg">Позначено всі епізоди як переглянуті</string> + <string name="mark_all_read_confirmation_msg">Будь ласка, підтвердіть що ви бажаєте позначити всі епізоди як прочитані.</string> + <string name="mark_all_read_feed_confirmation_msg">Будь ласка, підтвердіть що ви бажаєте позначити всі епізоди цього канала як прочитані.</string> <string name="show_info_label">Інформація</string> <string name="remove_feed_label">Видалити подкаст</string> <string name="share_link_label">Поділитися URL сайту</string> @@ -85,6 +88,7 @@ <string name="remove_episode_lable">Видалити епізод</string> <string name="mark_read_label">Позначити як переглянутий</string> <string name="mark_unread_label">Позначити як не переглянутий</string> + <string name="marked_as_read_label">Позначено як прочитане</string> <string name="add_to_queue_label">Додати до черги</string> <string name="remove_from_queue_label">Видалити з черги</string> <string name="visit_website_label">Відкрити сайт</string> @@ -150,6 +154,7 @@ <string name="duration">За тривалістю</string> <string name="ascending">За зростанням</string> <string name="descending">За спаданням</string> + <string name="clear_queue_confirmation_msg">Будь ласка, підтвердіть що ви бажаєте вилучити всі епізоди з черги.</string> <!--Flattr--> <string name="flattr_auth_label">Увійти до Flattr</string> <string name="flattr_auth_explanation">Нажміть цю кнопку для початку авторізації. Буде відкрито flattr в браузері, буде запит на дозвіл доступу Antennapod до flattr. Після надання доступу ви повернетесь до цього екрану автоматично</string> @@ -225,6 +230,7 @@ <string name="pref_autodl_wifi_filter_sum">Дозволити автоматичне завантаження тільки в цих Wi-Fi мережах</string> <string name="pref_automatic_download_on_battery_title">Завантаження без зарядного пристрою</string> <string name="pref_automatic_download_on_battery_sum">Дозволити завантаження коли зарядний пристрій не підключено</string> + <string name="pref_parallel_downloads_title">Паралельні завантаження</string> <string name="pref_episode_cache_title">Кеш епізодів</string> <string name="pref_theme_title_light">Світла</string> <string name="pref_theme_title_dark">Темна</string> @@ -249,6 +255,8 @@ <string name="pref_persistNotify_title">Завжди показувати елементи керування відтворенням</string> <string name="pref_persistNotify_sum">Показувати повідомлення та елементи керування на lockscreen в режимі паузи.</string> <string name="pref_expand_notify_unsupport_toast">Android до версії 4.1 не підтримує розширені повідомлення.</string> + <string name="pref_queueAddToFront_sum">Додавати нові епізоди до початку черги.</string> + <string name="pref_queueAddToFront_title">Додавати в початок черги.</string> <!--Auto-Flattr dialog--> <string name="auto_flattr_enable">Включити автоматичне заохочення авторів через сервіс flattr</string> <string name="auto_flattr_after_percent">Заохотити автора через Flattr щойно %d відсотків епізода було відтворено</string> @@ -263,7 +271,9 @@ <string name="found_in_title_label">Знайдено у назві</string> <!--OPML import and export--> <string name="opml_import_txtv_button_lable">OPML файли дозволяют вам перенести подкасти з однієї программи до іншої</string> - <string name="opml_import_explanation">Для імпорту OPML файлу, скопіюйте його в цю папку та натіснить кнопку внизу для початку імпорту</string> + <string name="opml_import_explanation_1">Виберіть локальну папку.</string> + <string name="opml_import_explanation_2">Вибрати OPML файл за допомогою таких додатків як Dropbox, Google Drive або файловий менеджер.</string> + <string name="opml_import_explanation_3">Багато додатків таких як Google Mail, Dropbox, Google Drive та більшість файлових менеджерів здатні <i>відкрити</i> OPML файли <i>для</i> AntennaPod.</string> <string name="start_import_label">Почати імпорт</string> <string name="opml_import_label">OPML імпорт</string> <string name="opml_directory_error">Помилка!</string> @@ -272,7 +282,8 @@ <string name="opml_import_error_dir_empty">Директорія імпорту пуста</string> <string name="select_all_label">Обрати все</string> <string name="deselect_all_label">Убрати виділення</string> - <string name="choose_file_to_import_label">Обрати файл для імпорту</string> + <string name="choose_file_from_filesystem">З локальної файлової системи</string> + <string name="choose_file_from_external_application">За допомогою додатка</string> <string name="opml_export_label">OPML экспорт</string> <string name="exporting_label">Експорт ...</string> <string name="export_error_label">Помилка експорту</string> @@ -358,4 +369,5 @@ <string name="authentication_descr">Змінити ваші логін та пароль для подкаста та епізодів</string> <!--AntennaPodSP--> <string name="sp_apps_importing_feeds_msg">Імпорт подкастів з інших програм...</string> + <string name="search_itunes_label">Пошук в iTunes</string> </resources> diff --git a/core/src/main/res/values-zh-rCN/strings.xml b/core/src/main/res/values-zh-rCN/strings.xml index 59958ec35..d857ea194 100644 --- a/core/src/main/res/values-zh-rCN/strings.xml +++ b/core/src/main/res/values-zh-rCN/strings.xml @@ -166,7 +166,10 @@ <string name="services_label">服务</string> <string name="flattr_label">Flattr</string> <string name="pref_pauseOnHeadsetDisconnect_sum">耳机断开时暂停播放 </string> + <string name="pref_unpauseOnHeadsetReconnect_sum">当耳机重新连接时恢复播放</string> <string name="pref_followQueue_sum">播放完成跳转到播放列表下一项</string> + <string name="pref_auto_delete_sum">当播放完成后删除单集</string> + <string name="pref_auto_delete_title">自动删除</string> <string name="playback_pref">播放</string> <string name="network_pref">网络</string> <string name="pref_autoUpdateIntervall_title">更新周期</string> @@ -175,6 +178,7 @@ <string name="pref_followQueue_title">连续播放</string> <string name="pref_downloadMediaOnWifiOnly_title">仅在 WIFI 情况下载</string> <string name="pref_pauseOnHeadsetDisconnect_title">耳机断开</string> + <string name="pref_unpauseOnHeadsetReconnect_title">耳机重新连接</string> <string name="pref_mobileUpdate_title">数据网络时更新</string> <string name="pref_mobileUpdate_sum">允许移动数据网络情况下进行数据链接</string> <string name="refreshing_label">刷新中</string> @@ -208,8 +212,15 @@ <string name="pref_gpodnet_setlogin_information_sum">改变 gpodder.net 账户登录信息.</string> <string name="pref_playback_speed_title">播放速度</string> <string name="pref_playback_speed_sum">自定义音频播放速度</string> + <string name="pref_seek_delta_title">定位时间</string> + <string name="pref_seek_delta_sum">当倒退或快速回放时以这些秒为单位</string> <string name="pref_gpodnet_sethostname_title">设置主机名</string> <string name="pref_gpodnet_sethostname_use_default_host">使用默认主机</string> + <string name="pref_expandNotify_title">扩展通知</string> + <string name="pref_expandNotify_sum">总是扩展通知以显示播放按钮</string> + <string name="pref_persistNotify_title">保持播放控制</string> + <string name="pref_persistNotify_sum">在暂停时保持通知和锁屏界面的控制。</string> + <string name="pref_expand_notify_unsupport_toast">Android 版本 4.1 之前不支持扩展通知</string> <!--Auto-Flattr dialog--> <!--Search--> <string name="search_hint">搜索订阅或者曲目</string> @@ -220,7 +231,6 @@ <string name="found_in_title_label">标题中查找</string> <!--OPML import and export--> <string name="opml_import_txtv_button_lable">OPML 文件可以方便的从别的播客转移数据过来。</string> - <string name="opml_import_explanation">导入 OPML 文件, 您必须将它放在以下目录, 之后按下面的按钮开始导入处理. </string> <string name="start_import_label">开始导入</string> <string name="opml_import_label">OPML 导入</string> <string name="opml_directory_error">错误!</string> @@ -229,7 +239,6 @@ <string name="opml_import_error_dir_empty">导入目录为空.</string> <string name="select_all_label">全选</string> <string name="deselect_all_label">取消所有选择</string> - <string name="choose_file_to_import_label">选择导入文件</string> <string name="opml_export_label">OPML 导出</string> <string name="exporting_label">导出中...</string> <string name="export_error_label">导出出错</string> diff --git a/core/src/main/res/values/attrs.xml b/core/src/main/res/values/attrs.xml index f36119c8d..368921f76 100644 --- a/core/src/main/res/values/attrs.xml +++ b/core/src/main/res/values/attrs.xml @@ -11,6 +11,7 @@ <attr name="av_rewind" format="reference"/> <attr name="content_discard" format="reference"/> <attr name="content_new" format="reference"/> + <attr name="feed" format="reference"/> <attr name="device_access_time" format="reference"/> <attr name="location_web_site" format="reference"/> <attr name="navigation_accept" format="reference"/> diff --git a/core/src/main/res/values/strings.xml b/core/src/main/res/values/strings.xml index 277d7d3c0..186651224 100644 --- a/core/src/main/res/values/strings.xml +++ b/core/src/main/res/values/strings.xml @@ -67,18 +67,21 @@ <string name="close_label">Close</string> <string name="retry_label">Retry</string> <string name="auto_download_label">Include in auto downloads</string> + <string name="parallel_downloads_suffix">\u0020parallel downloads</string> <!-- 'Add Feed' Activity labels --> <string name="feedurl_label">Feed URL</string> - <string name="etxtFeedurlHint">URL of feed or website</string> + <string name="etxtFeedurlHint">www.example.com/feed</string> <string name="txtvfeedurl_label">Add Podcast by URL</string> <string name="podcastdirectories_label">Find podcast in directory</string> - <string name="podcastdirectories_descr">You can search for new podcasts by name, category or popularity in the gpodder.net directory.</string> + <string name="podcastdirectories_descr">You can search for new podcasts by name, category or popularity in the gpodder.net directory, or search the iTunes store.</string> <string name="browse_gpoddernet_label">Browse gpodder.net</string> <!-- Actions on feeds --> <string name="mark_all_read_label">Mark all as read</string> <string name="mark_all_read_msg">Marked all episodes as read</string> + <string name="mark_all_read_confirmation_msg">Please confirm that you want to mark all episodes as being read.</string> + <string name="mark_all_read_feed_confirmation_msg">Please confirm that you want to mark all episodes in this feed as being read.</string> <string name="show_info_label">Show information</string> <string name="remove_feed_label">Remove podcast</string> <string name="share_link_label">Share website link</string> @@ -97,6 +100,7 @@ <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="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> @@ -165,6 +169,7 @@ <string name="duration">Duration</string> <string name="ascending">Ascending</string> <string name="descending">Descending</string> + <string name="clear_queue_confirmation_msg">Please confirm that you want to clear the queue of ALL of the episodes in it</string> <!-- Flattr --> <string name="flattr_auth_label">Flattr sign-in</string> @@ -245,6 +250,7 @@ <string name="pref_autodl_wifi_filter_sum">Allow automatic download only for selected Wi-Fi networks.</string> <string name="pref_automatic_download_on_battery_title">Download when not charging</string> <string name="pref_automatic_download_on_battery_sum">Allow automatic download when the battery is not charging</string> + <string name="pref_parallel_downloads_title">Parallel downloads</string> <string name="pref_episode_cache_title">Episode cache</string> <string name="pref_theme_title_light">Light</string> <string name="pref_theme_title_dark">Dark</string> @@ -269,6 +275,8 @@ <string name="pref_persistNotify_title">Persistent playback controls</string> <string name="pref_persistNotify_sum">Keep notification and lockscreen controls when playback is paused.</string> <string name="pref_expand_notify_unsupport_toast">Android versions before 4.1 do not support expanded notifications.</string> + <string name="pref_queueAddToFront_sum">Add new episodes to the front of the queue.</string> + <string name="pref_queueAddToFront_title">Enqueue at front.</string> <!-- Auto-Flattr dialog --> <string name="auto_flattr_enable">Enable automatic flattring</string> @@ -286,8 +294,9 @@ <!-- OPML import and export --> <string name="opml_import_txtv_button_lable">OPML files allow you to move your podcasts from one podcatcher to another.</string> - <string name="opml_import_explanation">To import an OPML file, you have to place it in the following directory and press the button below to start the import process. </string> - <string name="start_import_label">Start import</string> + <string name="opml_import_explanation_1">Choose a specific file path from the local filesystem.</string> + <string name="opml_import_explanation_2">Use an external applications like Dropbox, Google Drive or your favourite file manager to open an OPML file.</string> + <string name="opml_import_explanation_3">Many applications like Google Mail, Dropbox, Google Drive and most file managers can <i>open</i> OPML files <i>with</i> AntennaPod.</string> <string name="start_import_label">Start import</string> <string name="opml_import_label">OPML import</string> <string name="opml_directory_error">ERROR!</string> <string name="reading_opml_label">Reading OPML file</string> @@ -295,11 +304,12 @@ <string name="opml_import_error_dir_empty">The import directory is empty.</string> <string name="select_all_label">Select all</string> <string name="deselect_all_label">Deselect all</string> - <string name="choose_file_to_import_label">Choose file to import</string> + <string name="choose_file_from_filesystem">From local filesystem</string> + <string name="choose_file_from_external_application">Use external application</string> <string name="opml_export_label">OPML export</string> <string name="exporting_label">Exporting...</string> <string name="export_error_label">Export error</string> - <string name="opml_export_success_title">Opml export successful.</string> + <string name="opml_export_success_title">OPML export successful.</string> <string name="opml_export_success_sum">The .opml file was written to:\u0020</string> <!-- Sleep timer --> @@ -391,4 +401,5 @@ <!-- AntennaPodSP --> <string name="sp_apps_importing_feeds_msg">Importing subscriptions from single-purpose apps…</string> + <string name="search_itunes_label">Search iTunes</string> </resources> diff --git a/core/src/main/res/values/styles.xml b/core/src/main/res/values/styles.xml index a2f180395..4ac4a79fd 100644 --- a/core/src/main/res/values/styles.xml +++ b/core/src/main/res/values/styles.xml @@ -15,6 +15,7 @@ <item name="attr/content_discard">@drawable/ic_delete_grey600_24dp</item> <item name="attr/content_new">@drawable/ic_add_grey600_24dp</item> <item name="attr/device_access_time">@drawable/ic_timer_grey600_24dp</item> + <item name="attr/feed">@drawable/ic_feed_grey600_24dp</item> <item name="attr/location_web_site">@drawable/ic_web_grey600_24dp</item> <item name="attr/navigation_accept">@drawable/ic_done_grey600_24dp</item> <item name="attr/navigation_cancel">@drawable/ic_cancel_grey600_24dp</item> @@ -56,6 +57,7 @@ <item name="attr/content_discard">@drawable/ic_delete_white_24dp</item> <item name="attr/content_new">@drawable/ic_add_white_24dp</item> <item name="attr/device_access_time">@drawable/ic_timer_white_24dp</item> + <item name="attr/feed">@drawable/ic_feed_white_24dp</item> <item name="attr/location_web_site">@drawable/ic_web_white_24dp</item> <item name="attr/navigation_accept">@drawable/ic_done_white_24dp</item> <item name="attr/navigation_cancel">@drawable/ic_cancel_white_24dp</item> @@ -100,6 +102,7 @@ <item name="attr/content_discard">@drawable/ic_delete_grey600_24dp</item> <item name="attr/content_new">@drawable/ic_add_grey600_24dp</item> <item name="attr/device_access_time">@drawable/ic_timer_grey600_24dp</item> + <item name="attr/feed">@drawable/ic_feed_grey600_24dp</item> <item name="attr/location_web_site">@drawable/ic_web_grey600_24dp</item> <item name="attr/navigation_accept">@drawable/ic_done_grey600_24dp</item> <item name="attr/navigation_cancel">@drawable/ic_cancel_grey600_24dp</item> @@ -143,6 +146,7 @@ <item name="attr/content_discard">@drawable/ic_delete_white_24dp</item> <item name="attr/content_new">@drawable/ic_add_white_24dp</item> <item name="attr/device_access_time">@drawable/ic_timer_white_24dp</item> + <item name="attr/feed">@drawable/ic_feed_white_24dp</item> <item name="attr/location_web_site">@drawable/ic_web_white_24dp</item> <item name="attr/navigation_accept">@drawable/ic_done_white_24dp</item> <item name="attr/navigation_cancel">@drawable/ic_cancel_white_24dp</item> @@ -215,7 +219,6 @@ <style name="AntennaPod.TextView.Heading" parent="@android:style/TextAppearance.Medium"> <item name="android:textSize">@dimen/text_size_large</item> - <item name="android:textStyle">italic</item> <item name="android:textColor">?android:attr/textColorPrimary</item> </style> diff --git a/description/en.txt b/description/en.txt new file mode 100755 index 000000000..3f3ffea02 --- /dev/null +++ b/description/en.txt @@ -0,0 +1,22 @@ +An open-source podcast manager for Android. + +AntennaPod is an open-source podcast manager for Android 2.3.3 and above. It offers all the basic features you expect from a podcatcher, like streaming and downloading episodes, refreshing all feeds automatically or adding them to a queue to listen to them later. Moreover, AntennaPod lets you flattr podcasts and episodes from within the app. + +So far the following features are implemented: + +* Downloading and Streaming of episodes +* Variable speed playback (requires Presto Sound Library or Prestissimo) +* Support for Atom and RSS feeds +* Support for password-protected feeds and episodes +* Support for searching iTunes listings +* OPML import and export +* Flattr integration including automatic flattring +* Player homescreen widget +* Search +* Automatic feed updates +* Automatic download of new episodes +* Sleep timer +* Access to the gpodder.net podcast directory +* Subscription syncing with the gpodder.net service +* Supports MP3 chapters, VorbisComment chapters and Podlove Simple Chapters +* Supports paged feeds (http://podlove.org/paged-feeds/) diff --git a/library/drag-sort-listview/.gitignore b/library/drag-sort-listview/.gitignore new file mode 100644 index 000000000..a45024b5c --- /dev/null +++ b/library/drag-sort-listview/.gitignore @@ -0,0 +1,22 @@ +# built application files +*.apk +*.ap_ + +# files for the dex VM +*.dex + +# Java class files +*.class + +# generated files +bin/ +gen/ +target/ + +# Local configuration file (sdk path, etc) +local.properties +.gitattributes + +# Eclipse project files +.classpath +.project diff --git a/library/drag-sort-listview/build.gradle b/library/drag-sort-listview/build.gradle new file mode 100644 index 000000000..f33ddd8eb --- /dev/null +++ b/library/drag-sort-listview/build.gradle @@ -0,0 +1,30 @@ +apply plugin: 'com.android.library' + +android { + compileSdkVersion 21 + buildToolsVersion "21.1.2" + + defaultConfig { + minSdkVersion 10 + targetSdkVersion 21 + versionCode 4 + versionName "0.6.1" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_7 + targetCompatibility JavaVersion.VERSION_1_7 + } + +} + +dependencies { + compile fileTree(dir: 'libs', include: ['*.jar']) + compile 'com.android.support:support-v4:21.0.3' +} diff --git a/library/drag-sort-listview/proguard-project.txt b/library/drag-sort-listview/proguard-project.txt new file mode 100644 index 000000000..f2fe1559a --- /dev/null +++ b/library/drag-sort-listview/proguard-project.txt @@ -0,0 +1,20 @@ +# To enable ProGuard in your project, edit project.properties +# to define the proguard.config property as described in that file. +# +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in ${sdk.dir}/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the ProGuard +# include property in project.properties. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} diff --git a/library/drag-sort-listview/src/main/AndroidManifest.xml b/library/drag-sort-listview/src/main/AndroidManifest.xml new file mode 100644 index 000000000..f0ef29f3e --- /dev/null +++ b/library/drag-sort-listview/src/main/AndroidManifest.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="utf-8"?> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.mobeta.android.dslv" + android:versionCode="4" + android:versionName="0.6.1"> + <uses-sdk android:targetSdkVersion="7" + android:minSdkVersion="7" /> +</manifest> diff --git a/library/drag-sort-listview/src/main/java/com/mobeta/android/dslv/DragSortController.java b/library/drag-sort-listview/src/main/java/com/mobeta/android/dslv/DragSortController.java new file mode 100644 index 000000000..6acf6b42e --- /dev/null +++ b/library/drag-sort-listview/src/main/java/com/mobeta/android/dslv/DragSortController.java @@ -0,0 +1,465 @@ +package com.mobeta.android.dslv; + +import android.graphics.Point; +import android.view.GestureDetector; +import android.view.HapticFeedbackConstants; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewConfiguration; +import android.widget.AdapterView; + +/** + * Class that starts and stops item drags on a {@link DragSortListView} + * based on touch gestures. This class also inherits from + * {@link SimpleFloatViewManager}, which provides basic float View + * creation. + * + * An instance of this class is meant to be passed to the methods + * {@link DragSortListView#setTouchListener()} and + * {@link DragSortListView#setFloatViewManager()} of your + * {@link DragSortListView} instance. + */ +public class DragSortController extends SimpleFloatViewManager implements View.OnTouchListener, GestureDetector.OnGestureListener { + + /** + * Drag init mode enum. + */ + public static final int ON_DOWN = 0; + public static final int ON_DRAG = 1; + public static final int ON_LONG_PRESS = 2; + + private int mDragInitMode = ON_DOWN; + + private boolean mSortEnabled = true; + + /** + * Remove mode enum. + */ + public static final int CLICK_REMOVE = 0; + public static final int FLING_REMOVE = 1; + + /** + * The current remove mode. + */ + private int mRemoveMode; + + private boolean mRemoveEnabled = false; + private boolean mIsRemoving = false; + + private GestureDetector mDetector; + + private GestureDetector mFlingRemoveDetector; + + private int mTouchSlop; + + public static final int MISS = -1; + + private int mHitPos = MISS; + private int mFlingHitPos = MISS; + + private int mClickRemoveHitPos = MISS; + + private int[] mTempLoc = new int[2]; + + private int mItemX; + private int mItemY; + + private int mCurrX; + private int mCurrY; + + private boolean mDragging = false; + + private float mFlingSpeed = 500f; + + private int mDragHandleId; + + private int mClickRemoveId; + + private int mFlingHandleId; + private boolean mCanDrag; + + private DragSortListView mDslv; + private int mPositionX; + + /** + * Calls {@link #DragSortController(DragSortListView, int)} with a + * 0 drag handle id, FLING_RIGHT_REMOVE remove mode, + * and ON_DOWN drag init. By default, sorting is enabled, and + * removal is disabled. + * + * @param dslv The DSLV instance + */ + public DragSortController(DragSortListView dslv) { + this(dslv, 0, ON_DOWN, FLING_REMOVE); + } + + public DragSortController(DragSortListView dslv, int dragHandleId, int dragInitMode, int removeMode) { + this(dslv, dragHandleId, dragInitMode, removeMode, 0); + } + + public DragSortController(DragSortListView dslv, int dragHandleId, int dragInitMode, int removeMode, int clickRemoveId) { + this(dslv, dragHandleId, dragInitMode, removeMode, clickRemoveId, 0); + } + + /** + * By default, sorting is enabled, and removal is disabled. + * + * @param dslv The DSLV instance + * @param dragHandleId The resource id of the View that represents + * the drag handle in a list item. + */ + public DragSortController(DragSortListView dslv, int dragHandleId, int dragInitMode, + int removeMode, int clickRemoveId, int flingHandleId) { + super(dslv); + mDslv = dslv; + mDetector = new GestureDetector(dslv.getContext(), this); + mFlingRemoveDetector = new GestureDetector(dslv.getContext(), mFlingRemoveListener); + mFlingRemoveDetector.setIsLongpressEnabled(false); + mTouchSlop = ViewConfiguration.get(dslv.getContext()).getScaledTouchSlop(); + mDragHandleId = dragHandleId; + mClickRemoveId = clickRemoveId; + mFlingHandleId = flingHandleId; + setRemoveMode(removeMode); + setDragInitMode(dragInitMode); + } + + + public int getDragInitMode() { + return mDragInitMode; + } + + /** + * Set how a drag is initiated. Needs to be one of + * {@link ON_DOWN}, {@link ON_DRAG}, or {@link ON_LONG_PRESS}. + * + * @param mode The drag init mode. + */ + public void setDragInitMode(int mode) { + mDragInitMode = mode; + } + + /** + * Enable/Disable list item sorting. Disabling is useful if only item + * removal is desired. Prevents drags in the vertical direction. + * + * @param enabled Set <code>true</code> to enable list + * item sorting. + */ + public void setSortEnabled(boolean enabled) { + mSortEnabled = enabled; + } + + public boolean isSortEnabled() { + return mSortEnabled; + } + + /** + * One of {@link CLICK_REMOVE}, {@link FLING_RIGHT_REMOVE}, + * {@link FLING_LEFT_REMOVE}, + * {@link SLIDE_RIGHT_REMOVE}, or {@link SLIDE_LEFT_REMOVE}. + */ + public void setRemoveMode(int mode) { + mRemoveMode = mode; + } + + public int getRemoveMode() { + return mRemoveMode; + } + + /** + * Enable/Disable item removal without affecting remove mode. + */ + public void setRemoveEnabled(boolean enabled) { + mRemoveEnabled = enabled; + } + + public boolean isRemoveEnabled() { + return mRemoveEnabled; + } + + /** + * Set the resource id for the View that represents the drag + * handle in a list item. + * + * @param id An android resource id. + */ + public void setDragHandleId(int id) { + mDragHandleId = id; + } + + /** + * Set the resource id for the View that represents the fling + * handle in a list item. + * + * @param id An android resource id. + */ + public void setFlingHandleId(int id) { + mFlingHandleId = id; + } + + /** + * Set the resource id for the View that represents click + * removal button. + * + * @param id An android resource id. + */ + public void setClickRemoveId(int id) { + mClickRemoveId = id; + } + + /** + * Sets flags to restrict certain motions of the floating View + * based on DragSortController settings (such as remove mode). + * Starts the drag on the DragSortListView. + * + * @param position The list item position (includes headers). + * @param deltaX Touch x-coord minus left edge of floating View. + * @param deltaY Touch y-coord minus top edge of floating View. + * + * @return True if drag started, false otherwise. + */ + public boolean startDrag(int position, int deltaX, int deltaY) { + + int dragFlags = 0; + if (mSortEnabled && !mIsRemoving) { + dragFlags |= DragSortListView.DRAG_POS_Y | DragSortListView.DRAG_NEG_Y; + } + if (mRemoveEnabled && mIsRemoving) { + dragFlags |= DragSortListView.DRAG_POS_X; + dragFlags |= DragSortListView.DRAG_NEG_X; + } + + mDragging = mDslv.startDrag(position - mDslv.getHeaderViewsCount(), dragFlags, deltaX, + deltaY); + return mDragging; + } + + @Override + public boolean onTouch(View v, MotionEvent ev) { + if (!mDslv.isDragEnabled() || mDslv.listViewIntercepted()) { + return false; + } + + mDetector.onTouchEvent(ev); + if (mRemoveEnabled && mDragging && mRemoveMode == FLING_REMOVE) { + mFlingRemoveDetector.onTouchEvent(ev); + } + + int action = ev.getAction() & MotionEvent.ACTION_MASK; + switch (action) { + case MotionEvent.ACTION_DOWN: + mCurrX = (int) ev.getX(); + mCurrY = (int) ev.getY(); + break; + case MotionEvent.ACTION_UP: + if (mRemoveEnabled && mIsRemoving) { + int x = mPositionX >= 0 ? mPositionX : -mPositionX; + int removePoint = mDslv.getWidth() / 2; + if (x > removePoint) { + mDslv.stopDragWithVelocity(true, 0); + } + } + case MotionEvent.ACTION_CANCEL: + mIsRemoving = false; + mDragging = false; + break; + } + + return false; + } + + /** + * Overrides to provide fading when slide removal is enabled. + */ + @Override + public void onDragFloatView(View floatView, Point position, Point touch) { + + if (mRemoveEnabled && mIsRemoving) { + mPositionX = position.x; + } + } + + /** + * Get the position to start dragging based on the ACTION_DOWN + * MotionEvent. This function simply calls + * {@link #dragHandleHitPosition(MotionEvent)}. Override + * to change drag handle behavior; + * this function is called internally when an ACTION_DOWN + * event is detected. + * + * @param ev The ACTION_DOWN MotionEvent. + * + * @return The list position to drag if a drag-init gesture is + * detected; MISS if unsuccessful. + */ + public int startDragPosition(MotionEvent ev) { + return dragHandleHitPosition(ev); + } + + public int startFlingPosition(MotionEvent ev) { + return mRemoveMode == FLING_REMOVE ? flingHandleHitPosition(ev) : MISS; + } + + /** + * Checks for the touch of an item's drag handle (specified by + * {@link #setDragHandleId(int)}), and returns that item's position + * if a drag handle touch was detected. + * + * @param ev The ACTION_DOWN MotionEvent. + + * @return The list position of the item whose drag handle was + * touched; MISS if unsuccessful. + */ + public int dragHandleHitPosition(MotionEvent ev) { + return viewIdHitPosition(ev, mDragHandleId); + } + + public int flingHandleHitPosition(MotionEvent ev) { + return viewIdHitPosition(ev, mFlingHandleId); + } + + public int viewIdHitPosition(MotionEvent ev, int id) { + final int x = (int) ev.getX(); + final int y = (int) ev.getY(); + + int touchPos = mDslv.pointToPosition(x, y); // includes headers/footers + + final int numHeaders = mDslv.getHeaderViewsCount(); + final int numFooters = mDslv.getFooterViewsCount(); + final int count = mDslv.getCount(); + + // Log.d("mobeta", "touch down on position " + itemnum); + // We're only interested if the touch was on an + // item that's not a header or footer. + if (touchPos != AdapterView.INVALID_POSITION && touchPos >= numHeaders + && touchPos < (count - numFooters)) { + final View item = mDslv.getChildAt(touchPos - mDslv.getFirstVisiblePosition()); + final int rawX = (int) ev.getRawX(); + final int rawY = (int) ev.getRawY(); + + View dragBox = id == 0 ? item : (View) item.findViewById(id); + if (dragBox != null) { + dragBox.getLocationOnScreen(mTempLoc); + + if (rawX > mTempLoc[0] && rawY > mTempLoc[1] && + rawX < mTempLoc[0] + dragBox.getWidth() && + rawY < mTempLoc[1] + dragBox.getHeight()) { + + mItemX = item.getLeft(); + mItemY = item.getTop(); + + return touchPos; + } + } + } + + return MISS; + } + + @Override + public boolean onDown(MotionEvent ev) { + if (mRemoveEnabled && mRemoveMode == CLICK_REMOVE) { + mClickRemoveHitPos = viewIdHitPosition(ev, mClickRemoveId); + } + + mHitPos = startDragPosition(ev); + if (mHitPos != MISS && mDragInitMode == ON_DOWN) { + startDrag(mHitPos, (int) ev.getX() - mItemX, (int) ev.getY() - mItemY); + } + + mIsRemoving = false; + mCanDrag = true; + mPositionX = 0; + mFlingHitPos = startFlingPosition(ev); + + return true; + } + + @Override + public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { + + final int x1 = (int) e1.getX(); + final int y1 = (int) e1.getY(); + final int x2 = (int) e2.getX(); + final int y2 = (int) e2.getY(); + final int deltaX = x2 - mItemX; + final int deltaY = y2 - mItemY; + + if (mCanDrag && !mDragging && (mHitPos != MISS || mFlingHitPos != MISS)) { + if (mHitPos != MISS) { + if (mDragInitMode == ON_DRAG && Math.abs(y2 - y1) > mTouchSlop && mSortEnabled) { + startDrag(mHitPos, deltaX, deltaY); + } + else if (mDragInitMode != ON_DOWN && Math.abs(x2 - x1) > mTouchSlop && mRemoveEnabled) + { + mIsRemoving = true; + startDrag(mFlingHitPos, deltaX, deltaY); + } + } else if (mFlingHitPos != MISS) { + if (Math.abs(x2 - x1) > mTouchSlop && mRemoveEnabled) { + mIsRemoving = true; + startDrag(mFlingHitPos, deltaX, deltaY); + } else if (Math.abs(y2 - y1) > mTouchSlop) { + mCanDrag = false; // if started to scroll the list then + // don't allow sorting nor fling-removing + } + } + } + // return whatever + return false; + } + + @Override + public void onLongPress(MotionEvent e) { + // Log.d("mobeta", "lift listener long pressed"); + if (mHitPos != MISS && mDragInitMode == ON_LONG_PRESS) { + mDslv.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); + startDrag(mHitPos, mCurrX - mItemX, mCurrY - mItemY); + } + } + + // complete the OnGestureListener interface + @Override + public final boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { + return false; + } + + // complete the OnGestureListener interface + @Override + public boolean onSingleTapUp(MotionEvent ev) { + if (mRemoveEnabled && mRemoveMode == CLICK_REMOVE) { + if (mClickRemoveHitPos != MISS) { + mDslv.removeItem(mClickRemoveHitPos - mDslv.getHeaderViewsCount()); + } + } + return true; + } + + // complete the OnGestureListener interface + @Override + public void onShowPress(MotionEvent ev) { + // do nothing + } + + private GestureDetector.OnGestureListener mFlingRemoveListener = + new GestureDetector.SimpleOnGestureListener() { + @Override + public final boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, + float velocityY) { + ViewConfiguration vc = ViewConfiguration.get(mDslv.getContext()); + int minSwipeVelocity = vc.getScaledMinimumFlingVelocity(); + int maxSwipeVelocity = vc.getScaledMaximumFlingVelocity(); + if (mRemoveEnabled && mIsRemoving) { + int w = mDslv.getWidth(); + if(mPositionX >= w/2) { + mDslv.stopDragWithVelocity(true, velocityX); + } else if(mPositionX >= w/5 && minSwipeVelocity <= velocityX && velocityX <= maxSwipeVelocity) { + mDslv.stopDragWithVelocity(true, velocityX); + } + mIsRemoving = false; + } + return false; + } + }; + +} diff --git a/library/drag-sort-listview/src/main/java/com/mobeta/android/dslv/DragSortCursorAdapter.java b/library/drag-sort-listview/src/main/java/com/mobeta/android/dslv/DragSortCursorAdapter.java new file mode 100644 index 000000000..267c6f869 --- /dev/null +++ b/library/drag-sort-listview/src/main/java/com/mobeta/android/dslv/DragSortCursorAdapter.java @@ -0,0 +1,241 @@ +package com.mobeta.android.dslv; + +import java.util.ArrayList; + +import android.content.Context; +import android.database.Cursor; +import android.util.SparseIntArray; +import android.view.View; +import android.view.ViewGroup; +import android.support.v4.widget.CursorAdapter; + + +/** + * A subclass of {@link android.widget.CursorAdapter} that provides + * reordering of the elements in the Cursor based on completed + * drag-sort operations. The reordering is a simple mapping of + * list positions into Cursor positions (the Cursor is unchanged). + * To persist changes made by drag-sorts, one can retrieve the + * mapping with the {@link #getCursorPositions()} method, which + * returns the reordered list of Cursor positions. + * + * An instance of this class is passed + * to {@link DragSortListView#setAdapter(ListAdapter)} and, since + * this class implements the {@link DragSortListView.DragSortListener} + * interface, it is automatically set as the DragSortListener for + * the DragSortListView instance. + */ +public abstract class DragSortCursorAdapter extends CursorAdapter implements DragSortListView.DragSortListener { + + public static final int REMOVED = -1; + + /** + * Key is ListView position, value is Cursor position + */ + private SparseIntArray mListMapping = new SparseIntArray(); + + private ArrayList<Integer> mRemovedCursorPositions = new ArrayList<Integer>(); + + public DragSortCursorAdapter(Context context, Cursor c) { + super(context, c); + } + + public DragSortCursorAdapter(Context context, Cursor c, boolean autoRequery) { + super(context, c, autoRequery); + } + + public DragSortCursorAdapter(Context context, Cursor c, int flags) { + super(context, c, flags); + } + + /** + * Swaps Cursor and clears list-Cursor mapping. + * + * @see android.widget.CursorAdapter#swapCursor(android.database.Cursor) + */ + @Override + public Cursor swapCursor(Cursor newCursor) { + Cursor old = super.swapCursor(newCursor); + resetMappings(); + return old; + } + + /** + * Changes Cursor and clears list-Cursor mapping. + * + * @see android.widget.CursorAdapter#changeCursor(android.database.Cursor) + */ + @Override + public void changeCursor(Cursor cursor) { + super.changeCursor(cursor); + resetMappings(); + } + + /** + * Resets list-cursor mapping. + */ + public void reset() { + resetMappings(); + notifyDataSetChanged(); + } + + private void resetMappings() { + mListMapping.clear(); + mRemovedCursorPositions.clear(); + } + + @Override + public Object getItem(int position) { + return super.getItem(mListMapping.get(position, position)); + } + + @Override + public long getItemId(int position) { + return super.getItemId(mListMapping.get(position, position)); + } + + @Override + public View getDropDownView(int position, View convertView, ViewGroup parent) { + return super.getDropDownView(mListMapping.get(position, position), convertView, parent); + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + return super.getView(mListMapping.get(position, position), convertView, parent); + } + + /** + * On drop, this updates the mapping between Cursor positions + * and ListView positions. The Cursor is unchanged. Retrieve + * the current mapping with {@link getCursorPositions()}. + * + * @see DragSortListView.DropListener#drop(int, int) + */ + @Override + public void drop(int from, int to) { + if (from != to) { + int cursorFrom = mListMapping.get(from, from); + + if (from > to) { + for (int i = from; i > to; --i) { + mListMapping.put(i, mListMapping.get(i - 1, i - 1)); + } + } else { + for (int i = from; i < to; ++i) { + mListMapping.put(i, mListMapping.get(i + 1, i + 1)); + } + } + mListMapping.put(to, cursorFrom); + + cleanMapping(); + notifyDataSetChanged(); + } + } + + /** + * On remove, this updates the mapping between Cursor positions + * and ListView positions. The Cursor is unchanged. Retrieve + * the current mapping with {@link getCursorPositions()}. + * + * @see DragSortListView.RemoveListener#remove(int) + */ + @Override + public void remove(int which) { + int cursorPos = mListMapping.get(which, which); + if (!mRemovedCursorPositions.contains(cursorPos)) { + mRemovedCursorPositions.add(cursorPos); + } + + int newCount = getCount(); + for (int i = which; i < newCount; ++i) { + mListMapping.put(i, mListMapping.get(i + 1, i + 1)); + } + + mListMapping.delete(newCount); + + cleanMapping(); + notifyDataSetChanged(); + } + + /** + * Does nothing. Just completes DragSortListener interface. + */ + @Override + public void drag(int from, int to) { + // do nothing + } + + /** + * Remove unnecessary mappings from sparse array. + */ + private void cleanMapping() { + ArrayList<Integer> toRemove = new ArrayList<Integer>(); + + int size = mListMapping.size(); + for (int i = 0; i < size; ++i) { + if (mListMapping.keyAt(i) == mListMapping.valueAt(i)) { + toRemove.add(mListMapping.keyAt(i)); + } + } + + size = toRemove.size(); + for (int i = 0; i < size; ++i) { + mListMapping.delete(toRemove.get(i)); + } + } + + @Override + public int getCount() { + return super.getCount() - mRemovedCursorPositions.size(); + } + + /** + * Get the Cursor position mapped to by the provided list position + * (given all previously handled drag-sort + * operations). + * + * @param position List position + * + * @return The mapped-to Cursor position + */ + public int getCursorPosition(int position) { + return mListMapping.get(position, position); + } + + /** + * Get the current order of Cursor positions presented by the + * list. + */ + public ArrayList<Integer> getCursorPositions() { + ArrayList<Integer> result = new ArrayList<Integer>(); + + for (int i = 0; i < getCount(); ++i) { + result.add(mListMapping.get(i, i)); + } + + return result; + } + + /** + * Get the list position mapped to by the provided Cursor position. + * If the provided Cursor position has been removed by a drag-sort, + * this returns {@link #REMOVED}. + * + * @param cursorPosition A Cursor position + * @return The mapped-to list position or REMOVED + */ + public int getListPosition(int cursorPosition) { + if (mRemovedCursorPositions.contains(cursorPosition)) { + return REMOVED; + } + + int index = mListMapping.indexOfValue(cursorPosition); + if (index < 0) { + return cursorPosition; + } else { + return mListMapping.keyAt(index); + } + } + + +} diff --git a/library/drag-sort-listview/src/main/java/com/mobeta/android/dslv/DragSortItemView.java b/library/drag-sort-listview/src/main/java/com/mobeta/android/dslv/DragSortItemView.java new file mode 100644 index 000000000..cef7b82bb --- /dev/null +++ b/library/drag-sort-listview/src/main/java/com/mobeta/android/dslv/DragSortItemView.java @@ -0,0 +1,100 @@ +package com.mobeta.android.dslv; + +import android.content.Context; +import android.view.Gravity; +import android.view.View; +import android.view.View.MeasureSpec; +import android.view.ViewGroup; +import android.widget.AbsListView; +import android.util.Log; + +/** + * Lightweight ViewGroup that wraps list items obtained from user's + * ListAdapter. ItemView expects a single child that has a definite + * height (i.e. the child's layout height is not MATCH_PARENT). + * The width of + * ItemView will always match the width of its child (that is, + * the width MeasureSpec given to ItemView is passed directly + * to the child, and the ItemView measured width is set to the + * child's measured width). The height of ItemView can be anything; + * the + * + * + * The purpose of this class is to optimize slide + * shuffle animations. + */ +public class DragSortItemView extends ViewGroup { + + private int mGravity = Gravity.TOP; + + public DragSortItemView(Context context) { + super(context); + + // always init with standard ListView layout params + setLayoutParams(new AbsListView.LayoutParams( + ViewGroup.LayoutParams.FILL_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT)); + + //setClipChildren(true); + } + + public void setGravity(int gravity) { + mGravity = gravity; + } + + public int getGravity() { + return mGravity; + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + final View child = getChildAt(0); + + if (child == null) { + return; + } + + if (mGravity == Gravity.TOP) { + child.layout(0, 0, getMeasuredWidth(), child.getMeasuredHeight()); + } else { + child.layout(0, getMeasuredHeight() - child.getMeasuredHeight(), getMeasuredWidth(), getMeasuredHeight()); + } + } + + /** + * + */ + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + + int height = MeasureSpec.getSize(heightMeasureSpec); + int width = MeasureSpec.getSize(widthMeasureSpec); + + int heightMode = MeasureSpec.getMode(heightMeasureSpec); + + final View child = getChildAt(0); + if (child == null) { + setMeasuredDimension(0, width); + return; + } + + if (child.isLayoutRequested()) { + // Always let child be as tall as it wants. + measureChild(child, widthMeasureSpec, + MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); + } + + if (heightMode == MeasureSpec.UNSPECIFIED) { + ViewGroup.LayoutParams lp = getLayoutParams(); + + if (lp.height > 0) { + height = lp.height; + } else { + height = child.getMeasuredHeight(); + } + } + + setMeasuredDimension(width, height); + } + +} diff --git a/library/drag-sort-listview/src/main/java/com/mobeta/android/dslv/DragSortItemViewCheckable.java b/library/drag-sort-listview/src/main/java/com/mobeta/android/dslv/DragSortItemViewCheckable.java new file mode 100644 index 000000000..27d612e01 --- /dev/null +++ b/library/drag-sort-listview/src/main/java/com/mobeta/android/dslv/DragSortItemViewCheckable.java @@ -0,0 +1,55 @@ +package com.mobeta.android.dslv; + +import android.content.Context; +import android.view.Gravity; +import android.view.View; +import android.view.View.MeasureSpec; +import android.view.ViewGroup; +import android.widget.AbsListView; +import android.widget.Checkable; +import android.util.Log; + +/** + * Lightweight ViewGroup that wraps list items obtained from user's + * ListAdapter. ItemView expects a single child that has a definite + * height (i.e. the child's layout height is not MATCH_PARENT). + * The width of + * ItemView will always match the width of its child (that is, + * the width MeasureSpec given to ItemView is passed directly + * to the child, and the ItemView measured width is set to the + * child's measured width). The height of ItemView can be anything; + * the + * + * + * The purpose of this class is to optimize slide + * shuffle animations. + */ +public class DragSortItemViewCheckable extends DragSortItemView implements Checkable { + + public DragSortItemViewCheckable(Context context) { + super(context); + } + + @Override + public boolean isChecked() { + View child = getChildAt(0); + if (child instanceof Checkable) + return ((Checkable) child).isChecked(); + else + return false; + } + + @Override + public void setChecked(boolean checked) { + View child = getChildAt(0); + if (child instanceof Checkable) + ((Checkable) child).setChecked(checked); + } + + @Override + public void toggle() { + View child = getChildAt(0); + if (child instanceof Checkable) + ((Checkable) child).toggle(); + } +} diff --git a/library/drag-sort-listview/src/main/java/com/mobeta/android/dslv/DragSortListView.java b/library/drag-sort-listview/src/main/java/com/mobeta/android/dslv/DragSortListView.java new file mode 100644 index 000000000..4f8ec744d --- /dev/null +++ b/library/drag-sort-listview/src/main/java/com/mobeta/android/dslv/DragSortListView.java @@ -0,0 +1,3073 @@ +/* + * DragSortListView. + * + * A subclass of the Android ListView component that enables drag + * and drop re-ordering of list items. + * + * Copyright 2012 Carl Bauer + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.mobeta.android.dslv; + +import android.content.Context; +import android.content.res.TypedArray; +import android.database.DataSetObserver; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Point; +import android.graphics.drawable.Drawable; +import android.os.Environment; +import android.os.SystemClock; +import android.util.AttributeSet; +import android.util.Log; +import android.util.SparseBooleanArray; +import android.util.SparseIntArray; +import android.view.Gravity; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AbsListView; +import android.widget.BaseAdapter; +import android.widget.Checkable; +import android.widget.ListAdapter; +import android.widget.ListView; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.util.ArrayList; + +/** + * ListView subclass that mediates drag and drop resorting of items. + * + * + * @author heycosmo + * + */ +public class DragSortListView extends ListView { + + + /** + * The View that floats above the ListView and represents + * the dragged item. + */ + private View mFloatView; + + /** + * The float View location. First based on touch location + * and given deltaX and deltaY. Then restricted by callback + * to FloatViewManager.onDragFloatView(). Finally restricted + * by bounds of DSLV. + */ + private Point mFloatLoc = new Point(); + + private Point mTouchLoc = new Point(); + + /** + * The middle (in the y-direction) of the floating View. + */ + private int mFloatViewMid; + + /** + * Flag to make sure float View isn't measured twice + */ + private boolean mFloatViewOnMeasured = false; + + /** + * Watch the Adapter for data changes. Cancel a drag if + * coincident with a change. + */ + private DataSetObserver mObserver; + + /** + * Transparency for the floating View (XML attribute). + */ + private float mFloatAlpha = 1.0f; + private float mCurrFloatAlpha = 1.0f; + + /** + * While drag-sorting, the current position of the floating + * View. If dropped, the dragged item will land in this position. + */ + private int mFloatPos; + + /** + * The first expanded ListView position that helps represent + * the drop slot tracking the floating View. + */ + private int mFirstExpPos; + + /** + * The second expanded ListView position that helps represent + * the drop slot tracking the floating View. This can equal + * mFirstExpPos if there is no slide shuffle occurring; otherwise + * it is equal to mFirstExpPos + 1. + */ + private int mSecondExpPos; + + /** + * Flag set if slide shuffling is enabled. + */ + private boolean mAnimate = false; + + /** + * The user dragged from this position. + */ + private int mSrcPos; + + /** + * Offset (in x) within the dragged item at which the user + * picked it up (or first touched down with the digitalis). + */ + private int mDragDeltaX; + + /** + * Offset (in y) within the dragged item at which the user + * picked it up (or first touched down with the digitalis). + */ + private int mDragDeltaY; + + + /** + * The difference (in x) between screen coordinates and coordinates + * in this view. + */ + private int mOffsetX; + + /** + * The difference (in y) between screen coordinates and coordinates + * in this view. + */ + private int mOffsetY; + + /** + * A listener that receives callbacks whenever the floating View + * hovers over a new position. + */ + private DragListener mDragListener; + + /** + * A listener that receives a callback when the floating View + * is dropped. + */ + private DropListener mDropListener; + + /** + * A listener that receives a callback when the floating View + * (or more precisely the originally dragged item) is removed + * by one of the provided gestures. + */ + private RemoveListener mRemoveListener; + + /** + * Enable/Disable item dragging + * + * @attr name dslv:drag_enabled + */ + private boolean mDragEnabled = true; + + /** + * Drag state enum. + */ + private final static int IDLE = 0; + private final static int REMOVING = 1; + private final static int DROPPING = 2; + private final static int STOPPED = 3; + private final static int DRAGGING = 4; + + private int mDragState = IDLE; + + /** + * Height in pixels to which the originally dragged item + * is collapsed during a drag-sort. Currently, this value + * must be greater than zero. + */ + private int mItemHeightCollapsed = 1; + + /** + * Height of the floating View. Stored for the purpose of + * providing the tracking drop slot. + */ + private int mFloatViewHeight; + + /** + * Convenience member. See above. + */ + private int mFloatViewHeightHalf; + + /** + * Save the given width spec for use in measuring children + */ + private int mWidthMeasureSpec = 0; + + /** + * Sample Views ultimately used for calculating the height + * of ListView items that are off-screen. + */ + private View[] mSampleViewTypes = new View[1]; + + /** + * Drag-scroll encapsulator! + */ + private DragScroller mDragScroller; + + /** + * Determines the start of the upward drag-scroll region + * at the top of the ListView. Specified by a fraction + * of the ListView height, thus screen resolution agnostic. + */ + private float mDragUpScrollStartFrac = 1.0f / 3.0f; + + /** + * Determines the start of the downward drag-scroll region + * at the bottom of the ListView. Specified by a fraction + * of the ListView height, thus screen resolution agnostic. + */ + private float mDragDownScrollStartFrac = 1.0f / 3.0f; + + /** + * The following are calculated from the above fracs. + */ + private int mUpScrollStartY; + private int mDownScrollStartY; + private float mDownScrollStartYF; + private float mUpScrollStartYF; + + /** + * Calculated from above above and current ListView height. + */ + private float mDragUpScrollHeight; + + /** + * Calculated from above above and current ListView height. + */ + private float mDragDownScrollHeight; + + /** + * Maximum drag-scroll speed in pixels per ms. Only used with + * default linear drag-scroll profile. + */ + private float mMaxScrollSpeed = 0.5f; + + /** + * Defines the scroll speed during a drag-scroll. User can + * provide their own; this default is a simple linear profile + * where scroll speed increases linearly as the floating View + * nears the top/bottom of the ListView. + */ + private DragScrollProfile mScrollProfile = new DragScrollProfile() { + @Override + public float getSpeed(float w, long t) { + return mMaxScrollSpeed * w; + } + }; + + /** + * Current touch x. + */ + private int mX; + + /** + * Current touch y. + */ + private int mY; + + /** + * Last touch x. + */ + private int mLastX; + + /** + * Last touch y. + */ + private int mLastY; + + /** + * The touch y-coord at which drag started + */ + private int mDragStartY; + + /** + * Drag flag bit. Floating View can move in the positive + * x direction. + */ + public final static int DRAG_POS_X = 0x1; + + /** + * Drag flag bit. Floating View can move in the negative + * x direction. + */ + public final static int DRAG_NEG_X = 0x2; + + /** + * Drag flag bit. Floating View can move in the positive + * y direction. This is subtle. What this actually means is + * that, if enabled, the floating View can be dragged below its starting + * position. Remove in favor of upper-bounding item position? + */ + public final static int DRAG_POS_Y = 0x4; + + /** + * Drag flag bit. Floating View can move in the negative + * y direction. This is subtle. What this actually means is + * that the floating View can be dragged above its starting + * position. Remove in favor of lower-bounding item position? + */ + public final static int DRAG_NEG_Y = 0x8; + + /** + * Flags that determine limits on the motion of the + * floating View. See flags above. + */ + private int mDragFlags = 0; + + /** + * Last call to an on*TouchEvent was a call to + * onInterceptTouchEvent. + */ + private boolean mLastCallWasIntercept = false; + + /** + * A touch event is in progress. + */ + private boolean mInTouchEvent = false; + + /** + * Let the user customize the floating View. + */ + private FloatViewManager mFloatViewManager = null; + + /** + * Given to ListView to cancel its action when a drag-sort + * begins. + */ + private MotionEvent mCancelEvent; + + /** + * Enum telling where to cancel the ListView action when a + * drag-sort begins + */ + private static final int NO_CANCEL = 0; + private static final int ON_TOUCH_EVENT = 1; + private static final int ON_INTERCEPT_TOUCH_EVENT = 2; + + /** + * Where to cancel the ListView action when a + * drag-sort begins + */ + private int mCancelMethod = NO_CANCEL; + + /** + * Determines when a slide shuffle animation starts. That is, + * defines how close to the edge of the drop slot the floating + * View must be to initiate the slide. + */ + private float mSlideRegionFrac = 0.25f; + + /** + * Number between 0 and 1 indicating the relative location of + * a sliding item (only used if drag-sort animations + * are turned on). Nearly 1 means the item is + * at the top of the slide region (nearly full blank item + * is directly below). + */ + private float mSlideFrac = 0.0f; + + /** + * Wraps the user-provided ListAdapter. This is used to wrap each + * item View given by the user inside another View (currenly + * a RelativeLayout) which + * expands and collapses to simulate the item shuffling. + */ + private AdapterWrapper mAdapterWrapper; + + /** + * Turn on custom debugger. + */ + private boolean mTrackDragSort = false; + + /** + * Debugging class. + */ + private DragSortTracker mDragSortTracker; + + /** + * Needed for adjusting item heights from within layoutChildren + */ + private boolean mBlockLayoutRequests = false; + + /** + * Set to true when a down event happens during drag sort; + * for example, when drag finish animations are + * playing. + */ + private boolean mIgnoreTouchEvent = false; + + /** + * Caches DragSortItemView child heights. Sometimes DSLV has to + * know the height of an offscreen item. Since ListView virtualizes + * these, DSLV must get the item from the ListAdapter to obtain + * its height. That process can be expensive, but often the same + * offscreen item will be requested many times in a row. Once an + * offscreen item height is calculated, we cache it in this guy. + * Actually, we cache the height of the child of the + * DragSortItemView since the item height changes often during a + * drag-sort. + */ + private static final int sCacheSize = 3; + private HeightCache mChildHeightCache = new HeightCache(sCacheSize); + + private RemoveAnimator mRemoveAnimator; + + private LiftAnimator mLiftAnimator; + + private DropAnimator mDropAnimator; + + private boolean mUseRemoveVelocity; + private float mRemoveVelocityX = 0; + + public DragSortListView(Context context, AttributeSet attrs) { + super(context, attrs); + + int defaultDuration = 150; + int removeAnimDuration = defaultDuration; // ms + int dropAnimDuration = defaultDuration; // ms + + if (attrs != null) { + TypedArray a = getContext().obtainStyledAttributes(attrs, + R.styleable.DragSortListView, 0, 0); + + mItemHeightCollapsed = Math.max(1, a.getDimensionPixelSize( + R.styleable.DragSortListView_collapsed_height, 1)); + + mTrackDragSort = a.getBoolean( + R.styleable.DragSortListView_track_drag_sort, false); + + if (mTrackDragSort) { + mDragSortTracker = new DragSortTracker(); + } + + // alpha between 0 and 255, 0=transparent, 255=opaque + mFloatAlpha = a.getFloat(R.styleable.DragSortListView_float_alpha, mFloatAlpha); + mCurrFloatAlpha = mFloatAlpha; + + mDragEnabled = a.getBoolean(R.styleable.DragSortListView_drag_enabled, mDragEnabled); + + mSlideRegionFrac = Math.max(0.0f, + Math.min(1.0f, 1.0f - a.getFloat( + R.styleable.DragSortListView_slide_shuffle_speed, + 0.75f))); + + mAnimate = mSlideRegionFrac > 0.0f; + + float frac = a.getFloat( + R.styleable.DragSortListView_drag_scroll_start, + mDragUpScrollStartFrac); + + setDragScrollStart(frac); + + mMaxScrollSpeed = a.getFloat( + R.styleable.DragSortListView_max_drag_scroll_speed, + mMaxScrollSpeed); + + removeAnimDuration = a.getInt( + R.styleable.DragSortListView_remove_animation_duration, + removeAnimDuration); + + dropAnimDuration = a.getInt( + R.styleable.DragSortListView_drop_animation_duration, + dropAnimDuration); + + boolean useDefault = a.getBoolean( + R.styleable.DragSortListView_use_default_controller, + true); + + if (useDefault) { + boolean removeEnabled = a.getBoolean( + R.styleable.DragSortListView_remove_enabled, + false); + int removeMode = a.getInt( + R.styleable.DragSortListView_remove_mode, + DragSortController.FLING_REMOVE); + boolean sortEnabled = a.getBoolean( + R.styleable.DragSortListView_sort_enabled, + true); + int dragInitMode = a.getInt( + R.styleable.DragSortListView_drag_start_mode, + DragSortController.ON_DOWN); + int dragHandleId = a.getResourceId( + R.styleable.DragSortListView_drag_handle_id, + 0); + int flingHandleId = a.getResourceId( + R.styleable.DragSortListView_fling_handle_id, + 0); + int clickRemoveId = a.getResourceId( + R.styleable.DragSortListView_click_remove_id, + 0); + int bgColor = a.getColor( + R.styleable.DragSortListView_float_background_color, + Color.BLACK); + + DragSortController controller = new DragSortController( + this, dragHandleId, dragInitMode, removeMode, + clickRemoveId, flingHandleId); + controller.setRemoveEnabled(removeEnabled); + controller.setSortEnabled(sortEnabled); + controller.setBackgroundColor(bgColor); + + mFloatViewManager = controller; + setOnTouchListener(controller); + } + + a.recycle(); + } + + mDragScroller = new DragScroller(); + + float smoothness = 0.5f; + if (removeAnimDuration > 0) { + mRemoveAnimator = new RemoveAnimator(smoothness, removeAnimDuration); + } + // mLiftAnimator = new LiftAnimator(smoothness, 100); + if (dropAnimDuration > 0) { + mDropAnimator = new DropAnimator(smoothness, dropAnimDuration); + } + + mCancelEvent = MotionEvent.obtain(0, 0, MotionEvent.ACTION_CANCEL, 0f, 0f, 0f, 0f, 0, 0f, + 0f, 0, 0); + + // construct the dataset observer + mObserver = new DataSetObserver() { + private void cancel() { + if (mDragState == DRAGGING) { + cancelDrag(); + } + } + + @Override + public void onChanged() { + cancel(); + } + + @Override + public void onInvalidated() { + cancel(); + } + }; + } + + /** + * Usually called from a FloatViewManager. The float alpha + * will be reset to the xml-defined value every time a drag + * is stopped. + */ + public void setFloatAlpha(float alpha) { + mCurrFloatAlpha = alpha; + } + + public float getFloatAlpha() { + return mCurrFloatAlpha; + } + + /** + * Set maximum drag scroll speed in positions/second. Only applies + * if using default ScrollSpeedProfile. + * + * @param max Maximum scroll speed. + */ + public void setMaxScrollSpeed(float max) { + mMaxScrollSpeed = max; + } + + /** + * For each DragSortListView Listener interface implemented by + * <code>adapter</code>, this method calls the appropriate + * set*Listener method with <code>adapter</code> as the argument. + * + * @param adapter The ListAdapter providing data to back + * DragSortListView. + * + * @see android.widget.ListView#setAdapter(android.widget.ListAdapter) + */ + @Override + public void setAdapter(ListAdapter adapter) { + if (adapter != null) { + mAdapterWrapper = new AdapterWrapper(adapter); + adapter.registerDataSetObserver(mObserver); + + if (adapter instanceof DropListener) { + setDropListener((DropListener) adapter); + } + if (adapter instanceof DragListener) { + setDragListener((DragListener) adapter); + } + if (adapter instanceof RemoveListener) { + setRemoveListener((RemoveListener) adapter); + } + } else { + mAdapterWrapper = null; + } + + super.setAdapter(mAdapterWrapper); + } + + /** + * As opposed to {@link ListView#getAdapter()}, which returns + * a heavily wrapped ListAdapter (DragSortListView wraps the + * input ListAdapter {\emph and} ListView wraps the wrapped one). + * + * @return The ListAdapter set as the argument of {@link setAdapter()} + */ + public ListAdapter getInputAdapter() { + if (mAdapterWrapper == null) { + return null; + } else { + return mAdapterWrapper.getAdapter(); + } + } + + private class AdapterWrapper extends BaseAdapter { + private ListAdapter mAdapter; + + public AdapterWrapper(ListAdapter adapter) { + super(); + mAdapter = adapter; + + mAdapter.registerDataSetObserver(new DataSetObserver() { + public void onChanged() { + notifyDataSetChanged(); + } + + public void onInvalidated() { + notifyDataSetInvalidated(); + } + }); + } + + public ListAdapter getAdapter() { + return mAdapter; + } + + @Override + public long getItemId(int position) { + return mAdapter.getItemId(position); + } + + @Override + public Object getItem(int position) { + return mAdapter.getItem(position); + } + + @Override + public int getCount() { + return mAdapter.getCount(); + } + + @Override + public boolean areAllItemsEnabled() { + return mAdapter.areAllItemsEnabled(); + } + + @Override + public boolean isEnabled(int position) { + return mAdapter.isEnabled(position); + } + + @Override + public int getItemViewType(int position) { + return mAdapter.getItemViewType(position); + } + + @Override + public int getViewTypeCount() { + return mAdapter.getViewTypeCount(); + } + + @Override + public boolean hasStableIds() { + return mAdapter.hasStableIds(); + } + + @Override + public boolean isEmpty() { + return mAdapter.isEmpty(); + } + + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + + DragSortItemView v; + View child; + // Log.d("mobeta", + // "getView: position="+position+" convertView="+convertView); + if (convertView != null) { + v = (DragSortItemView) convertView; + View oldChild = v.getChildAt(0); + + child = mAdapter.getView(position, oldChild, DragSortListView.this); + if (child != oldChild) { + // shouldn't get here if user is reusing convertViews + // properly + if (oldChild != null) { + v.removeViewAt(0); + } + v.addView(child); + } + } else { + child = mAdapter.getView(position, null, DragSortListView.this); + if (child instanceof Checkable) { + v = new DragSortItemViewCheckable(getContext()); + } else { + v = new DragSortItemView(getContext()); + } + v.setLayoutParams(new AbsListView.LayoutParams( + ViewGroup.LayoutParams.FILL_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT)); + v.addView(child); + } + + // Set the correct item height given drag state; passed + // View needs to be measured if measurement is required. + adjustItem(position + getHeaderViewsCount(), v, true); + + return v; + } + } + + private void drawDivider(int expPosition, Canvas canvas) { + + final Drawable divider = getDivider(); + final int dividerHeight = getDividerHeight(); + // Log.d("mobeta", "div="+divider+" divH="+dividerHeight); + + if (divider != null && dividerHeight != 0) { + final ViewGroup expItem = (ViewGroup) getChildAt(expPosition + - getFirstVisiblePosition()); + if (expItem != null) { + final int l = getPaddingLeft(); + final int r = getWidth() - getPaddingRight(); + final int t; + final int b; + + final int childHeight = expItem.getChildAt(0).getHeight(); + + if (expPosition > mSrcPos) { + t = expItem.getTop() + childHeight; + b = t + dividerHeight; + } else { + b = expItem.getBottom() - childHeight; + t = b - dividerHeight; + } + // Log.d("mobeta", "l="+l+" t="+t+" r="+r+" b="+b); + + // Have to clip to support ColorDrawable on <= Gingerbread + canvas.save(); + canvas.clipRect(l, t, r, b); + divider.setBounds(l, t, r, b); + divider.draw(canvas); + canvas.restore(); + } + } + } + + @Override + protected void dispatchDraw(Canvas canvas) { + super.dispatchDraw(canvas); + + if (mDragState != IDLE) { + // draw the divider over the expanded item + if (mFirstExpPos != mSrcPos) { + drawDivider(mFirstExpPos, canvas); + } + if (mSecondExpPos != mFirstExpPos && mSecondExpPos != mSrcPos) { + drawDivider(mSecondExpPos, canvas); + } + } + + if (mFloatView != null) { + // draw the float view over everything + final int w = mFloatView.getWidth(); + final int h = mFloatView.getHeight(); + + int x = mFloatLoc.x; + + int width = getWidth(); + if (x < 0) + x = -x; + float alphaMod; + if (x < width) { + alphaMod = ((float) (width - x)) / ((float) width); + alphaMod *= alphaMod; + } else { + alphaMod = 0; + } + + final int alpha = (int) (255f * mCurrFloatAlpha * alphaMod); + + canvas.save(); + // Log.d("mobeta", "clip rect bounds: " + canvas.getClipBounds()); + canvas.translate(mFloatLoc.x, mFloatLoc.y); + canvas.clipRect(0, 0, w, h); + + // Log.d("mobeta", "clip rect bounds: " + canvas.getClipBounds()); + canvas.saveLayerAlpha(0, 0, w, h, alpha, Canvas.ALL_SAVE_FLAG); + mFloatView.draw(canvas); + canvas.restore(); + canvas.restore(); + } + } + + private int getItemHeight(int position) { + View v = getChildAt(position - getFirstVisiblePosition()); + + if (v != null) { + // item is onscreen, just get the height of the View + return v.getHeight(); + } else { + // item is offscreen. get child height and calculate + // item height based on current shuffle state + return calcItemHeight(position, getChildHeight(position)); + } + } + + private void printPosData() { + Log.d("mobeta", "mSrcPos=" + mSrcPos + " mFirstExpPos=" + mFirstExpPos + " mSecondExpPos=" + + mSecondExpPos); + } + + private class HeightCache { + + private SparseIntArray mMap; + private ArrayList<Integer> mOrder; + private int mMaxSize; + + public HeightCache(int size) { + mMap = new SparseIntArray(size); + mOrder = new ArrayList<Integer>(size); + mMaxSize = size; + } + + /** + * Add item height at position if doesn't already exist. + */ + public void add(int position, int height) { + int currHeight = mMap.get(position, -1); + if (currHeight != height) { + if (currHeight == -1) { + if (mMap.size() == mMaxSize) { + // remove oldest entry + mMap.delete(mOrder.remove(0)); + } + } else { + // move position to newest slot + mOrder.remove((Integer) position); + } + mMap.put(position, height); + mOrder.add(position); + } + } + + public int get(int position) { + return mMap.get(position, -1); + } + + public void clear() { + mMap.clear(); + mOrder.clear(); + } + + } + + /** + * Get the shuffle edge for item at position when top of + * item is at y-coord top. Assumes that current item heights + * are consistent with current float view location and + * thus expanded positions and slide fraction. i.e. Should not be + * called between update of expanded positions/slide fraction + * and layoutChildren. + * + * @param position + * @param top + * @param height Height of item at position. If -1, this function + * calculates this height. + * + * @return Shuffle line between position-1 and position (for + * the given view of the list; that is, for when top of item at + * position has y-coord of given `top`). If + * floating View (treated as horizontal line) is dropped + * immediately above this line, it lands in position-1. If + * dropped immediately below this line, it lands in position. + */ + private int getShuffleEdge(int position, int top) { + + final int numHeaders = getHeaderViewsCount(); + final int numFooters = getFooterViewsCount(); + + // shuffle edges are defined between items that can be + // dragged; there are N-1 of them if there are N draggable + // items. + + if (position <= numHeaders || (position >= getCount() - numFooters)) { + return top; + } + + int divHeight = getDividerHeight(); + + int edge; + + int maxBlankHeight = mFloatViewHeight - mItemHeightCollapsed; + int childHeight = getChildHeight(position); + int itemHeight = getItemHeight(position); + + // first calculate top of item given that floating View is + // centered over src position + int otop = top; + if (mSecondExpPos <= mSrcPos) { + // items are expanded on and/or above the source position + + if (position == mSecondExpPos && mFirstExpPos != mSecondExpPos) { + if (position == mSrcPos) { + otop = top + itemHeight - mFloatViewHeight; + } else { + int blankHeight = itemHeight - childHeight; + otop = top + blankHeight - maxBlankHeight; + } + } else if (position > mSecondExpPos && position <= mSrcPos) { + otop = top - maxBlankHeight; + } + + } else { + // items are expanded on and/or below the source position + + if (position > mSrcPos && position <= mFirstExpPos) { + otop = top + maxBlankHeight; + } else if (position == mSecondExpPos && mFirstExpPos != mSecondExpPos) { + int blankHeight = itemHeight - childHeight; + otop = top + blankHeight; + } + } + + // otop is set + if (position <= mSrcPos) { + edge = otop + (mFloatViewHeight - divHeight - getChildHeight(position - 1)) / 2; + } else { + edge = otop + (childHeight - divHeight - mFloatViewHeight) / 2; + } + + return edge; + } + + private boolean updatePositions() { + + final int first = getFirstVisiblePosition(); + int startPos = mFirstExpPos; + View startView = getChildAt(startPos - first); + + if (startView == null) { + startPos = first + getChildCount() / 2; + startView = getChildAt(startPos - first); + } + int startTop = startView.getTop(); + + int itemHeight = startView.getHeight(); + + int edge = getShuffleEdge(startPos, startTop); + int lastEdge = edge; + + int divHeight = getDividerHeight(); + + // Log.d("mobeta", "float mid="+mFloatViewMid); + + int itemPos = startPos; + int itemTop = startTop; + if (mFloatViewMid < edge) { + // scanning up for float position + // Log.d("mobeta", " edge="+edge); + while (itemPos >= 0) { + itemPos--; + itemHeight = getItemHeight(itemPos); + + if (itemPos == 0) { + edge = itemTop - divHeight - itemHeight; + break; + } + + itemTop -= itemHeight + divHeight; + edge = getShuffleEdge(itemPos, itemTop); + // Log.d("mobeta", " edge="+edge); + + if (mFloatViewMid >= edge) { + break; + } + + lastEdge = edge; + } + } else { + // scanning down for float position + // Log.d("mobeta", " edge="+edge); + final int count = getCount(); + while (itemPos < count) { + if (itemPos == count - 1) { + edge = itemTop + divHeight + itemHeight; + break; + } + + itemTop += divHeight + itemHeight; + itemHeight = getItemHeight(itemPos + 1); + edge = getShuffleEdge(itemPos + 1, itemTop); + // Log.d("mobeta", " edge="+edge); + + // test for hit + if (mFloatViewMid < edge) { + break; + } + + lastEdge = edge; + itemPos++; + } + } + + final int numHeaders = getHeaderViewsCount(); + final int numFooters = getFooterViewsCount(); + + boolean updated = false; + + int oldFirstExpPos = mFirstExpPos; + int oldSecondExpPos = mSecondExpPos; + float oldSlideFrac = mSlideFrac; + + if (mAnimate) { + int edgeToEdge = Math.abs(edge - lastEdge); + + int edgeTop, edgeBottom; + if (mFloatViewMid < edge) { + edgeBottom = edge; + edgeTop = lastEdge; + } else { + edgeTop = edge; + edgeBottom = lastEdge; + } + // Log.d("mobeta", "edgeTop="+edgeTop+" edgeBot="+edgeBottom); + + int slideRgnHeight = (int) (0.5f * mSlideRegionFrac * edgeToEdge); + float slideRgnHeightF = (float) slideRgnHeight; + int slideEdgeTop = edgeTop + slideRgnHeight; + int slideEdgeBottom = edgeBottom - slideRgnHeight; + + // Three regions + if (mFloatViewMid < slideEdgeTop) { + mFirstExpPos = itemPos - 1; + mSecondExpPos = itemPos; + mSlideFrac = 0.5f * ((float) (slideEdgeTop - mFloatViewMid)) / slideRgnHeightF; + // Log.d("mobeta", + // "firstExp="+mFirstExpPos+" secExp="+mSecondExpPos+" slideFrac="+mSlideFrac); + } else if (mFloatViewMid < slideEdgeBottom) { + mFirstExpPos = itemPos; + mSecondExpPos = itemPos; + } else { + mFirstExpPos = itemPos; + mSecondExpPos = itemPos + 1; + mSlideFrac = 0.5f * (1.0f + ((float) (edgeBottom - mFloatViewMid)) + / slideRgnHeightF); + // Log.d("mobeta", + // "firstExp="+mFirstExpPos+" secExp="+mSecondExpPos+" slideFrac="+mSlideFrac); + } + + } else { + mFirstExpPos = itemPos; + mSecondExpPos = itemPos; + } + + // correct for headers and footers + if (mFirstExpPos < numHeaders) { + itemPos = numHeaders; + mFirstExpPos = itemPos; + mSecondExpPos = itemPos; + } else if (mSecondExpPos >= getCount() - numFooters) { + itemPos = getCount() - numFooters - 1; + mFirstExpPos = itemPos; + mSecondExpPos = itemPos; + } + + if (mFirstExpPos != oldFirstExpPos || mSecondExpPos != oldSecondExpPos + || mSlideFrac != oldSlideFrac) { + updated = true; + } + + if (itemPos != mFloatPos) { + if (mDragListener != null) { + mDragListener.drag(mFloatPos - numHeaders, itemPos - numHeaders); + } + + mFloatPos = itemPos; + updated = true; + } + + return updated; + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + if (mTrackDragSort) { + mDragSortTracker.appendState(); + } + } + + private class SmoothAnimator implements Runnable { + protected long mStartTime; + + private float mDurationF; + + private float mAlpha; + private float mA, mB, mC, mD; + + private boolean mCanceled; + + public SmoothAnimator(float smoothness, int duration) { + mAlpha = smoothness; + mDurationF = (float) duration; + mA = mD = 1f / (2f * mAlpha * (1f - mAlpha)); + mB = mAlpha / (2f * (mAlpha - 1f)); + mC = 1f / (1f - mAlpha); + } + + public float transform(float frac) { + if (frac < mAlpha) { + return mA * frac * frac; + } else if (frac < 1f - mAlpha) { + return mB + mC * frac; + } else { + return 1f - mD * (frac - 1f) * (frac - 1f); + } + } + + public void start() { + mStartTime = SystemClock.uptimeMillis(); + mCanceled = false; + onStart(); + post(this); + } + + public void cancel() { + mCanceled = true; + } + + public void onStart() { + // stub + } + + public void onUpdate(float frac, float smoothFrac) { + // stub + } + + public void onStop() { + // stub + } + + @Override + public void run() { + if (mCanceled) { + return; + } + + float fraction = ((float) (SystemClock.uptimeMillis() - mStartTime)) / mDurationF; + + if (fraction >= 1f) { + onUpdate(1f, 1f); + onStop(); + } else { + onUpdate(fraction, transform(fraction)); + post(this); + } + } + } + + /** + * Centers floating View under touch point. + */ + private class LiftAnimator extends SmoothAnimator { + + private float mInitDragDeltaY; + private float mFinalDragDeltaY; + + public LiftAnimator(float smoothness, int duration) { + super(smoothness, duration); + } + + @Override + public void onStart() { + mInitDragDeltaY = mDragDeltaY; + mFinalDragDeltaY = mFloatViewHeightHalf; + } + + @Override + public void onUpdate(float frac, float smoothFrac) { + if (mDragState != DRAGGING) { + cancel(); + } else { + mDragDeltaY = (int) (smoothFrac * mFinalDragDeltaY + (1f - smoothFrac) + * mInitDragDeltaY); + mFloatLoc.y = mY - mDragDeltaY; + doDragFloatView(true); + } + } + } + + /** + * Centers floating View over drop slot before destroying. + */ + private class DropAnimator extends SmoothAnimator { + + private int mDropPos; + private int srcPos; + private float mInitDeltaY; + private float mInitDeltaX; + + public DropAnimator(float smoothness, int duration) { + super(smoothness, duration); + } + + @Override + public void onStart() { + mDropPos = mFloatPos; + srcPos = mSrcPos; + mDragState = DROPPING; + mInitDeltaY = mFloatLoc.y - getTargetY(); + mInitDeltaX = mFloatLoc.x - getPaddingLeft(); + } + + private int getTargetY() { + final int first = getFirstVisiblePosition(); + final int otherAdjust = (mItemHeightCollapsed + getDividerHeight()) / 2; + View v = getChildAt(mDropPos - first); + int targetY = -1; + if (v != null) { + if (mDropPos == srcPos) { + targetY = v.getTop(); + } else if (mDropPos < srcPos) { + // expanded down + targetY = v.getTop() - otherAdjust; + } else { + // expanded up + targetY = v.getBottom() + otherAdjust - mFloatViewHeight; + } + } else { + // drop position is not on screen?? no animation + cancel(); + } + + return targetY; + } + + @Override + public void onUpdate(float frac, float smoothFrac) { + final int targetY = getTargetY(); + final int targetX = getPaddingLeft(); + final float deltaY = mFloatLoc.y - targetY; + final float deltaX = mFloatLoc.x - targetX; + final float f = 1f - smoothFrac; + if (f < Math.abs(deltaY / mInitDeltaY) || f < Math.abs(deltaX / mInitDeltaX)) { + mFloatLoc.y = targetY + (int) (mInitDeltaY * f); + mFloatLoc.x = getPaddingLeft() + (int) (mInitDeltaX * f); + doDragFloatView(true); + } + } + + @Override + public void onStop() { + dropFloatView(); + } + + } + + /** + * Collapses expanded items. + */ + private class RemoveAnimator extends SmoothAnimator { + + private float mFloatLocX; + private float mFirstStartBlank; + private float mSecondStartBlank; + + private int mFirstChildHeight = -1; + private int mSecondChildHeight = -1; + + private int mFirstPos; + private int mSecondPos; + private int srcPos; + + public RemoveAnimator(float smoothness, int duration) { + super(smoothness, duration); + } + + @Override + public void onStart() { + mFirstChildHeight = -1; + mSecondChildHeight = -1; + mFirstPos = mFirstExpPos; + mSecondPos = mSecondExpPos; + srcPos = mSrcPos; + mDragState = REMOVING; + + mFloatLocX = mFloatLoc.x; + if (mUseRemoveVelocity) { + float minVelocity = 2f * getWidth(); + if (mRemoveVelocityX == 0) { + mRemoveVelocityX = (mFloatLocX < 0 ? -1 : 1) * minVelocity; + } else { + minVelocity *= 2; + if (mRemoveVelocityX < 0 && mRemoveVelocityX > -minVelocity) + mRemoveVelocityX = -minVelocity; + else if (mRemoveVelocityX > 0 && mRemoveVelocityX < minVelocity) + mRemoveVelocityX = minVelocity; + } + } else { + destroyFloatView(); + } + } + + @Override + public void onUpdate(float frac, float smoothFrac) { + float f = 1f - smoothFrac; + + final int firstVis = getFirstVisiblePosition(); + View item = getChildAt(mFirstPos - firstVis); + ViewGroup.LayoutParams lp; + int blank; + + if (mUseRemoveVelocity) { + float dt = (float) (SystemClock.uptimeMillis() - mStartTime) / 1000; + if (dt == 0) + return; + float dx = mRemoveVelocityX * dt; + int w = getWidth(); + mRemoveVelocityX += (mRemoveVelocityX > 0 ? 1 : -1) * dt * w; + mFloatLocX += dx; + mFloatLoc.x = (int) mFloatLocX; + if (mFloatLocX < w && mFloatLocX > -w) { + mStartTime = SystemClock.uptimeMillis(); + doDragFloatView(true); + return; + } + } + + if (item != null) { + if (mFirstChildHeight == -1) { + mFirstChildHeight = getChildHeight(mFirstPos, item, false); + mFirstStartBlank = (float) (item.getHeight() - mFirstChildHeight); + } + blank = Math.max((int) (f * mFirstStartBlank), 1); + lp = item.getLayoutParams(); + lp.height = mFirstChildHeight + blank; + item.setLayoutParams(lp); + } + if (mSecondPos != mFirstPos) { + item = getChildAt(mSecondPos - firstVis); + if (item != null) { + if (mSecondChildHeight == -1) { + mSecondChildHeight = getChildHeight(mSecondPos, item, false); + mSecondStartBlank = (float) (item.getHeight() - mSecondChildHeight); + } + blank = Math.max((int) (f * mSecondStartBlank), 1); + lp = item.getLayoutParams(); + lp.height = mSecondChildHeight + blank; + item.setLayoutParams(lp); + } + } + } + + @Override + public void onStop() { + doRemoveItem(); + } + } + + public void removeItem(int which) { + + mUseRemoveVelocity = false; + removeItem(which, 0); + } + + /** + * Removes an item from the list and animates the removal. + * + * @param which Position to remove (NOTE: headers/footers ignored! + * this is a position in your input ListAdapter). + * @param velocityX + */ + public void removeItem(int which, float velocityX) { + if (mDragState == IDLE || mDragState == DRAGGING) { + + if (mDragState == IDLE) { + // called from outside drag-sort + mSrcPos = getHeaderViewsCount() + which; + mFirstExpPos = mSrcPos; + mSecondExpPos = mSrcPos; + mFloatPos = mSrcPos; + View v = getChildAt(mSrcPos - getFirstVisiblePosition()); + if (v != null) { + v.setVisibility(View.INVISIBLE); + } + } + + mDragState = REMOVING; + mRemoveVelocityX = velocityX; + + if (mInTouchEvent) { + switch (mCancelMethod) { + case ON_TOUCH_EVENT: + super.onTouchEvent(mCancelEvent); + break; + case ON_INTERCEPT_TOUCH_EVENT: + super.onInterceptTouchEvent(mCancelEvent); + break; + } + } + + if (mRemoveAnimator != null) { + mRemoveAnimator.start(); + } else { + doRemoveItem(which); + } + } + } + + /** + * Move an item, bypassing the drag-sort process. Simply calls + * through to {@link DropListener#drop(int, int)}. + * + * @param from Position to move (NOTE: headers/footers ignored! + * this is a position in your input ListAdapter). + * @param to Target position (NOTE: headers/footers ignored! + * this is a position in your input ListAdapter). + */ + public void moveItem(int from, int to) { + if (mDropListener != null) { + final int count = getInputAdapter().getCount(); + if (from >= 0 && from < count && to >= 0 && to < count) { + mDropListener.drop(from, to); + } + } + } + + /** + * Cancel a drag. Calls {@link #stopDrag(boolean, boolean)} with + * <code>true</code> as the first argument. + */ + public void cancelDrag() { + if (mDragState == DRAGGING) { + mDragScroller.stopScrolling(true); + destroyFloatView(); + clearPositions(); + adjustAllItems(); + + if (mInTouchEvent) { + mDragState = STOPPED; + } else { + mDragState = IDLE; + } + } + } + + private void clearPositions() { + mSrcPos = -1; + mFirstExpPos = -1; + mSecondExpPos = -1; + mFloatPos = -1; + } + + private void dropFloatView() { + // must set to avoid cancelDrag being called from the + // DataSetObserver + mDragState = DROPPING; + + if (mDropListener != null && mFloatPos >= 0 && mFloatPos < getCount()) { + final int numHeaders = getHeaderViewsCount(); + mDropListener.drop(mSrcPos - numHeaders, mFloatPos - numHeaders); + } + + destroyFloatView(); + + adjustOnReorder(); + clearPositions(); + adjustAllItems(); + + // now the drag is done + if (mInTouchEvent) { + mDragState = STOPPED; + } else { + mDragState = IDLE; + } + } + + private void doRemoveItem() { + doRemoveItem(mSrcPos - getHeaderViewsCount()); + } + + /** + * Removes dragged item from the list. Calls RemoveListener. + */ + private void doRemoveItem(int which) { + // must set to avoid cancelDrag being called from the + // DataSetObserver + mDragState = REMOVING; + + // end it + if (mRemoveListener != null) { + mRemoveListener.remove(which); + } + + destroyFloatView(); + + adjustOnReorder(); + clearPositions(); + + // now the drag is done + if (mInTouchEvent) { + mDragState = STOPPED; + } else { + mDragState = IDLE; + } + } + + private void adjustOnReorder() { + final int firstPos = getFirstVisiblePosition(); + // Log.d("mobeta", "first="+firstPos+" src="+mSrcPos); + if (mSrcPos < firstPos) { + // collapsed src item is off screen; + // adjust the scroll after item heights have been fixed + View v = getChildAt(0); + int top = 0; + if (v != null) { + top = v.getTop(); + } + // Log.d("mobeta", "top="+top+" fvh="+mFloatViewHeight); + setSelectionFromTop(firstPos - 1, top - getPaddingTop()); + } + } + + /** + * Stop a drag in progress. Pass <code>true</code> if you would + * like to remove the dragged item from the list. + * + * @param remove Remove the dragged item from the list. Calls + * a registered RemoveListener, if one exists. Otherwise, calls + * the DropListener, if one exists. + * + * @return True if the stop was successful. False if there is + * no floating View. + */ + public boolean stopDrag(boolean remove) { + mUseRemoveVelocity = false; + return stopDrag(remove, 0); + } + + public boolean stopDragWithVelocity(boolean remove, float velocityX) { + + mUseRemoveVelocity = true; + return stopDrag(remove, velocityX); + } + + public boolean stopDrag(boolean remove, float velocityX) { + if (mFloatView != null) { + mDragScroller.stopScrolling(true); + + if (remove) { + removeItem(mSrcPos - getHeaderViewsCount(), velocityX); + } else { + if (mDropAnimator != null) { + mDropAnimator.start(); + } else { + dropFloatView(); + } + } + + if (mTrackDragSort) { + mDragSortTracker.stopTracking(); + } + + return true; + } else { + // stop failed + return false; + } + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + if (mIgnoreTouchEvent) { + mIgnoreTouchEvent = false; + return false; + } + + if (!mDragEnabled) { + return super.onTouchEvent(ev); + } + + boolean more = false; + + boolean lastCallWasIntercept = mLastCallWasIntercept; + mLastCallWasIntercept = false; + + if (!lastCallWasIntercept) { + saveTouchCoords(ev); + } + + // if (mFloatView != null) { + if (mDragState == DRAGGING) { + onDragTouchEvent(ev); + more = true; // give us more! + } else { + // what if float view is null b/c we dropped in middle + // of drag touch event? + + // if (mDragState != STOPPED) { + if (mDragState == IDLE) { + if (super.onTouchEvent(ev)) { + more = true; + } + } + + int action = ev.getAction() & MotionEvent.ACTION_MASK; + + switch (action) { + case MotionEvent.ACTION_CANCEL: + case MotionEvent.ACTION_UP: + doActionUpOrCancel(); + break; + default: + if (more) { + mCancelMethod = ON_TOUCH_EVENT; + } + } + } + + return more; + } + + private void doActionUpOrCancel() { + mCancelMethod = NO_CANCEL; + mInTouchEvent = false; + if (mDragState == STOPPED) { + mDragState = IDLE; + } + mCurrFloatAlpha = mFloatAlpha; + mListViewIntercepted = false; + mChildHeightCache.clear(); + } + + private void saveTouchCoords(MotionEvent ev) { + int action = ev.getAction() & MotionEvent.ACTION_MASK; + if (action != MotionEvent.ACTION_DOWN) { + mLastX = mX; + mLastY = mY; + } + mX = (int) ev.getX(); + mY = (int) ev.getY(); + if (action == MotionEvent.ACTION_DOWN) { + mLastX = mX; + mLastY = mY; + } + mOffsetX = (int) ev.getRawX() - mX; + mOffsetY = (int) ev.getRawY() - mY; + } + + public boolean listViewIntercepted() { + return mListViewIntercepted; + } + + private boolean mListViewIntercepted = false; + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + if (!mDragEnabled) { + return super.onInterceptTouchEvent(ev); + } + + saveTouchCoords(ev); + mLastCallWasIntercept = true; + + int action = ev.getAction() & MotionEvent.ACTION_MASK; + + if (action == MotionEvent.ACTION_DOWN) { + if (mDragState != IDLE) { + // intercept and ignore + mIgnoreTouchEvent = true; + return true; + } + mInTouchEvent = true; + } + + boolean intercept = false; + + // the following deals with calls to super.onInterceptTouchEvent + if (mFloatView != null) { + // super's touch event canceled in startDrag + intercept = true; + } else { + if (super.onInterceptTouchEvent(ev)) { + mListViewIntercepted = true; + intercept = true; + } + + switch (action) { + case MotionEvent.ACTION_CANCEL: + case MotionEvent.ACTION_UP: + doActionUpOrCancel(); + break; + default: + if (intercept) { + mCancelMethod = ON_TOUCH_EVENT; + } else { + mCancelMethod = ON_INTERCEPT_TOUCH_EVENT; + } + } + } + + if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) { + mInTouchEvent = false; + } + + return intercept; + } + + /** + * Set the width of each drag scroll region by specifying + * a fraction of the ListView height. + * + * @param heightFraction Fraction of ListView height. Capped at + * 0.5f. + * + */ + public void setDragScrollStart(float heightFraction) { + setDragScrollStarts(heightFraction, heightFraction); + } + + /** + * Set the width of each drag scroll region by specifying + * a fraction of the ListView height. + * + * @param upperFrac Fraction of ListView height for up-scroll bound. + * Capped at 0.5f. + * @param lowerFrac Fraction of ListView height for down-scroll bound. + * Capped at 0.5f. + * + */ + public void setDragScrollStarts(float upperFrac, float lowerFrac) { + if (lowerFrac > 0.5f) { + mDragDownScrollStartFrac = 0.5f; + } else { + mDragDownScrollStartFrac = lowerFrac; + } + + if (upperFrac > 0.5f) { + mDragUpScrollStartFrac = 0.5f; + } else { + mDragUpScrollStartFrac = upperFrac; + } + + if (getHeight() != 0) { + updateScrollStarts(); + } + } + + private void continueDrag(int x, int y) { + + // proposed position + mFloatLoc.x = x - mDragDeltaX; + mFloatLoc.y = y - mDragDeltaY; + + doDragFloatView(true); + + int minY = Math.min(y, mFloatViewMid + mFloatViewHeightHalf); + int maxY = Math.max(y, mFloatViewMid - mFloatViewHeightHalf); + + // get the current scroll direction + int currentScrollDir = mDragScroller.getScrollDir(); + + if (minY > mLastY && minY > mDownScrollStartY && currentScrollDir != DragScroller.DOWN) { + // dragged down, it is below the down scroll start and it is not + // scrolling up + + if (currentScrollDir != DragScroller.STOP) { + // moved directly from up scroll to down scroll + mDragScroller.stopScrolling(true); + } + + // start scrolling down + mDragScroller.startScrolling(DragScroller.DOWN); + } else if (maxY < mLastY && maxY < mUpScrollStartY && currentScrollDir != DragScroller.UP) { + // dragged up, it is above the up scroll start and it is not + // scrolling up + + if (currentScrollDir != DragScroller.STOP) { + // moved directly from down scroll to up scroll + mDragScroller.stopScrolling(true); + } + + // start scrolling up + mDragScroller.startScrolling(DragScroller.UP); + } + else if (maxY >= mUpScrollStartY && minY <= mDownScrollStartY + && mDragScroller.isScrolling()) { + // not in the upper nor in the lower drag-scroll regions but it is + // still scrolling + + mDragScroller.stopScrolling(true); + } + } + + private void updateScrollStarts() { + final int padTop = getPaddingTop(); + final int listHeight = getHeight() - padTop - getPaddingBottom(); + float heightF = (float) listHeight; + + mUpScrollStartYF = padTop + mDragUpScrollStartFrac * heightF; + mDownScrollStartYF = padTop + (1.0f - mDragDownScrollStartFrac) * heightF; + + mUpScrollStartY = (int) mUpScrollStartYF; + mDownScrollStartY = (int) mDownScrollStartYF; + + mDragUpScrollHeight = mUpScrollStartYF - padTop; + mDragDownScrollHeight = padTop + listHeight - mDownScrollStartYF; + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + updateScrollStarts(); + } + + private void adjustAllItems() { + final int first = getFirstVisiblePosition(); + final int last = getLastVisiblePosition(); + + int begin = Math.max(0, getHeaderViewsCount() - first); + int end = Math.min(last - first, getCount() - 1 - getFooterViewsCount() - first); + + for (int i = begin; i <= end; ++i) { + View v = getChildAt(i); + if (v != null) { + adjustItem(first + i, v, false); + } + } + } + + private void adjustItem(int position) { + View v = getChildAt(position - getFirstVisiblePosition()); + + if (v != null) { + adjustItem(position, v, false); + } + } + + /** + * Sets layout param height, gravity, and visibility on + * wrapped item. + */ + private void adjustItem(int position, View v, boolean invalidChildHeight) { + + // Adjust item height + ViewGroup.LayoutParams lp = v.getLayoutParams(); + int height; + if (position != mSrcPos && position != mFirstExpPos && position != mSecondExpPos) { + height = ViewGroup.LayoutParams.WRAP_CONTENT; + } else { + height = calcItemHeight(position, v, invalidChildHeight); + } + + if (height != lp.height) { + lp.height = height; + v.setLayoutParams(lp); + } + + // Adjust item gravity + if (position == mFirstExpPos || position == mSecondExpPos) { + if (position < mSrcPos) { + ((DragSortItemView) v).setGravity(Gravity.BOTTOM); + } else if (position > mSrcPos) { + ((DragSortItemView) v).setGravity(Gravity.TOP); + } + } + + // Finally adjust item visibility + + int oldVis = v.getVisibility(); + int vis = View.VISIBLE; + + if (position == mSrcPos && mFloatView != null) { + vis = View.INVISIBLE; + } + + if (vis != oldVis) { + v.setVisibility(vis); + } + } + + private int getChildHeight(int position) { + if (position == mSrcPos) { + return 0; + } + + View v = getChildAt(position - getFirstVisiblePosition()); + + if (v != null) { + // item is onscreen, therefore child height is valid, + // hence the "true" + return getChildHeight(position, v, false); + } else { + // item is offscreen + // first check cache for child height at this position + int childHeight = mChildHeightCache.get(position); + if (childHeight != -1) { + // Log.d("mobeta", "found child height in cache!"); + return childHeight; + } + + final ListAdapter adapter = getAdapter(); + int type = adapter.getItemViewType(position); + + // There might be a better place for checking for the following + final int typeCount = adapter.getViewTypeCount(); + if (typeCount != mSampleViewTypes.length) { + mSampleViewTypes = new View[typeCount]; + } + + if (type >= 0) { + if (mSampleViewTypes[type] == null) { + v = adapter.getView(position, null, this); + mSampleViewTypes[type] = v; + } else { + v = adapter.getView(position, mSampleViewTypes[type], this); + } + } else { + // type is HEADER_OR_FOOTER or IGNORE + v = adapter.getView(position, null, this); + } + + // current child height is invalid, hence "true" below + childHeight = getChildHeight(position, v, true); + + // cache it because this could have been expensive + mChildHeightCache.add(position, childHeight); + + return childHeight; + } + } + + private int getChildHeight(int position, View item, boolean invalidChildHeight) { + if (position == mSrcPos) { + return 0; + } + + View child; + if (position < getHeaderViewsCount() || position >= getCount() - getFooterViewsCount()) { + child = item; + } else { + child = ((ViewGroup) item).getChildAt(0); + } + + ViewGroup.LayoutParams lp = child.getLayoutParams(); + + if (lp != null) { + if (lp.height > 0) { + return lp.height; + } + } + + int childHeight = child.getHeight(); + + if (childHeight == 0 || invalidChildHeight) { + measureItem(child); + childHeight = child.getMeasuredHeight(); + } + + return childHeight; + } + + private int calcItemHeight(int position, View item, boolean invalidChildHeight) { + return calcItemHeight(position, getChildHeight(position, item, invalidChildHeight)); + } + + private int calcItemHeight(int position, int childHeight) { + + int divHeight = getDividerHeight(); + + boolean isSliding = mAnimate && mFirstExpPos != mSecondExpPos; + int maxNonSrcBlankHeight = mFloatViewHeight - mItemHeightCollapsed; + int slideHeight = (int) (mSlideFrac * maxNonSrcBlankHeight); + + int height; + + if (position == mSrcPos) { + if (mSrcPos == mFirstExpPos) { + if (isSliding) { + height = slideHeight + mItemHeightCollapsed; + } else { + height = mFloatViewHeight; + } + } else if (mSrcPos == mSecondExpPos) { + // if gets here, we know an item is sliding + height = mFloatViewHeight - slideHeight; + } else { + height = mItemHeightCollapsed; + } + } else if (position == mFirstExpPos) { + if (isSliding) { + height = childHeight + slideHeight; + } else { + height = childHeight + maxNonSrcBlankHeight; + } + } else if (position == mSecondExpPos) { + // we know an item is sliding (b/c 2ndPos != 1stPos) + height = childHeight + maxNonSrcBlankHeight - slideHeight; + } else { + height = childHeight; + } + + return height; + } + + @Override + public void requestLayout() { + if (!mBlockLayoutRequests) { + super.requestLayout(); + } + } + + private int adjustScroll(int movePos, View moveItem, int oldFirstExpPos, int oldSecondExpPos) { + int adjust = 0; + + final int childHeight = getChildHeight(movePos); + + int moveHeightBefore = moveItem.getHeight(); + int moveHeightAfter = calcItemHeight(movePos, childHeight); + + int moveBlankBefore = moveHeightBefore; + int moveBlankAfter = moveHeightAfter; + if (movePos != mSrcPos) { + moveBlankBefore -= childHeight; + moveBlankAfter -= childHeight; + } + + int maxBlank = mFloatViewHeight; + if (mSrcPos != mFirstExpPos && mSrcPos != mSecondExpPos) { + maxBlank -= mItemHeightCollapsed; + } + + if (movePos <= oldFirstExpPos) { + if (movePos > mFirstExpPos) { + adjust += maxBlank - moveBlankAfter; + } + } else if (movePos == oldSecondExpPos) { + if (movePos <= mFirstExpPos) { + adjust += moveBlankBefore - maxBlank; + } else if (movePos == mSecondExpPos) { + adjust += moveHeightBefore - moveHeightAfter; + } else { + adjust += moveBlankBefore; + } + } else { + if (movePos <= mFirstExpPos) { + adjust -= maxBlank; + } else if (movePos == mSecondExpPos) { + adjust -= moveBlankAfter; + } + } + + return adjust; + } + + private void measureItem(View item) { + ViewGroup.LayoutParams lp = item.getLayoutParams(); + if (lp == null) { + lp = new AbsListView.LayoutParams(ViewGroup.LayoutParams.FILL_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); + item.setLayoutParams(lp); + } + int wspec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec, getListPaddingLeft() + + getListPaddingRight(), lp.width); + int hspec; + if (lp.height > 0) { + hspec = MeasureSpec.makeMeasureSpec(lp.height, MeasureSpec.EXACTLY); + } else { + hspec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); + } + item.measure(wspec, hspec); + } + + private void measureFloatView() { + if (mFloatView != null) { + measureItem(mFloatView); + mFloatViewHeight = mFloatView.getMeasuredHeight(); + mFloatViewHeightHalf = mFloatViewHeight / 2; + } + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + // Log.d("mobeta", "onMeasure called"); + if (mFloatView != null) { + if (mFloatView.isLayoutRequested()) { + measureFloatView(); + } + mFloatViewOnMeasured = true; // set to false after layout + } + mWidthMeasureSpec = widthMeasureSpec; + } + + @Override + protected void layoutChildren() { + super.layoutChildren(); + + if (mFloatView != null) { + if (mFloatView.isLayoutRequested() && !mFloatViewOnMeasured) { + // Have to measure here when usual android measure + // pass is skipped. This happens during a drag-sort + // when layoutChildren is called directly. + measureFloatView(); + } + mFloatView.layout(0, 0, mFloatView.getMeasuredWidth(), mFloatView.getMeasuredHeight()); + mFloatViewOnMeasured = false; + } + } + + protected boolean onDragTouchEvent(MotionEvent ev) { + // we are in a drag + int action = ev.getAction() & MotionEvent.ACTION_MASK; + + switch (ev.getAction() & MotionEvent.ACTION_MASK) { + case MotionEvent.ACTION_CANCEL: + if (mDragState == DRAGGING) { + cancelDrag(); + } + doActionUpOrCancel(); + break; + case MotionEvent.ACTION_UP: + // Log.d("mobeta", "calling stopDrag from onDragTouchEvent"); + if (mDragState == DRAGGING) { + stopDrag(false); + } + doActionUpOrCancel(); + break; + case MotionEvent.ACTION_MOVE: + continueDrag((int) ev.getX(), (int) ev.getY()); + break; + } + + return true; + } + + private boolean mFloatViewInvalidated = false; + + private void invalidateFloatView() { + mFloatViewInvalidated = true; + } + + /** + * Start a drag of item at <code>position</code> using the + * registered FloatViewManager. Calls through + * to {@link #startDrag(int,View,int,int,int)} after obtaining + * the floating View from the FloatViewManager. + * + * @param position Item to drag. + * @param dragFlags Flags that restrict some movements of the + * floating View. For example, set <code>dragFlags |= + * ~{@link #DRAG_NEG_X}</code> to allow dragging the floating + * View in all directions except off the screen to the left. + * @param deltaX Offset in x of the touch coordinate from the + * left edge of the floating View (i.e. touch-x minus float View + * left). + * @param deltaY Offset in y of the touch coordinate from the + * top edge of the floating View (i.e. touch-y minus float View + * top). + * + * @return True if the drag was started, false otherwise. This + * <code>startDrag</code> will fail if we are not currently in + * a touch event, there is no registered FloatViewManager, + * or the FloatViewManager returns a null View. + */ + public boolean startDrag(int position, int dragFlags, int deltaX, int deltaY) { + if (!mInTouchEvent || mFloatViewManager == null) { + return false; + } + + View v = mFloatViewManager.onCreateFloatView(position); + + if (v == null) { + return false; + } else { + return startDrag(position, v, dragFlags, deltaX, deltaY); + } + + } + + /** + * Start a drag of item at <code>position</code> without using + * a FloatViewManager. + * + * @param position Item to drag. + * @param floatView Floating View. + * @param dragFlags Flags that restrict some movements of the + * floating View. For example, set <code>dragFlags |= + * ~{@link #DRAG_NEG_X}</code> to allow dragging the floating + * View in all directions except off the screen to the left. + * @param deltaX Offset in x of the touch coordinate from the + * left edge of the floating View (i.e. touch-x minus float View + * left). + * @param deltaY Offset in y of the touch coordinate from the + * top edge of the floating View (i.e. touch-y minus float View + * top). + * + * @return True if the drag was started, false otherwise. This + * <code>startDrag</code> will fail if we are not currently in + * a touch event, <code>floatView</code> is null, or there is + * a drag in progress. + */ + public boolean startDrag(int position, View floatView, int dragFlags, int deltaX, int deltaY) { + if (mDragState != IDLE || !mInTouchEvent || mFloatView != null || floatView == null + || !mDragEnabled) { + return false; + } + + if (getParent() != null) { + getParent().requestDisallowInterceptTouchEvent(true); + } + + int pos = position + getHeaderViewsCount(); + mFirstExpPos = pos; + mSecondExpPos = pos; + mSrcPos = pos; + mFloatPos = pos; + + // mDragState = dragType; + mDragState = DRAGGING; + mDragFlags = 0; + mDragFlags |= dragFlags; + + mFloatView = floatView; + measureFloatView(); // sets mFloatViewHeight + + mDragDeltaX = deltaX; + mDragDeltaY = deltaY; + mDragStartY = mY; + + // updateFloatView(mX - mDragDeltaX, mY - mDragDeltaY); + mFloatLoc.x = mX - mDragDeltaX; + mFloatLoc.y = mY - mDragDeltaY; + + // set src item invisible + final View srcItem = getChildAt(mSrcPos - getFirstVisiblePosition()); + + if (srcItem != null) { + srcItem.setVisibility(View.INVISIBLE); + } + + if (mTrackDragSort) { + mDragSortTracker.startTracking(); + } + + // once float view is created, events are no longer passed + // to ListView + switch (mCancelMethod) { + case ON_TOUCH_EVENT: + super.onTouchEvent(mCancelEvent); + break; + case ON_INTERCEPT_TOUCH_EVENT: + super.onInterceptTouchEvent(mCancelEvent); + break; + } + + requestLayout(); + + if (mLiftAnimator != null) { + mLiftAnimator.start(); + } + + return true; + } + + private void doDragFloatView(boolean forceInvalidate) { + int movePos = getFirstVisiblePosition() + getChildCount() / 2; + View moveItem = getChildAt(getChildCount() / 2); + + if (moveItem == null) { + return; + } + + doDragFloatView(movePos, moveItem, forceInvalidate); + } + + private void doDragFloatView(int movePos, View moveItem, boolean forceInvalidate) { + mBlockLayoutRequests = true; + + updateFloatView(); + + int oldFirstExpPos = mFirstExpPos; + int oldSecondExpPos = mSecondExpPos; + + boolean updated = updatePositions(); + + if (updated) { + adjustAllItems(); + int scroll = adjustScroll(movePos, moveItem, oldFirstExpPos, oldSecondExpPos); + // Log.d("mobeta", " adjust scroll="+scroll); + + setSelectionFromTop(movePos, moveItem.getTop() + scroll - getPaddingTop()); + layoutChildren(); + } + + if (updated || forceInvalidate) { + invalidate(); + } + + mBlockLayoutRequests = false; + } + + /** + * Sets float View location based on suggested values and + * constraints set in mDragFlags. + */ + private void updateFloatView() { + + if (mFloatViewManager != null) { + mTouchLoc.set(mX, mY); + mFloatViewManager.onDragFloatView(mFloatView, mFloatLoc, mTouchLoc); + } + + final int floatX = mFloatLoc.x; + final int floatY = mFloatLoc.y; + + // restrict x motion + int padLeft = getPaddingLeft(); + if ((mDragFlags & DRAG_POS_X) == 0 && floatX > padLeft) { + mFloatLoc.x = padLeft; + } else if ((mDragFlags & DRAG_NEG_X) == 0 && floatX < padLeft) { + mFloatLoc.x = padLeft; + } + + // keep floating view from going past bottom of last header view + final int numHeaders = getHeaderViewsCount(); + final int numFooters = getFooterViewsCount(); + final int firstPos = getFirstVisiblePosition(); + final int lastPos = getLastVisiblePosition(); + + // Log.d("mobeta", + // "nHead="+numHeaders+" nFoot="+numFooters+" first="+firstPos+" last="+lastPos); + int topLimit = getPaddingTop(); + if (firstPos < numHeaders) { + topLimit = getChildAt(numHeaders - firstPos - 1).getBottom(); + } + if ((mDragFlags & DRAG_NEG_Y) == 0) { + if (firstPos <= mSrcPos) { + topLimit = Math.max(getChildAt(mSrcPos - firstPos).getTop(), topLimit); + } + } + // bottom limit is top of first footer View or + // bottom of last item in list + int bottomLimit = getHeight() - getPaddingBottom(); + if (lastPos >= getCount() - numFooters - 1) { + bottomLimit = getChildAt(getCount() - numFooters - 1 - firstPos).getBottom(); + } + if ((mDragFlags & DRAG_POS_Y) == 0) { + if (lastPos >= mSrcPos) { + bottomLimit = Math.min(getChildAt(mSrcPos - firstPos).getBottom(), bottomLimit); + } + } + + // Log.d("mobeta", "dragView top=" + (y - mDragDeltaY)); + // Log.d("mobeta", "limit=" + limit); + // Log.d("mobeta", "mDragDeltaY=" + mDragDeltaY); + + if (floatY < topLimit) { + mFloatLoc.y = topLimit; + } else if (floatY + mFloatViewHeight > bottomLimit) { + mFloatLoc.y = bottomLimit - mFloatViewHeight; + } + + // get y-midpoint of floating view (constrained to ListView bounds) + mFloatViewMid = mFloatLoc.y + mFloatViewHeightHalf; + } + + private void destroyFloatView() { + if (mFloatView != null) { + mFloatView.setVisibility(GONE); + if (mFloatViewManager != null) { + mFloatViewManager.onDestroyFloatView(mFloatView); + } + mFloatView = null; + invalidate(); + } + } + + /** + * Interface for customization of the floating View appearance + * and dragging behavior. Implement + * your own and pass it to {@link #setFloatViewManager}. If + * your own is not passed, the default {@link SimpleFloatViewManager} + * implementation is used. + */ + public interface FloatViewManager { + /** + * Return the floating View for item at <code>position</code>. + * DragSortListView will measure and layout this View for you, + * so feel free to just inflate it. You can help DSLV by + * setting some {@link ViewGroup.LayoutParams} on this View; + * otherwise it will set some for you (with a width of FILL_PARENT + * and a height of WRAP_CONTENT). + * + * @param position Position of item to drag (NOTE: + * <code>position</code> excludes header Views; thus, if you + * want to call {@link ListView#getChildAt(int)}, you will need + * to add {@link ListView#getHeaderViewsCount()} to the index). + * + * @return The View you wish to display as the floating View. + */ + public View onCreateFloatView(int position); + + /** + * Called whenever the floating View is dragged. Float View + * properties can be changed here. Also, the upcoming location + * of the float View can be altered by setting + * <code>location.x</code> and <code>location.y</code>. + * + * @param floatView The floating View. + * @param location The location (top-left; relative to DSLV + * top-left) at which the float + * View would like to appear, given the current touch location + * and the offset provided in {@link DragSortListView#startDrag}. + * @param touch The current touch location (relative to DSLV + * top-left). + * @param pendingScroll + */ + public void onDragFloatView(View floatView, Point location, Point touch); + + /** + * Called when the float View is dropped; lets you perform + * any necessary cleanup. The internal DSLV floating View + * reference is set to null immediately after this is called. + * + * @param floatView The floating View passed to + * {@link #onCreateFloatView(int)}. + */ + public void onDestroyFloatView(View floatView); + } + + public void setFloatViewManager(FloatViewManager manager) { + mFloatViewManager = manager; + } + + public void setDragListener(DragListener l) { + mDragListener = l; + } + + /** + * Allows for easy toggling between a DragSortListView + * and a regular old ListView. If enabled, items are + * draggable, where the drag init mode determines how + * items are lifted (see {@link setDragInitMode(int)}). + * If disabled, items cannot be dragged. + * + * @param enabled Set <code>true</code> to enable list + * item dragging + */ + public void setDragEnabled(boolean enabled) { + mDragEnabled = enabled; + } + + public boolean isDragEnabled() { + return mDragEnabled; + } + + /** + * This better reorder your ListAdapter! DragSortListView does not do this + * for you; doesn't make sense to. Make sure + * {@link BaseAdapter#notifyDataSetChanged()} or something like it is called + * in your implementation. Furthermore, if you have a choiceMode other than + * none and the ListAdapter does not return true for + * {@link ListAdapter#hasStableIds()}, you will need to call + * {@link #moveCheckState(int, int)} to move the check boxes along with the + * list items. + * + * @param l + */ + public void setDropListener(DropListener l) { + mDropListener = l; + } + + /** + * Probably a no-brainer, but make sure that your remove listener + * calls {@link BaseAdapter#notifyDataSetChanged()} or something like it. + * When an item removal occurs, DragSortListView + * relies on a redraw of all the items to recover invisible views + * and such. Strictly speaking, if you remove something, your dataset + * has changed... + * + * @param l + */ + public void setRemoveListener(RemoveListener l) { + mRemoveListener = l; + } + + public interface DragListener { + public void drag(int from, int to); + } + + /** + * Your implementation of this has to reorder your ListAdapter! + * Make sure to call + * {@link BaseAdapter#notifyDataSetChanged()} or something like it + * in your implementation. + * + * @author heycosmo + * + */ + public interface DropListener { + public void drop(int from, int to); + } + + /** + * Make sure to call + * {@link BaseAdapter#notifyDataSetChanged()} or something like it + * in your implementation. + * + * @author heycosmo + * + */ + public interface RemoveListener { + public void remove(int which); + } + + public interface DragSortListener extends DropListener, DragListener, RemoveListener { + } + + public void setDragSortListener(DragSortListener l) { + setDropListener(l); + setDragListener(l); + setRemoveListener(l); + } + + /** + * Completely custom scroll speed profile. Default increases linearly + * with position and is constant in time. Create your own by implementing + * {@link DragSortListView.DragScrollProfile}. + * + * @param ssp + */ + public void setDragScrollProfile(DragScrollProfile ssp) { + if (ssp != null) { + mScrollProfile = ssp; + } + } + + /** + * Use this to move the check state of an item from one position to another + * in a drop operation. If you have a choiceMode which is not none, this + * method must be called when the order of items changes in an underlying + * adapter which does not have stable IDs (see + * {@link ListAdapter#hasStableIds()}). This is because without IDs, the + * ListView has no way of knowing which items have moved where, and cannot + * update the check state accordingly. + * <p> + * A word of warning about a "feature" in Android that you may run into when + * dealing with movable list items: for an adapter that <em>does</em> have + * stable IDs, ListView will attempt to locate each item based on its ID and + * move the check state from the item's old position to the new position — + * which is all fine and good (and removes the need for calling this + * function), except for the half-baked approach. Apparently to save time in + * the naive algorithm used, ListView will only search for an ID in the + * close neighborhood of the old position. If the user moves an item too far + * (specifically, more than 20 rows away), ListView will give up and just + * force the item to be unchecked. So if there is a reasonable chance that + * the user will move items more than 20 rows away from the original + * position, you may wish to use an adapter with unstable IDs and call this + * method manually instead. + * + * @param from + * @param to + */ + public void moveCheckState(int from, int to) { + // This method runs in O(n log n) time (n being the number of list + // items). The bottleneck is the call to AbsListView.setItemChecked, + // which is O(log n) because of the binary search involved in calling + // SparseBooleanArray.put(). + // + // To improve on the average time, we minimize the number of calls to + // setItemChecked by only calling it for items that actually have a + // changed state. This is achieved by building a list containing the + // start and end of the "runs" of checked items, and then moving the + // runs. Note that moving an item from A to B is essentially a rotation + // of the range of items in [A, B]. Let's say we have + // . . U V X Y Z . . + // and move U after Z. This is equivalent to a rotation one step to the + // left within the range you are moving across: + // . . V X Y Z U . . + // + // So, to perform the move we enumerate all the runs within the move + // range, then rotate each run one step to the left or right (depending + // on move direction). For example, in the list: + // X X . X X X . X + // we have two runs. One begins at the last item of the list and wraps + // around to the beginning, ending at position 1. The second begins at + // position 3 and ends at position 5. To rotate a run, regardless of + // length, we only need to set a check mark at one end of the run, and + // clear a check mark at the other end: + // X . X X X . X X + SparseBooleanArray cip = getCheckedItemPositions(); + int rangeStart = from; + int rangeEnd = to; + if (to < from) { + rangeStart = to; + rangeEnd = from; + } + rangeEnd += 1; + + int[] runStart = new int[cip.size()]; + int[] runEnd = new int[cip.size()]; + int runCount = buildRunList(cip, rangeStart, rangeEnd, runStart, runEnd); + if (runCount == 1 && (runStart[0] == runEnd[0])) { + // Special case where all items are checked, we can never set any + // item to false like we do below. + return; + } + + if (from < to) { + for (int i = 0; i != runCount; i++) { + setItemChecked(rotate(runStart[i], -1, rangeStart, rangeEnd), true); + setItemChecked(rotate(runEnd[i], -1, rangeStart, rangeEnd), false); + } + + } else { + for (int i = 0; i != runCount; i++) { + setItemChecked(runStart[i], false); + setItemChecked(runEnd[i], true); + } + } + } + + /** + * Use this when an item has been deleted, to move the check state of all + * following items up one step. If you have a choiceMode which is not none, + * this method must be called when the order of items changes in an + * underlying adapter which does not have stable IDs (see + * {@link ListAdapter#hasStableIds()}). This is because without IDs, the + * ListView has no way of knowing which items have moved where, and cannot + * update the check state accordingly. + * + * See also further comments on {@link #moveCheckState(int, int)}. + * + * @param position + */ + public void removeCheckState(int position) { + SparseBooleanArray cip = getCheckedItemPositions(); + + if (cip.size() == 0) + return; + int[] runStart = new int[cip.size()]; + int[] runEnd = new int[cip.size()]; + int rangeStart = position; + int rangeEnd = cip.keyAt(cip.size() - 1) + 1; + int runCount = buildRunList(cip, rangeStart, rangeEnd, runStart, runEnd); + for (int i = 0; i != runCount; i++) { + if (!(runStart[i] == position || (runEnd[i] < runStart[i] && runEnd[i] > position))) { + // Only set a new check mark in front of this run if it does + // not contain the deleted position. If it does, we only need + // to make it one check mark shorter at the end. + setItemChecked(rotate(runStart[i], -1, rangeStart, rangeEnd), true); + } + setItemChecked(rotate(runEnd[i], -1, rangeStart, rangeEnd), false); + } + } + + private static int buildRunList(SparseBooleanArray cip, int rangeStart, + int rangeEnd, int[] runStart, int[] runEnd) { + int runCount = 0; + + int i = findFirstSetIndex(cip, rangeStart, rangeEnd); + if (i == -1) + return 0; + + int position = cip.keyAt(i); + int currentRunStart = position; + int currentRunEnd = currentRunStart + 1; + for (i++; i < cip.size() && (position = cip.keyAt(i)) < rangeEnd; i++) { + if (!cip.valueAt(i)) // not checked => not interesting + continue; + if (position == currentRunEnd) { + currentRunEnd++; + } else { + runStart[runCount] = currentRunStart; + runEnd[runCount] = currentRunEnd; + runCount++; + currentRunStart = position; + currentRunEnd = position + 1; + } + } + + if (currentRunEnd == rangeEnd) { + // rangeStart and rangeEnd are equivalent positions so to be + // consistent we translate them to the same integer value. That way + // we can check whether a run covers the entire range by just + // checking if the start equals the end position. + currentRunEnd = rangeStart; + } + runStart[runCount] = currentRunStart; + runEnd[runCount] = currentRunEnd; + runCount++; + + if (runCount > 1) { + if (runStart[0] == rangeStart && runEnd[runCount - 1] == rangeStart) { + // The last run ends at the end of the range, and the first run + // starts at the beginning of the range. So they are actually + // part of the same run, except they wrap around the end of the + // range. To avoid adjacent runs, we need to merge them. + runStart[0] = runStart[runCount - 1]; + runCount--; + } + } + return runCount; + } + + private static int rotate(int value, int offset, int lowerBound, int upperBound) { + int windowSize = upperBound - lowerBound; + + value += offset; + if (value < lowerBound) { + value += windowSize; + } else if (value >= upperBound) { + value -= windowSize; + } + return value; + } + + private static int findFirstSetIndex(SparseBooleanArray sba, int rangeStart, int rangeEnd) { + int size = sba.size(); + int i = insertionIndexForKey(sba, rangeStart); + while (i < size && sba.keyAt(i) < rangeEnd && !sba.valueAt(i)) + i++; + if (i == size || sba.keyAt(i) >= rangeEnd) + return -1; + return i; + } + + private static int insertionIndexForKey(SparseBooleanArray sba, int key) { + int low = 0; + int high = sba.size(); + while (high - low > 0) { + int middle = (low + high) >> 1; + if (sba.keyAt(middle) < key) + low = middle + 1; + else + high = middle; + } + return low; + } + + /** + * Interface for controlling + * scroll speed as a function of touch position and time. Use + * {@link DragSortListView#setDragScrollProfile(DragScrollProfile)} to + * set custom profile. + * + * @author heycosmo + * + */ + public interface DragScrollProfile { + /** + * Return a scroll speed in pixels/millisecond. Always return a + * positive number. + * + * @param w Normalized position in scroll region (i.e. w \in [0,1]). + * Small w typically means slow scrolling. + * @param t Time (in milliseconds) since start of scroll (handy if you + * want scroll acceleration). + * @return Scroll speed at position w and time t in pixels/ms. + */ + float getSpeed(float w, long t); + } + + private class DragScroller implements Runnable { + + private boolean mAbort; + + private long mPrevTime; + private long mCurrTime; + + private int dy; + private float dt; + private long tStart; + private int scrollDir; + + public final static int STOP = -1; + public final static int UP = 0; + public final static int DOWN = 1; + + private float mScrollSpeed; // pixels per ms + + private boolean mScrolling = false; + + private int mLastHeader; + private int mFirstFooter; + + public boolean isScrolling() { + return mScrolling; + } + + public int getScrollDir() { + return mScrolling ? scrollDir : STOP; + } + + public DragScroller() { + } + + public void startScrolling(int dir) { + if (!mScrolling) { + // Debug.startMethodTracing("dslv-scroll"); + mAbort = false; + mScrolling = true; + tStart = SystemClock.uptimeMillis(); + mPrevTime = tStart; + scrollDir = dir; + post(this); + } + } + + public void stopScrolling(boolean now) { + if (now) { + DragSortListView.this.removeCallbacks(this); + mScrolling = false; + } else { + mAbort = true; + } + + // Debug.stopMethodTracing(); + } + + @Override + public void run() { + if (mAbort) { + mScrolling = false; + return; + } + + // Log.d("mobeta", "scroll"); + + final int first = getFirstVisiblePosition(); + final int last = getLastVisiblePosition(); + final int count = getCount(); + final int padTop = getPaddingTop(); + final int listHeight = getHeight() - padTop - getPaddingBottom(); + + int minY = Math.min(mY, mFloatViewMid + mFloatViewHeightHalf); + int maxY = Math.max(mY, mFloatViewMid - mFloatViewHeightHalf); + + if (scrollDir == UP) { + View v = getChildAt(0); + // Log.d("mobeta", "vtop="+v.getTop()+" padtop="+padTop); + if (v == null) { + mScrolling = false; + return; + } else { + if (first == 0 && v.getTop() == padTop) { + mScrolling = false; + return; + } + } + mScrollSpeed = mScrollProfile.getSpeed((mUpScrollStartYF - maxY) + / mDragUpScrollHeight, mPrevTime); + } else { + View v = getChildAt(last - first); + if (v == null) { + mScrolling = false; + return; + } else { + if (last == count - 1 && v.getBottom() <= listHeight + padTop) { + mScrolling = false; + return; + } + } + mScrollSpeed = -mScrollProfile.getSpeed((minY - mDownScrollStartYF) + / mDragDownScrollHeight, mPrevTime); + } + + mCurrTime = SystemClock.uptimeMillis(); + dt = (float) (mCurrTime - mPrevTime); + + // dy is change in View position of a list item; i.e. positive dy + // means user is scrolling up (list item moves down the screen, + // remember + // y=0 is at top of View). + dy = (int) Math.round(mScrollSpeed * dt); + + int movePos; + if (dy >= 0) { + dy = Math.min(listHeight, dy); + movePos = first; + } else { + dy = Math.max(-listHeight, dy); + movePos = last; + } + + final View moveItem = getChildAt(movePos - first); + int top = moveItem.getTop() + dy; + + if (movePos == 0 && top > padTop) { + top = padTop; + } + + // always do scroll + mBlockLayoutRequests = true; + + setSelectionFromTop(movePos, top - padTop); + DragSortListView.this.layoutChildren(); + invalidate(); + + mBlockLayoutRequests = false; + + // scroll means relative float View movement + doDragFloatView(movePos, moveItem, false); + + mPrevTime = mCurrTime; + // Log.d("mobeta", " updated prevTime="+mPrevTime); + + post(this); + } + } + + private class DragSortTracker { + StringBuilder mBuilder = new StringBuilder(); + + File mFile; + + private int mNumInBuffer = 0; + private int mNumFlushes = 0; + + private boolean mTracking = false; + + public DragSortTracker() { + File root = Environment.getExternalStorageDirectory(); + mFile = new File(root, "dslv_state.txt"); + + if (!mFile.exists()) { + try { + mFile.createNewFile(); + Log.d("mobeta", "file created"); + } catch (IOException e) { + Log.w("mobeta", "Could not create dslv_state.txt"); + Log.d("mobeta", e.getMessage()); + } + } + + } + + public void startTracking() { + mBuilder.append("<DSLVStates>\n"); + mNumFlushes = 0; + mTracking = true; + } + + public void appendState() { + if (!mTracking) { + return; + } + + mBuilder.append("<DSLVState>\n"); + final int children = getChildCount(); + final int first = getFirstVisiblePosition(); + mBuilder.append(" <Positions>"); + for (int i = 0; i < children; ++i) { + mBuilder.append(first + i).append(","); + } + mBuilder.append("</Positions>\n"); + + mBuilder.append(" <Tops>"); + for (int i = 0; i < children; ++i) { + mBuilder.append(getChildAt(i).getTop()).append(","); + } + mBuilder.append("</Tops>\n"); + mBuilder.append(" <Bottoms>"); + for (int i = 0; i < children; ++i) { + mBuilder.append(getChildAt(i).getBottom()).append(","); + } + mBuilder.append("</Bottoms>\n"); + + mBuilder.append(" <FirstExpPos>").append(mFirstExpPos).append("</FirstExpPos>\n"); + mBuilder.append(" <FirstExpBlankHeight>") + .append(getItemHeight(mFirstExpPos) - getChildHeight(mFirstExpPos)) + .append("</FirstExpBlankHeight>\n"); + mBuilder.append(" <SecondExpPos>").append(mSecondExpPos).append("</SecondExpPos>\n"); + mBuilder.append(" <SecondExpBlankHeight>") + .append(getItemHeight(mSecondExpPos) - getChildHeight(mSecondExpPos)) + .append("</SecondExpBlankHeight>\n"); + mBuilder.append(" <SrcPos>").append(mSrcPos).append("</SrcPos>\n"); + mBuilder.append(" <SrcHeight>").append(mFloatViewHeight + getDividerHeight()) + .append("</SrcHeight>\n"); + mBuilder.append(" <ViewHeight>").append(getHeight()).append("</ViewHeight>\n"); + mBuilder.append(" <LastY>").append(mLastY).append("</LastY>\n"); + mBuilder.append(" <FloatY>").append(mFloatViewMid).append("</FloatY>\n"); + mBuilder.append(" <ShuffleEdges>"); + for (int i = 0; i < children; ++i) { + mBuilder.append(getShuffleEdge(first + i, getChildAt(i).getTop())).append(","); + } + mBuilder.append("</ShuffleEdges>\n"); + + mBuilder.append("</DSLVState>\n"); + mNumInBuffer++; + + if (mNumInBuffer > 1000) { + flush(); + mNumInBuffer = 0; + } + } + + public void flush() { + if (!mTracking) { + return; + } + + // save to file on sdcard + try { + boolean append = true; + if (mNumFlushes == 0) { + append = false; + } + FileWriter writer = new FileWriter(mFile, append); + + writer.write(mBuilder.toString()); + mBuilder.delete(0, mBuilder.length()); + + writer.flush(); + writer.close(); + + mNumFlushes++; + } catch (IOException e) { + // do nothing + } + } + + public void stopTracking() { + if (mTracking) { + mBuilder.append("</DSLVStates>\n"); + flush(); + mTracking = false; + } + } + + } + +} diff --git a/library/drag-sort-listview/src/main/java/com/mobeta/android/dslv/ResourceDragSortCursorAdapter.java b/library/drag-sort-listview/src/main/java/com/mobeta/android/dslv/ResourceDragSortCursorAdapter.java new file mode 100644 index 000000000..f2d08107f --- /dev/null +++ b/library/drag-sort-listview/src/main/java/com/mobeta/android/dslv/ResourceDragSortCursorAdapter.java @@ -0,0 +1,133 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.mobeta.android.dslv; + +import android.content.Context; +import android.database.Cursor; +import android.view.View; +import android.view.ViewGroup; +import android.view.LayoutInflater; + +// taken from v4 rev. 10 ResourceCursorAdapter.java + +/** + * Static library support version of the framework's {@link android.widget.ResourceCursorAdapter}. + * Used to write apps that run on platforms prior to Android 3.0. When running + * on Android 3.0 or above, this implementation is still used; it does not try + * to switch to the framework's implementation. See the framework SDK + * documentation for a class overview. + */ +public abstract class ResourceDragSortCursorAdapter extends DragSortCursorAdapter { + private int mLayout; + + private int mDropDownLayout; + + private LayoutInflater mInflater; + + /** + * Constructor the enables auto-requery. + * + * @deprecated This option is discouraged, as it results in Cursor queries + * being performed on the application's UI thread and thus can cause poor + * responsiveness or even Application Not Responding errors. As an alternative, + * use {@link android.app.LoaderManager} with a {@link android.content.CursorLoader}. + * + * @param context The context where the ListView associated with this adapter is running + * @param layout resource identifier of a layout file that defines the views + * for this list item. Unless you override them later, this will + * define both the item views and the drop down views. + */ + @Deprecated + public ResourceDragSortCursorAdapter(Context context, int layout, Cursor c) { + super(context, c); + mLayout = mDropDownLayout = layout; + mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + } + + /** + * Constructor with default behavior as per + * {@link CursorAdapter#CursorAdapter(Context, Cursor, boolean)}; it is recommended + * you not use this, but instead {@link #ResourceCursorAdapter(Context, int, Cursor, int)}. + * When using this constructor, {@link #FLAG_REGISTER_CONTENT_OBSERVER} + * will always be set. + * + * @param context The context where the ListView associated with this adapter is running + * @param layout resource identifier of a layout file that defines the views + * for this list item. Unless you override them later, this will + * define both the item views and the drop down views. + * @param c The cursor from which to get the data. + * @param autoRequery If true the adapter will call requery() on the + * cursor whenever it changes so the most recent + * data is always displayed. Using true here is discouraged. + */ + public ResourceDragSortCursorAdapter(Context context, int layout, Cursor c, boolean autoRequery) { + super(context, c, autoRequery); + mLayout = mDropDownLayout = layout; + mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + } + + /** + * Standard constructor. + * + * @param context The context where the ListView associated with this adapter is running + * @param layout Resource identifier of a layout file that defines the views + * for this list item. Unless you override them later, this will + * define both the item views and the drop down views. + * @param c The cursor from which to get the data. + * @param flags Flags used to determine the behavior of the adapter, + * as per {@link CursorAdapter#CursorAdapter(Context, Cursor, int)}. + */ + public ResourceDragSortCursorAdapter(Context context, int layout, Cursor c, int flags) { + super(context, c, flags); + mLayout = mDropDownLayout = layout; + mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + } + + /** + * Inflates view(s) from the specified XML file. + * + * @see android.widget.CursorAdapter#newView(android.content.Context, + * android.database.Cursor, ViewGroup) + */ + @Override + public View newView(Context context, Cursor cursor, ViewGroup parent) { + return mInflater.inflate(mLayout, parent, false); + } + + @Override + public View newDropDownView(Context context, Cursor cursor, ViewGroup parent) { + return mInflater.inflate(mDropDownLayout, parent, false); + } + + /** + * <p>Sets the layout resource of the item views.</p> + * + * @param layout the layout resources used to create item views + */ + public void setViewResource(int layout) { + mLayout = layout; + } + + /** + * <p>Sets the layout resource of the drop down views.</p> + * + * @param dropDownLayout the layout resources used to create drop down views + */ + public void setDropDownViewResource(int dropDownLayout) { + mDropDownLayout = dropDownLayout; + } +} diff --git a/library/drag-sort-listview/src/main/java/com/mobeta/android/dslv/SimpleDragSortCursorAdapter.java b/library/drag-sort-listview/src/main/java/com/mobeta/android/dslv/SimpleDragSortCursorAdapter.java new file mode 100644 index 000000000..7a76ea9d3 --- /dev/null +++ b/library/drag-sort-listview/src/main/java/com/mobeta/android/dslv/SimpleDragSortCursorAdapter.java @@ -0,0 +1,422 @@ +/* + * Copyright (C) 2006 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.mobeta.android.dslv; + +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.view.View; +import android.widget.TextView; +import android.widget.ImageView; + +// taken from sdk/sources/android-16/android/widget/SimpleCursorAdapter.java + +/** + * An easy adapter to map columns from a cursor to TextViews or ImageViews + * defined in an XML file. You can specify which columns you want, which + * views you want to display the columns, and the XML file that defines + * the appearance of these views. + * + * Binding occurs in two phases. First, if a + * {@link android.widget.SimpleCursorAdapter.ViewBinder} is available, + * {@link ViewBinder#setViewValue(android.view.View, android.database.Cursor, int)} + * is invoked. If the returned value is true, binding has occured. If the + * returned value is false and the view to bind is a TextView, + * {@link #setViewText(TextView, String)} is invoked. If the returned value + * is false and the view to bind is an ImageView, + * {@link #setViewImage(ImageView, String)} is invoked. If no appropriate + * binding can be found, an {@link IllegalStateException} is thrown. + * + * If this adapter is used with filtering, for instance in an + * {@link android.widget.AutoCompleteTextView}, you can use the + * {@link android.widget.SimpleCursorAdapter.CursorToStringConverter} and the + * {@link android.widget.FilterQueryProvider} interfaces + * to get control over the filtering process. You can refer to + * {@link #convertToString(android.database.Cursor)} and + * {@link #runQueryOnBackgroundThread(CharSequence)} for more information. + */ +public class SimpleDragSortCursorAdapter extends ResourceDragSortCursorAdapter { + /** + * A list of columns containing the data to bind to the UI. + * This field should be made private, so it is hidden from the SDK. + * {@hide} + */ + protected int[] mFrom; + /** + * A list of View ids representing the views to which the data must be bound. + * This field should be made private, so it is hidden from the SDK. + * {@hide} + */ + protected int[] mTo; + + private int mStringConversionColumn = -1; + private CursorToStringConverter mCursorToStringConverter; + private ViewBinder mViewBinder; + + String[] mOriginalFrom; + + /** + * Constructor the enables auto-requery. + * + * @deprecated This option is discouraged, as it results in Cursor queries + * being performed on the application's UI thread and thus can cause poor + * responsiveness or even Application Not Responding errors. As an alternative, + * use {@link android.app.LoaderManager} with a {@link android.content.CursorLoader}. + */ + @Deprecated + public SimpleDragSortCursorAdapter(Context context, int layout, Cursor c, String[] from, int[] to) { + super(context, layout, c); + mTo = to; + mOriginalFrom = from; + findColumns(c, from); + } + + /** + * Standard constructor. + * + * @param context The context where the ListView associated with this + * SimpleListItemFactory is running + * @param layout resource identifier of a layout file that defines the views + * for this list item. The layout file should include at least + * those named views defined in "to" + * @param c The database cursor. Can be null if the cursor is not available yet. + * @param from A list of column names representing the data to bind to the UI. Can be null + * if the cursor is not available yet. + * @param to The views that should display column in the "from" parameter. + * These should all be TextViews. The first N views in this list + * are given the values of the first N columns in the from + * parameter. Can be null if the cursor is not available yet. + * @param flags Flags used to determine the behavior of the adapter, + * as per {@link CursorAdapter#CursorAdapter(Context, Cursor, int)}. + */ + public SimpleDragSortCursorAdapter(Context context, int layout, + Cursor c, String[] from, int[] to, int flags) { + super(context, layout, c, flags); + mTo = to; + mOriginalFrom = from; + findColumns(c, from); + } + + /** + * Binds all of the field names passed into the "to" parameter of the + * constructor with their corresponding cursor columns as specified in the + * "from" parameter. + * + * Binding occurs in two phases. First, if a + * {@link android.widget.SimpleCursorAdapter.ViewBinder} is available, + * {@link ViewBinder#setViewValue(android.view.View, android.database.Cursor, int)} + * is invoked. If the returned value is true, binding has occured. If the + * returned value is false and the view to bind is a TextView, + * {@link #setViewText(TextView, String)} is invoked. If the returned value is + * false and the view to bind is an ImageView, + * {@link #setViewImage(ImageView, String)} is invoked. If no appropriate + * binding can be found, an {@link IllegalStateException} is thrown. + * + * @throws IllegalStateException if binding cannot occur + * + * @see android.widget.CursorAdapter#bindView(android.view.View, + * android.content.Context, android.database.Cursor) + * @see #getViewBinder() + * @see #setViewBinder(android.widget.SimpleCursorAdapter.ViewBinder) + * @see #setViewImage(ImageView, String) + * @see #setViewText(TextView, String) + */ + @Override + public void bindView(View view, Context context, Cursor cursor) { + final ViewBinder binder = mViewBinder; + final int count = mTo.length; + final int[] from = mFrom; + final int[] to = mTo; + + for (int i = 0; i < count; i++) { + final View v = view.findViewById(to[i]); + if (v != null) { + boolean bound = false; + if (binder != null) { + bound = binder.setViewValue(v, cursor, from[i]); + } + + if (!bound) { + String text = cursor.getString(from[i]); + if (text == null) { + text = ""; + } + + if (v instanceof TextView) { + setViewText((TextView) v, text); + } else if (v instanceof ImageView) { + setViewImage((ImageView) v, text); + } else { + throw new IllegalStateException(v.getClass().getName() + " is not a " + + " view that can be bounds by this SimpleCursorAdapter"); + } + } + } + } + } + + /** + * Returns the {@link ViewBinder} used to bind data to views. + * + * @return a ViewBinder or null if the binder does not exist + * + * @see #bindView(android.view.View, android.content.Context, android.database.Cursor) + * @see #setViewBinder(android.widget.SimpleCursorAdapter.ViewBinder) + */ + public ViewBinder getViewBinder() { + return mViewBinder; + } + + /** + * Sets the binder used to bind data to views. + * + * @param viewBinder the binder used to bind data to views, can be null to + * remove the existing binder + * + * @see #bindView(android.view.View, android.content.Context, android.database.Cursor) + * @see #getViewBinder() + */ + public void setViewBinder(ViewBinder viewBinder) { + mViewBinder = viewBinder; + } + + /** + * Called by bindView() to set the image for an ImageView but only if + * there is no existing ViewBinder or if the existing ViewBinder cannot + * handle binding to an ImageView. + * + * By default, the value will be treated as an image resource. If the + * value cannot be used as an image resource, the value is used as an + * image Uri. + * + * Intended to be overridden by Adapters that need to filter strings + * retrieved from the database. + * + * @param v ImageView to receive an image + * @param value the value retrieved from the cursor + */ + public void setViewImage(ImageView v, String value) { + try { + v.setImageResource(Integer.parseInt(value)); + } catch (NumberFormatException nfe) { + v.setImageURI(Uri.parse(value)); + } + } + + /** + * Called by bindView() to set the text for a TextView but only if + * there is no existing ViewBinder or if the existing ViewBinder cannot + * handle binding to a TextView. + * + * Intended to be overridden by Adapters that need to filter strings + * retrieved from the database. + * + * @param v TextView to receive text + * @param text the text to be set for the TextView + */ + public void setViewText(TextView v, String text) { + v.setText(text); + } + + /** + * Return the index of the column used to get a String representation + * of the Cursor. + * + * @return a valid index in the current Cursor or -1 + * + * @see android.widget.CursorAdapter#convertToString(android.database.Cursor) + * @see #setStringConversionColumn(int) + * @see #setCursorToStringConverter(android.widget.SimpleCursorAdapter.CursorToStringConverter) + * @see #getCursorToStringConverter() + */ + public int getStringConversionColumn() { + return mStringConversionColumn; + } + + /** + * Defines the index of the column in the Cursor used to get a String + * representation of that Cursor. The column is used to convert the + * Cursor to a String only when the current CursorToStringConverter + * is null. + * + * @param stringConversionColumn a valid index in the current Cursor or -1 to use the default + * conversion mechanism + * + * @see android.widget.CursorAdapter#convertToString(android.database.Cursor) + * @see #getStringConversionColumn() + * @see #setCursorToStringConverter(android.widget.SimpleCursorAdapter.CursorToStringConverter) + * @see #getCursorToStringConverter() + */ + public void setStringConversionColumn(int stringConversionColumn) { + mStringConversionColumn = stringConversionColumn; + } + + /** + * Returns the converter used to convert the filtering Cursor + * into a String. + * + * @return null if the converter does not exist or an instance of + * {@link android.widget.SimpleCursorAdapter.CursorToStringConverter} + * + * @see #setCursorToStringConverter(android.widget.SimpleCursorAdapter.CursorToStringConverter) + * @see #getStringConversionColumn() + * @see #setStringConversionColumn(int) + * @see android.widget.CursorAdapter#convertToString(android.database.Cursor) + */ + public CursorToStringConverter getCursorToStringConverter() { + return mCursorToStringConverter; + } + + /** + * Sets the converter used to convert the filtering Cursor + * into a String. + * + * @param cursorToStringConverter the Cursor to String converter, or + * null to remove the converter + * + * @see #setCursorToStringConverter(android.widget.SimpleCursorAdapter.CursorToStringConverter) + * @see #getStringConversionColumn() + * @see #setStringConversionColumn(int) + * @see android.widget.CursorAdapter#convertToString(android.database.Cursor) + */ + public void setCursorToStringConverter(CursorToStringConverter cursorToStringConverter) { + mCursorToStringConverter = cursorToStringConverter; + } + + /** + * Returns a CharSequence representation of the specified Cursor as defined + * by the current CursorToStringConverter. If no CursorToStringConverter + * has been set, the String conversion column is used instead. If the + * conversion column is -1, the returned String is empty if the cursor + * is null or Cursor.toString(). + * + * @param cursor the Cursor to convert to a CharSequence + * + * @return a non-null CharSequence representing the cursor + */ + @Override + public CharSequence convertToString(Cursor cursor) { + if (mCursorToStringConverter != null) { + return mCursorToStringConverter.convertToString(cursor); + } else if (mStringConversionColumn > -1) { + return cursor.getString(mStringConversionColumn); + } + + return super.convertToString(cursor); + } + + /** + * Create a map from an array of strings to an array of column-id integers in cursor c. + * If c is null, the array will be discarded. + * + * @param c the cursor to find the columns from + * @param from the Strings naming the columns of interest + */ + private void findColumns(Cursor c, String[] from) { + if (c != null) { + int i; + int count = from.length; + if (mFrom == null || mFrom.length != count) { + mFrom = new int[count]; + } + for (i = 0; i < count; i++) { + mFrom[i] = c.getColumnIndexOrThrow(from[i]); + } + } else { + mFrom = null; + } + } + + @Override + public Cursor swapCursor(Cursor c) { + // super.swapCursor() will notify observers before we have + // a valid mapping, make sure we have a mapping before this + // happens + findColumns(c, mOriginalFrom); + return super.swapCursor(c); + } + + /** + * Change the cursor and change the column-to-view mappings at the same time. + * + * @param c The database cursor. Can be null if the cursor is not available yet. + * @param from A list of column names representing the data to bind to the UI. Can be null + * if the cursor is not available yet. + * @param to The views that should display column in the "from" parameter. + * These should all be TextViews. The first N views in this list + * are given the values of the first N columns in the from + * parameter. Can be null if the cursor is not available yet. + */ + public void changeCursorAndColumns(Cursor c, String[] from, int[] to) { + mOriginalFrom = from; + mTo = to; + // super.changeCursor() will notify observers before we have + // a valid mapping, make sure we have a mapping before this + // happens + findColumns(c, mOriginalFrom); + super.changeCursor(c); + } + + /** + * This class can be used by external clients of SimpleCursorAdapter + * to bind values fom the Cursor to views. + * + * You should use this class to bind values from the Cursor to views + * that are not directly supported by SimpleCursorAdapter or to + * change the way binding occurs for views supported by + * SimpleCursorAdapter. + * + * @see SimpleCursorAdapter#bindView(android.view.View, android.content.Context, android.database.Cursor) + * @see SimpleCursorAdapter#setViewImage(ImageView, String) + * @see SimpleCursorAdapter#setViewText(TextView, String) + */ + public static interface ViewBinder { + /** + * Binds the Cursor column defined by the specified index to the specified view. + * + * When binding is handled by this ViewBinder, this method must return true. + * If this method returns false, SimpleCursorAdapter will attempts to handle + * the binding on its own. + * + * @param view the view to bind the data to + * @param cursor the cursor to get the data from + * @param columnIndex the column at which the data can be found in the cursor + * + * @return true if the data was bound to the view, false otherwise + */ + boolean setViewValue(View view, Cursor cursor, int columnIndex); + } + + /** + * This class can be used by external clients of SimpleCursorAdapter + * to define how the Cursor should be converted to a String. + * + * @see android.widget.CursorAdapter#convertToString(android.database.Cursor) + */ + public static interface CursorToStringConverter { + /** + * Returns a CharSequence representing the specified Cursor. + * + * @param cursor the cursor for which a CharSequence representation + * is requested + * + * @return a non-null CharSequence representing the cursor + */ + CharSequence convertToString(Cursor cursor); + } + +} diff --git a/library/drag-sort-listview/src/main/java/com/mobeta/android/dslv/SimpleFloatViewManager.java b/library/drag-sort-listview/src/main/java/com/mobeta/android/dslv/SimpleFloatViewManager.java new file mode 100644 index 000000000..af1df01c0 --- /dev/null +++ b/library/drag-sort-listview/src/main/java/com/mobeta/android/dslv/SimpleFloatViewManager.java @@ -0,0 +1,89 @@ +package com.mobeta.android.dslv; + +import android.graphics.Bitmap; +import android.graphics.Point; +import android.graphics.Color; +import android.widget.ListView; +import android.widget.ImageView; +import android.view.View; +import android.view.ViewGroup; +import android.util.Log; + +/** + * Simple implementation of the FloatViewManager class. Uses list + * items as they appear in the ListView to create the floating View. + */ +public class SimpleFloatViewManager implements DragSortListView.FloatViewManager { + + private Bitmap mFloatBitmap; + + private ImageView mImageView; + + private int mFloatBGColor = Color.BLACK; + + private ListView mListView; + + public SimpleFloatViewManager(ListView lv) { + mListView = lv; + } + + public void setBackgroundColor(int color) { + mFloatBGColor = color; + } + + /** + * This simple implementation creates a Bitmap copy of the + * list item currently shown at ListView <code>position</code>. + */ + @Override + public View onCreateFloatView(int position) { + // Guaranteed that this will not be null? I think so. Nope, got + // a NullPointerException once... + View v = mListView.getChildAt(position + mListView.getHeaderViewsCount() - mListView.getFirstVisiblePosition()); + + if (v == null) { + return null; + } + + v.setPressed(false); + + // Create a copy of the drawing cache so that it does not get + // recycled by the framework when the list tries to clean up memory + //v.setDrawingCacheQuality(View.DRAWING_CACHE_QUALITY_HIGH); + v.setDrawingCacheEnabled(true); + mFloatBitmap = Bitmap.createBitmap(v.getDrawingCache()); + v.setDrawingCacheEnabled(false); + + if (mImageView == null) { + mImageView = new ImageView(mListView.getContext()); + } + mImageView.setBackgroundColor(mFloatBGColor); + mImageView.setPadding(0, 0, 0, 0); + mImageView.setImageBitmap(mFloatBitmap); + mImageView.setLayoutParams(new ViewGroup.LayoutParams(v.getWidth(), v.getHeight())); + + return mImageView; + } + + /** + * This does nothing + */ + @Override + public void onDragFloatView(View floatView, Point position, Point touch) { + // do nothing + } + + /** + * Removes the Bitmap from the ImageView created in + * onCreateFloatView() and tells the system to recycle it. + */ + @Override + public void onDestroyFloatView(View floatView) { + ((ImageView) floatView).setImageDrawable(null); + + mFloatBitmap.recycle(); + mFloatBitmap = null; + } + +} + diff --git a/library/drag-sort-listview/src/main/res/values/dslv_attrs.xml b/library/drag-sort-listview/src/main/res/values/dslv_attrs.xml new file mode 100644 index 000000000..8c779c9bf --- /dev/null +++ b/library/drag-sort-listview/src/main/res/values/dslv_attrs.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="utf-8" ?> +<resources> + <declare-styleable name="DragSortListView"> + <attr name="collapsed_height" format="dimension" /> + <attr name="drag_scroll_start" format="float" /> + <attr name="max_drag_scroll_speed" format="float" /> + <attr name="float_background_color" format="color" /> + <attr name="remove_mode"> + <enum name="clickRemove" value="0" /> + <enum name="flingRemove" value="1" /> + </attr> + <attr name="track_drag_sort" format="boolean"/> + <attr name="float_alpha" format="float"/> + <attr name="slide_shuffle_speed" format="float"/> + <attr name="remove_animation_duration" format="integer"/> + <attr name="drop_animation_duration" format="integer"/> + <attr name="drag_enabled" format="boolean" /> + <attr name="sort_enabled" format="boolean" /> + <attr name="remove_enabled" format="boolean" /> + <attr name="drag_start_mode"> + <enum name="onDown" value="0" /> + <enum name="onMove" value="1" /> + <enum name="onLongPress" value="2"/> + </attr> + <attr name="drag_handle_id" format="integer" /> + <attr name="fling_handle_id" format="integer" /> + <attr name="click_remove_id" format="integer" /> + <attr name="use_default_controller" format="boolean" /> + </declare-styleable> +</resources> diff --git a/settings.gradle b/settings.gradle index de34bc1c1..b2a4011c3 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,2 +1,2 @@ include ':app', ':core' -include ':app:dslv:library' +include ':library:drag-sort-listview' |