diff options
Diffstat (limited to 'app/src')
39 files changed, 536 insertions, 399 deletions
diff --git a/app/src/androidTest/java/de/test/antennapod/EspressoTestUtils.java b/app/src/androidTest/java/de/test/antennapod/EspressoTestUtils.java index 02ee9a487..9e86275fc 100644 --- a/app/src/androidTest/java/de/test/antennapod/EspressoTestUtils.java +++ b/app/src/androidTest/java/de/test/antennapod/EspressoTestUtils.java @@ -1,6 +1,7 @@ package de.test.antennapod; import android.content.Context; +import android.content.SharedPreferences; import android.support.annotation.StringRes; import android.support.test.InstrumentationRegistry; import android.support.test.espresso.PerformException; @@ -15,6 +16,7 @@ import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.MainActivity; import de.danoeh.antennapod.core.storage.PodDBAdapter; import de.danoeh.antennapod.dialog.RatingDialog; +import de.danoeh.antennapod.fragment.QueueFragment; import org.hamcrest.Matcher; import java.io.File; @@ -97,6 +99,13 @@ public class EspressoTestUtils { RatingDialog.saveRated(); } + public static void setLastNavFragment(String tag) { + InstrumentationRegistry.getTargetContext().getSharedPreferences(MainActivity.PREF_NAME, Context.MODE_PRIVATE) + .edit() + .putString(MainActivity.PREF_LAST_FRAGMENT_TAG, tag) + .commit(); + } + public static void clearDatabase() { PodDBAdapter.init(InstrumentationRegistry.getTargetContext()); PodDBAdapter.deleteDatabase(); diff --git a/app/src/androidTest/java/de/test/antennapod/ui/QueueFragmentTest.java b/app/src/androidTest/java/de/test/antennapod/ui/QueueFragmentTest.java new file mode 100644 index 000000000..d1023dd9e --- /dev/null +++ b/app/src/androidTest/java/de/test/antennapod/ui/QueueFragmentTest.java @@ -0,0 +1,58 @@ +package de.test.antennapod.ui; + +import android.content.Intent; +import android.support.test.espresso.Espresso; +import android.support.test.espresso.intent.rule.IntentsTestRule; +import android.support.test.runner.AndroidJUnit4; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.activity.MainActivity; +import de.danoeh.antennapod.fragment.QueueFragment; +import de.test.antennapod.EspressoTestUtils; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import static android.support.test.espresso.Espresso.onView; +import static android.support.test.espresso.action.ViewActions.click; +import static android.support.test.espresso.matcher.ViewMatchers.withContentDescription; +import static android.support.test.espresso.matcher.ViewMatchers.withText; + +/** + * User interface tests for queue fragment + */ +@RunWith(AndroidJUnit4.class) +public class QueueFragmentTest { + + @Rule + public IntentsTestRule<MainActivity> mActivityRule = new IntentsTestRule<>(MainActivity.class, false, false); + + @Before + public void setUp() { + EspressoTestUtils.clearPreferences(); + EspressoTestUtils.makeNotFirstRun(); + EspressoTestUtils.clearDatabase(); + EspressoTestUtils.setLastNavFragment(QueueFragment.TAG); + mActivityRule.launchActivity(new Intent()); + } + + @Test + public void testLockEmptyQueue() { + onView(withContentDescription(R.string.lock_queue)).perform(click()); + onView(withContentDescription(R.string.unlock_queue)).perform(click()); + } + + @Test + public void testSortEmptyQueue() { + Espresso.openContextualActionModeOverflowMenu(); + onView(withText(R.string.sort)).perform(click()); + onView(withText(R.string.random)).perform(click()); + } + + @Test + public void testKeepEmptyQueueSorted() { + Espresso.openContextualActionModeOverflowMenu(); + onView(withText(R.string.sort)).perform(click()); + onView(withText(R.string.keep_sorted)).perform(click()); + } +} 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 3d8417bf6..4158fd31c 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 @@ -72,20 +72,6 @@ public class HTTPBin extends NanoHTTPD { return servedFiles.size() - 1; } - /** - * Removes the file with the given ID from the server. - * - * @return True if a file was removed, false otherwise - */ - public synchronized boolean removeFile(int id) { - if (id < 0) throw new IllegalArgumentException("ID < 0"); - if (id >= servedFiles.size()) { - return false; - } else { - return servedFiles.remove(id) != null; - } - } - public synchronized File accessFile(int id) { if (id < 0 || id >= servedFiles.size()) { return null; diff --git a/app/src/free/java/de/danoeh/antennapod/activity/CastEnabledActivity.java b/app/src/free/java/de/danoeh/antennapod/activity/CastEnabledActivity.java index 79d7e02f2..519d4c61b 100644 --- a/app/src/free/java/de/danoeh/antennapod/activity/CastEnabledActivity.java +++ b/app/src/free/java/de/danoeh/antennapod/activity/CastEnabledActivity.java @@ -7,212 +7,9 @@ import android.support.v7.app.AppCompatActivity; * network. */ public abstract class CastEnabledActivity extends AppCompatActivity { -// implements SharedPreferences.OnSharedPreferenceChangeListener { public static final String TAG = "CastEnabledActivity"; -// protected CastManager castManager; -// protected SwitchableMediaRouteActionProvider mediaRouteActionProvider; -// private final CastButtonVisibilityManager castButtonVisibilityManager = new CastButtonVisibilityManager(); -// -// @Override -// protected void onCreate(Bundle savedInstanceState) { -// super.onCreate(savedInstanceState); -// -// PreferenceManager.getDefaultSharedPreferences(getApplicationContext()). -// registerOnSharedPreferenceChangeListener(this); -// -// castManager = CastManager.getInstance(); -// castManager.addCastConsumer(castConsumer); -// castButtonVisibilityManager.setPrefEnabled(UserPreferences.isCastEnabled()); -// onCastConnectionChanged(castManager.isConnected()); -// } -// -// @Override -// protected void onDestroy() { -// PreferenceManager.getDefaultSharedPreferences(getApplicationContext()) -// .unregisterOnSharedPreferenceChangeListener(this); -// castManager.removeCastConsumer(castConsumer); -// super.onDestroy(); -// } -// -// @Override -// @CallSuper -// public boolean onCreateOptionsMenu(Menu menu) { -// super.onCreateOptionsMenu(menu); -// getMenuInflater().inflate(R.menu.cast_enabled, menu); -// castButtonVisibilityManager.setMenu(menu); -// return true; -// } -// -// @Override -// @CallSuper -// public boolean onPrepareOptionsMenu(Menu menu) { -// super.onPrepareOptionsMenu(menu); -// mediaRouteActionProvider = castManager -// .addMediaRouterButton(menu.findItem(R.id.media_route_menu_item)); -// mediaRouteActionProvider.setEnabled(castButtonVisibilityManager.shouldEnable()); -// return true; -// } -// -// @Override -// protected void onResume() { -// super.onResume(); -// castButtonVisibilityManager.setResumed(true); -// } -// -// @Override -// protected void onPause() { -// super.onPause(); -// castButtonVisibilityManager.setResumed(false); -// } -// -// @Override -// public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { -// if (UserPreferences.PREF_CAST_ENABLED.equals(key)) { -// boolean newValue = UserPreferences.isCastEnabled(); -// Log.d(TAG, "onSharedPreferenceChanged(), isCastEnabled set to " + newValue); -// castButtonVisibilityManager.setPrefEnabled(newValue); -// // PlaybackService has its own listener, so if it's active we don't have to take action here. -// if (!newValue && !PlaybackService.isRunning) { -// CastManager.getInstance().disconnect(); -// } -// } -// } -// -// CastConsumer castConsumer = new DefaultCastConsumer() { -// @Override -// public void onApplicationConnected(ApplicationMetadata appMetadata, String sessionId, boolean wasLaunched) { -// onCastConnectionChanged(true); -// } -// -// @Override -// public void onDisconnected() { -// onCastConnectionChanged(false); -// } -// }; -// -// private void onCastConnectionChanged(boolean connected) { -// if (connected) { -// castButtonVisibilityManager.onConnected(); -// setVolumeControlStream(AudioManager.USE_DEFAULT_STREAM_TYPE); -// } else { -// castButtonVisibilityManager.onDisconnected(); -// setVolumeControlStream(AudioManager.STREAM_MUSIC); -// } -// } -// -// /** -// * Should be called by any activity or fragment for which the cast button should be shown. -// * -// * @param showAsAction refer to {@link MenuItem#setShowAsAction(int)} -// */ public final void requestCastButton(int showAsAction) { // no-op } - -// private class CastButtonVisibilityManager { -// private volatile boolean prefEnabled = false; -// private volatile boolean viewRequested = false; -// private volatile boolean resumed = false; -// private volatile boolean connected = false; -// private volatile int showAsAction = MenuItem.SHOW_AS_ACTION_IF_ROOM; -// private Menu menu; -// -// public synchronized void setPrefEnabled(boolean newValue) { -// if (prefEnabled != newValue && resumed && (viewRequested || connected)) { -// if (newValue) { -// castManager.incrementUiCounter(); -// } else { -// castManager.decrementUiCounter(); -// } -// } -// prefEnabled = newValue; -// if (mediaRouteActionProvider != null) { -// mediaRouteActionProvider.setEnabled(prefEnabled && (viewRequested || connected)); -// } -// } -// -// public synchronized void setResumed(boolean newValue) { -// if (resumed == newValue) { -// Log.e(TAG, "resumed should never change to the same value"); -// return; -// } -// resumed = newValue; -// if (prefEnabled && (viewRequested || connected)) { -// if (resumed) { -// castManager.incrementUiCounter(); -// } else { -// castManager.decrementUiCounter(); -// } -// } -// } -// -// public synchronized void setViewRequested(boolean newValue) { -// if (viewRequested != newValue && resumed && prefEnabled && !connected) { -// if (newValue) { -// castManager.incrementUiCounter(); -// } else { -// castManager.decrementUiCounter(); -// } -// } -// viewRequested = newValue; -// if (mediaRouteActionProvider != null) { -// mediaRouteActionProvider.setEnabled(prefEnabled && (viewRequested || connected)); -// } -// } -// -// public synchronized void setConnected(boolean newValue) { -// if (connected != newValue && resumed && prefEnabled && !prefEnabled) { -// if (newValue) { -// castManager.incrementUiCounter(); -// } else { -// castManager.decrementUiCounter(); -// } -// } -// connected = newValue; -// if (mediaRouteActionProvider != null) { -// mediaRouteActionProvider.setEnabled(prefEnabled && (viewRequested || connected)); -// } -// } -// -// public synchronized boolean shouldEnable() { -// return prefEnabled && viewRequested; -// } -// -// public void setMenu(Menu menu) { -// setViewRequested(false); -// showAsAction = MenuItem.SHOW_AS_ACTION_IF_ROOM; -// this.menu = menu; -// setShowAsAction(); -// } -// -// public void requestCastButton(int showAsAction) { -// setViewRequested(true); -// this.showAsAction = showAsAction; -// setShowAsAction(); -// } -// -// public void onConnected() { -// setConnected(true); -// setShowAsAction(); -// } -// -// public void onDisconnected() { -// setConnected(false); -// setShowAsAction(); -// } -// -// private void setShowAsAction() { -// if (menu == null) { -// Log.d(TAG, "setShowAsAction() without a menu"); -// return; -// } -// MenuItem item = menu.findItem(R.id.media_route_menu_item); -// if (item == null) { -// Log.e(TAG, "setShowAsAction(), but cast button not inflated"); -// return; -// } -// MenuItemCompat.setShowAsAction(item, connected? MenuItem.SHOW_AS_ACTION_ALWAYS : showAsAction); -// } -// } } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 73af654e9..369511f10 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -36,7 +36,7 @@ android:usesCleartextTraffic="true" android:logo="@mipmap/ic_launcher"> <meta-data android:name="com.google.android.gms.car.notification.SmallIcon" - android:resource="@drawable/ic_notification" /> + android:resource="@drawable/ic_antenna" /> <meta-data android:name="com.google.android.backup.api_key" android:value="AEdPqrEAAAAI3a05VToCTlqBymJrbFGaKQMvF-bBAuLsOdavBA"/> @@ -209,6 +209,13 @@ android:name=".activity.OpmlFeedChooserActivity" android:label="@string/opml_import_label"> </activity> + <activity + android:name=".activity.CrashReportActivity" + android:label="@string/crash_report_title"> + <meta-data + android:name="android.support.PARENT_ACTIVITY" + android:value="de.danoeh.antennapod.activity.PreferenceActivity"/> + </activity> <activity android:name=".activity.VideoplayerActivity" diff --git a/app/src/main/java/de/danoeh/antennapod/CrashReportWriter.java b/app/src/main/java/de/danoeh/antennapod/CrashReportWriter.java index ea2166674..061ea9ae2 100644 --- a/app/src/main/java/de/danoeh/antennapod/CrashReportWriter.java +++ b/app/src/main/java/de/danoeh/antennapod/CrashReportWriter.java @@ -9,6 +9,9 @@ import java.io.File; import java.io.FileWriter; import java.io.IOException; import java.io.PrintWriter; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; import de.danoeh.antennapod.core.preferences.UserPreferences; @@ -32,13 +35,7 @@ public class CrashReportWriter implements Thread.UncaughtExceptionHandler { PrintWriter out = null; try { out = new PrintWriter(new FileWriter(path)); - out.println("[ Environment ]"); - out.println("Android version: " + Build.VERSION.RELEASE); - out.println("OS version: " + System.getProperty("os.version")); - out.println("AntennaPod version: " + BuildConfig.VERSION_NAME); - out.println("Model: " + Build.MODEL); - out.println("Device: " + Build.DEVICE); - out.println("Product: " + Build.PRODUCT); + out.println(getSystemInfo()); out.println(); out.println("[ StackTrace ]"); ex.printStackTrace(out); @@ -49,4 +46,15 @@ public class CrashReportWriter implements Thread.UncaughtExceptionHandler { } defaultHandler.uncaughtException(thread, ex); } + + public static String getSystemInfo() { + return "[ Environment ]" + + "\nAndroid version: " + Build.VERSION.RELEASE + + "\nOS version: " + System.getProperty("os.version") + + "\nAntennaPod version: " + BuildConfig.VERSION_NAME + + "\nModel: " + Build.MODEL + + "\nDevice: " + Build.DEVICE + + "\nProduct: " + Build.PRODUCT + + "\nTime: " + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()).format(new Date()) + "\n"; + } } diff --git a/app/src/main/java/de/danoeh/antennapod/activity/AudioplayerActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/AudioplayerActivity.java index f3b669aab..07c970197 100644 --- a/app/src/main/java/de/danoeh/antennapod/activity/AudioplayerActivity.java +++ b/app/src/main/java/de/danoeh/antennapod/activity/AudioplayerActivity.java @@ -7,6 +7,8 @@ import android.util.Log; import android.view.View; import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; +import java.util.Locale; import java.util.concurrent.atomic.AtomicBoolean; import de.danoeh.antennapod.core.feed.MediaType; @@ -101,7 +103,9 @@ public class AudioplayerActivity extends MediaplayerInfoActivity { } if (controller.canSetPlaybackSpeed()) { String[] availableSpeeds = UserPreferences.getPlaybackSpeedArray(); - String currentSpeed = new DecimalFormat("0.00x").format(UserPreferences.getPlaybackSpeed()); + DecimalFormatSymbols format = new DecimalFormatSymbols(Locale.US); + format.setDecimalSeparator('.'); + String currentSpeed = new DecimalFormat("0.00", format).format(UserPreferences.getPlaybackSpeed()); // Provide initial value in case the speed list has changed // out from under us diff --git a/app/src/main/java/de/danoeh/antennapod/activity/CrashReportActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/CrashReportActivity.java new file mode 100644 index 000000000..6baeddb26 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/activity/CrashReportActivity.java @@ -0,0 +1,64 @@ +package de.danoeh.antennapod.activity; + +import android.content.ActivityNotFoundException; +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.support.design.widget.Snackbar; +import android.support.v7.app.AppCompatActivity; +import android.widget.TextView; +import android.widget.Toast; +import de.danoeh.antennapod.CrashReportWriter; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.core.preferences.UserPreferences; +import org.apache.commons.io.IOUtils; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.nio.charset.Charset; + +/** + * Displays the 'crash report' screen + */ +public class CrashReportActivity extends AppCompatActivity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + setTheme(UserPreferences.getTheme()); + super.onCreate(savedInstanceState); + getSupportActionBar().setDisplayShowHomeEnabled(true); + setContentView(R.layout.crash_report); + + TextView crashDetailsText = findViewById(R.id.crash_report_logs); + + try { + File crashFile = CrashReportWriter.getFile(); + String crashReportContent = IOUtils.toString(new FileInputStream(crashFile), Charset.forName("UTF-8")); + crashDetailsText.setText(crashReportContent); + } catch (IOException e) { + e.printStackTrace(); + crashDetailsText.setText("No crash report recorded\n" + CrashReportWriter.getSystemInfo()); + } + + findViewById(R.id.btn_open_bug_tracker).setOnClickListener(v -> { + try { + Intent myIntent = new Intent(Intent.ACTION_VIEW, + Uri.parse("https://github.com/AntennaPod/AntennaPod/issues")); + startActivity(myIntent); + } catch (ActivityNotFoundException e) { + Toast.makeText(this, R.string.pref_no_browser_found, Toast.LENGTH_LONG).show(); + } + }); + + findViewById(R.id.btn_copy_log).setOnClickListener(v -> { + ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE); + ClipData clip = ClipData.newPlainText(getString(R.string.crash_report_title), crashDetailsText.getText()); + clipboard.setPrimaryClip(clip); + Snackbar.make(findViewById(android.R.id.content), R.string.copied_to_clipboard, Snackbar.LENGTH_SHORT).show(); + }); + } +} 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 ab4f584fe..a0a3c85c2 100644 --- a/app/src/main/java/de/danoeh/antennapod/activity/MainActivity.java +++ b/app/src/main/java/de/danoeh/antennapod/activity/MainActivity.java @@ -87,7 +87,7 @@ public class MainActivity extends CastEnabledActivity implements NavDrawerActivi public static final String PREF_NAME = "MainActivityPrefs"; public static final String PREF_IS_FIRST_LAUNCH = "prefMainActivityIsFirstLaunch"; - private static final String PREF_LAST_FRAGMENT_TAG = "prefMainActivityLastFragmentTag"; + public static final String PREF_LAST_FRAGMENT_TAG = "prefMainActivityLastFragmentTag"; public static final String EXTRA_NAV_TYPE = "nav_type"; public static final String EXTRA_NAV_INDEX = "nav_index"; @@ -240,7 +240,7 @@ public class MainActivity extends CastEnabledActivity implements NavDrawerActivi SharedPreferences.Editor edit = prefs.edit(); edit.putBoolean(PREF_IS_FIRST_LAUNCH, false); - edit.commit(); + edit.apply(); } } diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/DownloadlistAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/DownloadlistAdapter.java index b85d1d35d..b0ee87b7e 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/DownloadlistAdapter.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/DownloadlistAdapter.java @@ -1,7 +1,6 @@ package de.danoeh.antennapod.adapter; import android.content.Context; -import android.support.v4.content.ContextCompat; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -15,20 +14,15 @@ import de.danoeh.antennapod.core.service.download.DownloadRequest; import de.danoeh.antennapod.core.service.download.DownloadStatus; import de.danoeh.antennapod.core.service.download.Downloader; import de.danoeh.antennapod.core.util.Converter; -import de.danoeh.antennapod.core.util.ThemeUtils; public class DownloadlistAdapter extends BaseAdapter { - private static final int SELECTION_NONE = -1; - - private int selectedItemIndex; private final ItemAccess itemAccess; private final Context context; public DownloadlistAdapter(Context context, ItemAccess itemAccess) { super(); - this.selectedItemIndex = SELECTION_NONE; this.context = context; this.itemAccess = itemAccess; } @@ -74,13 +68,6 @@ public class DownloadlistAdapter extends BaseAdapter { holder = (Holder) convertView.getTag(); } - if (position == selectedItemIndex) { - convertView.setBackgroundColor(ContextCompat.getColor(convertView.getContext(), - ThemeUtils.getSelectionBackgroundColor())); - } else { - convertView.setBackgroundResource(0); - } - holder.title.setText(request.getTitle()); holder.progbar.setIndeterminate(request.getSoFar() <= 0); 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 a365b1b2e..d090bc4b1 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/FeedItemlistAdapter.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/FeedItemlistAdapter.java @@ -34,12 +34,8 @@ public class FeedItemlistAdapter extends BaseAdapter { private final ItemAccess itemAccess; private final Context context; private final boolean showFeedtitle; - private final int selectedItemIndex; /** true if played items should be made partially transparent */ private final boolean makePlayedItemsTransparent; - - private static final int SELECTION_NONE = -1; - private final int playingBackGroundColor; private final int normalBackGroundColor; @@ -51,7 +47,6 @@ public class FeedItemlistAdapter extends BaseAdapter { this.context = context; this.itemAccess = itemAccess; this.showFeedtitle = showFeedtitle; - this.selectedItemIndex = SELECTION_NONE; this.makePlayedItemsTransparent = makePlayedItemsTransparent; playingBackGroundColor = ThemeUtils.getColorFromAttr(context, R.attr.currently_playing_background); @@ -112,12 +107,6 @@ public class FeedItemlistAdapter extends BaseAdapter { if (!(getItemViewType(position) == Adapter.IGNORE_ITEM_VIEW_TYPE)) { convertView.setVisibility(View.VISIBLE); - if (position == selectedItemIndex) { - convertView.setBackgroundColor(ContextCompat.getColor(convertView.getContext(), - ThemeUtils.getSelectionBackgroundColor())); - } else { - convertView.setBackgroundResource(0); - } StringBuilder buffer = new StringBuilder(item.getTitle()); if (showFeedtitle) { diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/FeedItemlistDescriptionAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/FeedItemlistDescriptionAdapter.java index c10bb7638..8d469c7a6 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/FeedItemlistDescriptionAdapter.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/FeedItemlistDescriptionAdapter.java @@ -1,6 +1,7 @@ package de.danoeh.antennapod.adapter; import android.content.Context; +import android.os.Build; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -51,6 +52,17 @@ public class FeedItemlistDescriptionAdapter extends ArrayAdapter<FeedItem> { .replaceAll("\\s+", " ") .trim(); holder.description.setText(description); + + final int MAX_LINES_COLLAPSED = 3; + holder.description.setMaxLines(MAX_LINES_COLLAPSED); + holder.description.setOnClickListener(v -> { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN + && holder.description.getMaxLines() > MAX_LINES_COLLAPSED) { + holder.description.setMaxLines(MAX_LINES_COLLAPSED); + } else { + holder.description.setMaxLines(2000); + } + }); } return convertView; } diff --git a/app/src/main/java/de/danoeh/antennapod/asynctask/DocumentFileExportWorker.java b/app/src/main/java/de/danoeh/antennapod/asynctask/DocumentFileExportWorker.java new file mode 100644 index 000000000..0e9572a82 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/asynctask/DocumentFileExportWorker.java @@ -0,0 +1,72 @@ +package de.danoeh.antennapod.asynctask; + +import android.content.Context; +import android.net.Uri; +import android.support.annotation.NonNull; +import android.support.v4.provider.DocumentFile; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; + +import de.danoeh.antennapod.core.export.ExportWriter; +import de.danoeh.antennapod.core.storage.DBReader; +import de.danoeh.antennapod.core.util.LangUtils; +import io.reactivex.Observable; + +/** + * Writes an OPML file into the user selected export directory in the background. + */ +public class DocumentFileExportWorker { + + private final @NonNull ExportWriter exportWriter; + private @NonNull Context context; + private @NonNull Uri outputFileUri; + + public DocumentFileExportWorker(@NonNull ExportWriter exportWriter, @NonNull Context context, @NonNull Uri outputFileUri) { + this.exportWriter = exportWriter; + this.context = context; + this.outputFileUri = outputFileUri; + } + + public Observable<DocumentFile> exportObservable() { + DocumentFile output = DocumentFile.fromSingleUri(context, outputFileUri); + return Observable.create(subscriber -> { + OutputStream outputStream = null; + OutputStreamWriter writer = null; + try { + Uri uri = output.getUri(); + if (uri == null) { + throw new FileNotFoundException("Export file not found."); + } + outputStream = context.getContentResolver().openOutputStream(uri); + if (outputStream == null) { + throw new IOException(); + } + writer = new OutputStreamWriter(outputStream, LangUtils.UTF_8); + exportWriter.writeDocument(DBReader.getFeedList(), writer); + subscriber.onNext(output); + } catch (IOException e) { + subscriber.onError(e); + } finally { + if (writer != null) { + try { + writer.close(); + } catch (IOException e) { + subscriber.onError(e); + } + } + if (outputStream != null) { + try { + outputStream.close(); + } catch (IOException e) { + subscriber.onError(e); + } + } + subscriber.onComplete(); + } + }); + } + +} diff --git a/app/src/main/java/de/danoeh/antennapod/config/PlaybackServiceCallbacksImpl.java b/app/src/main/java/de/danoeh/antennapod/config/PlaybackServiceCallbacksImpl.java index eb70d8e0b..c3f5d898c 100644 --- a/app/src/main/java/de/danoeh/antennapod/config/PlaybackServiceCallbacksImpl.java +++ b/app/src/main/java/de/danoeh/antennapod/config/PlaybackServiceCallbacksImpl.java @@ -35,6 +35,6 @@ public class PlaybackServiceCallbacksImpl implements PlaybackServiceCallbacks { @Override public int getNotificationIconResource(Context context) { - return R.drawable.ic_stat_antenna_default; + return R.drawable.ic_antenna; } } diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/EpisodesApplyActionFragment.java b/app/src/main/java/de/danoeh/antennapod/dialog/EpisodesApplyActionFragment.java index bb47f8baa..ed35495fa 100644 --- a/app/src/main/java/de/danoeh/antennapod/dialog/EpisodesApplyActionFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/dialog/EpisodesApplyActionFragment.java @@ -3,7 +3,6 @@ package de.danoeh.antennapod.dialog; import android.app.AlertDialog; import android.content.res.TypedArray; import android.graphics.drawable.Drawable; -import android.os.Build; import android.os.Bundle; import android.support.annotation.IdRes; import android.support.annotation.NonNull; @@ -13,7 +12,6 @@ import android.support.design.widget.Snackbar; import android.support.v4.app.ActivityCompat; import android.support.v4.app.Fragment; import android.support.v4.util.ArrayMap; -import android.support.v4.view.ViewCompat; import android.support.v7.app.ActionBar; import android.support.v7.app.AppCompatActivity; import android.util.Log; @@ -168,7 +166,8 @@ public class EpisodesApplyActionFragment extends Fragment { return true; }); - for(FeedItem episode : episodes) { + titles.clear(); + for (FeedItem episode : episodes) { titles.add(episode.getTitle()); } @@ -205,10 +204,6 @@ public class EpisodesApplyActionFragment extends Fragment { return true; }); - if (Build.VERSION.SDK_INT == 23 || Build.VERSION.SDK_INT == 24) { - ViewCompat.setElevation(view.findViewById(R.id.fabSDScrollCtr), 8); - } - showSpeedDialIfAnyChecked(); return view; @@ -221,7 +216,13 @@ public class EpisodesApplyActionFragment extends Fragment { } private void showSpeedDialIfAnyChecked() { - mSpeedDialView.setVisibility(checkedIds.size() > 0 ? View.VISIBLE : View.GONE); + if (checkedIds.size() > 0) { + if (!mSpeedDialView.isShown()) { + mSpeedDialView.show(); + } + } else { + mSpeedDialView.hide(); // hide() also handles UI, e.g., overlay properly. + } } @Override @@ -245,10 +246,13 @@ public class EpisodesApplyActionFragment extends Fragment { // Prepare icon for select toggle button int[] icon = new int[1]; + @StringRes int titleResId; if (checkedIds.size() == episodes.size()) { icon[0] = R.attr.ic_select_none; + titleResId = R.string.deselect_all_label; } else { icon[0] = R.attr.ic_select_all; + titleResId = R.string.select_all_label; } TypedArray a = getActivity().obtainStyledAttributes(icon); @@ -256,6 +260,7 @@ public class EpisodesApplyActionFragment extends Fragment { a.recycle(); mSelectToggle.setIcon(iconDrawable); + mSelectToggle.setTitle(titleResId); } @Override diff --git a/app/src/main/java/de/danoeh/antennapod/discovery/CombinedSearcher.java b/app/src/main/java/de/danoeh/antennapod/discovery/CombinedSearcher.java index 18d65a03c..e34d8539c 100644 --- a/app/src/main/java/de/danoeh/antennapod/discovery/CombinedSearcher.java +++ b/app/src/main/java/de/danoeh/antennapod/discovery/CombinedSearcher.java @@ -24,7 +24,7 @@ public class CombinedSearcher implements PodcastSearcher { public CombinedSearcher(Context context) { addProvider(new FyydPodcastSearcher(), 1.f); addProvider(new ItunesPodcastSearcher(context), 1.f); - addProvider(new GpodnetPodcastSearcher(), 0.6f); + //addProvider(new GpodnetPodcastSearcher(), 0.6f); } private void addProvider(PodcastSearcher provider, float priority) { 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 35bcaa76e..3ef010f88 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/AddFeedFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/AddFeedFragment.java @@ -70,11 +70,8 @@ public class AddFeedFragment extends Fragment { combinedFeedSearchBox = root.findViewById(R.id.combinedFeedSearchBox); combinedFeedSearchBox.setOnEditorActionListener((v, actionId, event) -> { - if (actionId == EditorInfo.IME_ACTION_SEARCH) { - performSearch(); - return true; - } - return false; + performSearch(); + return true; }); } diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/ChaptersFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/ChaptersFragment.java index 4bebfe4c9..bb8f4df9a 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/ChaptersFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/ChaptersFragment.java @@ -87,11 +87,10 @@ public class ChaptersFragment extends ListFragment { controller = null; } - private void scrollTo(int position) { - getListView().setSelection(position); - } - private int getCurrentChapter(Playable media) { + if (media == null || media.getChapters() == null || media.getChapters().size() == 0 || controller == null) { + return -1; + } int currentPosition = controller.getPosition(); List<Chapter> chapters = media.getChapters(); @@ -126,8 +125,10 @@ public class ChaptersFragment extends ListFragment { if (adapter != null) { adapter.setMedia(media); adapter.notifyDataSetChanged(); - if (media != null && media.getChapters() != null && media.getChapters().size() != 0) { - scrollTo(getCurrentChapter(media)); + + int positionOfCurrentChapter = getCurrentChapter(media); + if (positionOfCurrentChapter != -1) { + getListView().setSelection(positionOfCurrentChapter); } } } diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/EpisodesListFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/EpisodesListFragment.java index 1cedb5a91..951dad38e 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/EpisodesListFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/EpisodesListFragment.java @@ -149,7 +149,7 @@ public abstract class EpisodesListFragment extends Fragment { SharedPreferences.Editor editor = prefs.edit(); editor.putInt(PREF_SCROLL_POSITION, firstItem); editor.putFloat(PREF_SCROLL_OFFSET, topOffset); - editor.commit(); + editor.apply(); } private void restoreScrollPosition() { @@ -162,7 +162,7 @@ public abstract class EpisodesListFragment extends Fragment { SharedPreferences.Editor editor = prefs.edit(); editor.putInt(PREF_SCROLL_POSITION, 0); editor.putFloat(PREF_SCROLL_OFFSET, 0.0f); - editor.commit(); + editor.apply(); } } diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/FeedItemlistFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/FeedItemlistFragment.java index f8ef7f7a3..4e4b40096 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/FeedItemlistFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/FeedItemlistFragment.java @@ -17,7 +17,6 @@ import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; -import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.ImageButton; import android.widget.ImageView; @@ -141,13 +140,6 @@ public class FeedItemlistFragment extends ListFragment { } @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - View view = super.onCreateView(inflater, container, savedInstanceState); - ((ListView) view.findViewById(android.R.id.list)).setFastScrollEnabled(true); - return view; - } - - @Override public void onStart() { super.onStart(); EventDistributor.getInstance().register(contentUpdate); 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 4f07e1b59..5f6fe26ee 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/QueueFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/QueueFragment.java @@ -7,6 +7,7 @@ import android.os.Bundle; import android.support.design.widget.Snackbar; import android.support.v4.app.Fragment; import android.support.v4.view.MenuItemCompat; +import android.support.v7.app.AlertDialog; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.support.v7.widget.SearchView; @@ -19,6 +20,8 @@ import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; +import android.widget.CheckBox; +import android.widget.LinearLayout; import android.widget.ProgressBar; import android.widget.TextView; @@ -91,10 +94,12 @@ public class QueueFragment extends Fragment { private static final String PREFS = "QueueFragment"; private static final String PREF_SCROLL_POSITION = "scroll_position"; private static final String PREF_SCROLL_OFFSET = "scroll_offset"; + private static final String PREF_SHOW_LOCK_WARNING = "show_lock_warning"; private Disposable disposable; private LinearLayoutManager layoutManager; private ItemTouchHelper itemTouchHelper; + private SharedPreferences prefs; @Override @@ -102,6 +107,7 @@ public class QueueFragment extends Fragment { super.onCreate(savedInstanceState); setRetainInstance(true); setHasOptionsMenu(true); + prefs = getActivity().getSharedPreferences(PREFS, Context.MODE_PRIVATE); } @Override @@ -219,15 +225,13 @@ public class QueueFragment extends Fragment { topOffset = firstItemView.getTop(); } - SharedPreferences prefs = getActivity().getSharedPreferences(PREFS, Context.MODE_PRIVATE); - SharedPreferences.Editor editor = prefs.edit(); - editor.putInt(PREF_SCROLL_POSITION, firstItem); - editor.putFloat(PREF_SCROLL_OFFSET, topOffset); - editor.commit(); + prefs.edit() + .putInt(PREF_SCROLL_POSITION, firstItem) + .putFloat(PREF_SCROLL_OFFSET, topOffset) + .apply(); } private void restoreScrollPosition() { - SharedPreferences prefs = getActivity().getSharedPreferences(PREFS, Context.MODE_PRIVATE); int position = prefs.getInt(PREF_SCROLL_POSITION, 0); float offset = prefs.getFloat(PREF_SCROLL_OFFSET, 0.0f); if (position > 0 || offset > 0) { @@ -299,19 +303,7 @@ public class QueueFragment extends Fragment { if (!super.onOptionsItemSelected(item)) { switch (item.getItemId()) { case R.id.queue_lock: - boolean newLockState = !UserPreferences.isQueueLocked(); - UserPreferences.setQueueLocked(newLockState); - getActivity().supportInvalidateOptionsMenu(); - if (recyclerAdapter != null) { - recyclerAdapter.setLocked(newLockState); - } - if (newLockState) { - Snackbar.make(getActivity().findViewById(R.id.content), R.string - .queue_locked, Snackbar.LENGTH_SHORT).show(); - } else { - Snackbar.make(getActivity().findViewById(R.id.content), R.string - .queue_unlocked, Snackbar.LENGTH_SHORT).show(); - } + toggleQueueLock(); return true; case R.id.refresh_item: List<Feed> feeds = ((MainActivity) getActivity()).getFeeds(); @@ -378,8 +370,10 @@ public class QueueFragment extends Fragment { if (keepSortedNew) { SortOrder sortOrder = UserPreferences.getQueueKeepSortedOrder(); QueueSorter.sort(sortOrder, true); - recyclerAdapter.setLocked(true); - } else { + if (recyclerAdapter != null) { + recyclerAdapter.setLocked(true); + } + } else if (recyclerAdapter != null) { recyclerAdapter.setLocked(UserPreferences.isQueueLocked()); } getActivity().invalidateOptionsMenu(); @@ -392,6 +386,48 @@ public class QueueFragment extends Fragment { } } + private void toggleQueueLock() { + boolean isLocked = UserPreferences.isQueueLocked(); + if (isLocked) { + setQueueLocked(false); + } else { + boolean shouldShowLockWarning = prefs.getBoolean(PREF_SHOW_LOCK_WARNING, true); + if (!shouldShowLockWarning) { + setQueueLocked(true); + } else { + AlertDialog.Builder builder = new AlertDialog.Builder(getContext()); + builder.setTitle(R.string.lock_queue); + builder.setMessage(R.string.queue_lock_warning); + + View view = View.inflate(getContext(), R.layout.checkbox_do_not_show_again, null); + CheckBox checkDoNotShowAgain = view.findViewById(R.id.checkbox_do_not_show_again); + builder.setView(view); + + builder.setPositiveButton(R.string.lock_queue, (dialog, which) -> { + prefs.edit().putBoolean(PREF_SHOW_LOCK_WARNING, !checkDoNotShowAgain.isChecked()).apply(); + setQueueLocked(true); + }); + builder.setNegativeButton(R.string.cancel_label, null); + builder.show(); + } + } + } + + private void setQueueLocked(boolean locked) { + UserPreferences.setQueueLocked(locked); + getActivity().supportInvalidateOptionsMenu(); + if (recyclerAdapter != null) { + recyclerAdapter.setLocked(locked); + } + if (locked) { + Snackbar.make(getActivity().findViewById(R.id.content), R.string + .queue_locked, Snackbar.LENGTH_SHORT).show(); + } else { + Snackbar.make(getActivity().findViewById(R.id.content), R.string + .queue_unlocked, Snackbar.LENGTH_SHORT).show(); + } + } + /** * This method is called if the user clicks on a sort order menu item. * diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/MainPreferencesFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/MainPreferencesFragment.java index 701d21ce0..fa7e545f4 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/MainPreferencesFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/MainPreferencesFragment.java @@ -1,28 +1,21 @@ package de.danoeh.antennapod.fragment.preferences; import android.content.ActivityNotFoundException; -import android.content.Context; import android.content.Intent; -import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; import android.net.Uri; -import android.os.Build; import android.os.Bundle; -import android.support.v4.content.FileProvider; import android.support.v7.app.AppCompatActivity; import android.support.v7.preference.PreferenceFragmentCompat; import android.util.Log; import android.widget.Toast; import com.bytehamster.lib.preferencesearch.SearchConfiguration; import com.bytehamster.lib.preferencesearch.SearchPreference; -import de.danoeh.antennapod.CrashReportWriter; import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.AboutActivity; +import de.danoeh.antennapod.activity.CrashReportActivity; import de.danoeh.antennapod.activity.PreferenceActivity; import de.danoeh.antennapod.activity.StatisticsActivity; -import java.util.List; - public class MainPreferencesFragment extends PreferenceFragmentCompat { private static final String TAG = "MainPreferencesFragment"; @@ -83,30 +76,11 @@ public class MainPreferencesFragment extends PreferenceFragmentCompat { return true; }); findPreference(PREF_FAQ).setOnPreferenceClickListener(preference -> { - openInBrowser("http://antennapod.org/faq.html"); + openInBrowser("https://antennapod.org/faq.html"); return true; }); findPreference(PREF_SEND_CRASH_REPORT).setOnPreferenceClickListener(preference -> { - Context context = getActivity().getApplicationContext(); - Intent emailIntent = new Intent(Intent.ACTION_SEND); - emailIntent.setType("text/plain"); - emailIntent.putExtra(Intent.EXTRA_EMAIL, new String[]{"Martin.Fietz@gmail.com"}); - emailIntent.putExtra(Intent.EXTRA_SUBJECT, "AntennaPod Crash Report"); - emailIntent.putExtra(Intent.EXTRA_TEXT, "Please describe what you were doing when the app crashed"); - // the attachment - Uri fileUri = FileProvider.getUriForFile(context, context.getString(R.string.provider_authority), - CrashReportWriter.getFile()); - emailIntent.putExtra(Intent.EXTRA_STREAM, fileUri); - emailIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - String intentTitle = getActivity().getString(R.string.send_email); - if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) { - List<ResolveInfo> resInfoList = context.getPackageManager().queryIntentActivities(emailIntent, PackageManager.MATCH_DEFAULT_ONLY); - for (ResolveInfo resolveInfo : resInfoList) { - String packageName = resolveInfo.activityInfo.packageName; - context.grantUriPermission(packageName, fileUri, Intent.FLAG_GRANT_READ_URI_PERMISSION); - } - } - getActivity().startActivity(Intent.createChooser(emailIntent, intentTitle)); + startActivity(new Intent(getActivity(), CrashReportActivity.class)); return true; }); } diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/StoragePreferencesFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/StoragePreferencesFragment.java index b4226b546..e36476c6f 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/StoragePreferencesFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/StoragePreferencesFragment.java @@ -4,6 +4,7 @@ import android.Manifest; import android.annotation.SuppressLint; import android.app.Activity; import android.app.ProgressDialog; +import android.content.ActivityNotFoundException; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; @@ -13,13 +14,16 @@ import android.os.Build; import android.os.Bundle; import android.support.v4.app.ActivityCompat; import android.support.v4.content.FileProvider; +import android.support.v4.provider.DocumentFile; import android.support.v7.app.AlertDialog; import android.support.v7.preference.PreferenceFragmentCompat; import android.util.Log; + import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.DirectoryChooserActivity; import de.danoeh.antennapod.activity.ImportExportActivity; import de.danoeh.antennapod.activity.OpmlImportFromPathActivity; +import de.danoeh.antennapod.asynctask.DocumentFileExportWorker; import de.danoeh.antennapod.asynctask.ExportWorker; import de.danoeh.antennapod.core.export.ExportWriter; import de.danoeh.antennapod.core.export.html.HtmlWriter; @@ -45,6 +49,12 @@ public class StoragePreferencesFragment extends PreferenceFragmentCompat { Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE }; private static final int PERMISSION_REQUEST_EXTERNAL_STORAGE = 41; + private static final int CHOOSE_OPML_EXPORT_PATH = 1; + private static final String DEFAULT_OPML_OUTPUT_NAME = "antennapod-feeds.opml"; + private static final String CONTENT_TYPE_OPML = "text/x-opml"; + private static final int CHOOSE_HTML_EXPORT_PATH = 2; + private static final String DEFAULT_HTML_OUTPUT_NAME = "antennapod-feeds.html"; + private static final String CONTENT_TYPE_HTML = "text/html"; private Disposable disposable; @Override @@ -59,6 +69,14 @@ public class StoragePreferencesFragment extends PreferenceFragmentCompat { setDataFolderText(); } + @Override + public void onStop() { + super.onStop(); + if (disposable != null) { + disposable.dispose(); + } + } + private void setupStorageScreen() { final Activity activity = getActivity(); @@ -69,9 +87,16 @@ public class StoragePreferencesFragment extends PreferenceFragmentCompat { } ); findPreference(PREF_OPML_EXPORT).setOnPreferenceClickListener( - preference -> export(new OpmlWriter())); + preference -> { + openOpmlExportPathPicker(); + return true; + } + ); findPreference(PREF_HTML_EXPORT).setOnPreferenceClickListener( - preference -> export(new HtmlWriter())); + preference -> { + openHtmlExportPathPicker(); + return true; + }); findPreference(PREF_OPML_IMPORT).setOnPreferenceClickListener( preference -> { activity.startActivity(new Intent(activity, OpmlImportFromPathActivity.class)); @@ -129,52 +154,65 @@ public class StoragePreferencesFragment extends PreferenceFragmentCompat { } private boolean export(ExportWriter exportWriter) { + return export(exportWriter, null); + } + + private boolean export(ExportWriter exportWriter, final Uri uri) { Context context = getActivity(); final ProgressDialog progressDialog = new ProgressDialog(context); progressDialog.setMessage(context.getString(R.string.exporting_label)); progressDialog.setIndeterminate(true); progressDialog.show(); - final AlertDialog.Builder alert = new AlertDialog.Builder(context) - .setNeutralButton(android.R.string.ok, (dialog, which) -> dialog.dismiss()); - Observable<File> observable = new ExportWorker(exportWriter).exportObservable(); - disposable = observable.subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(output -> { - alert.setTitle(R.string.export_success_title); - String message = context.getString(R.string.export_success_sum, output.toString()); - alert.setMessage(message); - alert.setPositiveButton(R.string.send_label, (dialog, which) -> { + if (uri == null) { + Observable<File> observable = new ExportWorker(exportWriter).exportObservable(); + disposable = observable.subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(output -> { Uri fileUri = FileProvider.getUriForFile(context.getApplicationContext(), context.getString(R.string.provider_authority), output); - Intent sendIntent = new Intent(Intent.ACTION_SEND); - sendIntent.putExtra(Intent.EXTRA_SUBJECT, - context.getResources().getText(R.string.opml_export_label)); - sendIntent.putExtra(Intent.EXTRA_STREAM, fileUri); - sendIntent.setType("text/plain"); - sendIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) { - List<ResolveInfo> resInfoList = context.getPackageManager().queryIntentActivities(sendIntent, PackageManager.MATCH_DEFAULT_ONLY); - for (ResolveInfo resolveInfo : resInfoList) { - String packageName = resolveInfo.activityInfo.packageName; - context.grantUriPermission(packageName, fileUri, Intent.FLAG_GRANT_READ_URI_PERMISSION); - } - } - context.startActivity(Intent.createChooser(sendIntent, - context.getResources().getText(R.string.send_label))); - }); - alert.create().show(); - }, error -> { - alert.setTitle(R.string.export_error_label); - alert.setMessage(error.getMessage()); - alert.show(); - }, progressDialog::dismiss); + showExportSuccessDialog(context.getString(R.string.export_success_sum, output.toString()), fileUri); + }, this::showExportErrorDialog, progressDialog::dismiss); + } else { + Observable<DocumentFile> observable = new DocumentFileExportWorker(exportWriter, context, uri).exportObservable(); + disposable = observable.subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(output -> { + showExportSuccessDialog(context.getString(R.string.export_success_sum, output.getUri()), output.getUri()); + }, this::showExportErrorDialog, progressDialog::dismiss); + } return true; } - public void unsubscribeExportSubscription() { - if (disposable != null) { - disposable.dispose(); - } + private void showExportSuccessDialog(final String message, final Uri streamUri) { + final AlertDialog.Builder alert = new AlertDialog.Builder(getContext()) + .setNeutralButton(android.R.string.ok, (dialog, which) -> dialog.dismiss()); + alert.setTitle(R.string.export_success_title); + alert.setMessage(message); + alert.setPositiveButton(R.string.send_label, (dialog, which) -> { + Intent sendIntent = new Intent(Intent.ACTION_SEND); + sendIntent.putExtra(Intent.EXTRA_SUBJECT, getString(R.string.opml_export_label)); + sendIntent.putExtra(Intent.EXTRA_STREAM, streamUri); + sendIntent.setType("text/plain"); + sendIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) { + List<ResolveInfo> resInfoList = getContext().getPackageManager() + .queryIntentActivities(sendIntent, PackageManager.MATCH_DEFAULT_ONLY); + for (ResolveInfo resolveInfo : resInfoList) { + String packageName = resolveInfo.activityInfo.packageName; + getContext().grantUriPermission(packageName, streamUri, Intent.FLAG_GRANT_READ_URI_PERMISSION); + } + } + getContext().startActivity(Intent.createChooser(sendIntent, getString(R.string.send_label))); + }); + alert.create().show(); + } + + private void showExportErrorDialog(final Throwable error) { + final AlertDialog.Builder alert = new AlertDialog.Builder(getContext()) + .setNeutralButton(android.R.string.ok, (dialog, which) -> dialog.dismiss()); + alert.setTitle(R.string.export_error_label); + alert.setMessage(error.getMessage()); + alert.show(); } @SuppressLint("NewApi") @@ -184,22 +222,22 @@ public class StoragePreferencesFragment extends PreferenceFragmentCompat { String dir = data.getStringExtra(DirectoryChooserActivity.RESULT_SELECTED_DIR); File path; - if(dir != null) { + if (dir != null) { path = new File(dir); } else { path = getActivity().getExternalFilesDir(null); } String message = null; - final Context context= getActivity().getApplicationContext(); - if(!path.exists()) { + final Context context = getActivity().getApplicationContext(); + if (!path.exists()) { message = String.format(context.getString(R.string.folder_does_not_exist_error), dir); - } else if(!path.canRead()) { + } else if (!path.canRead()) { message = String.format(context.getString(R.string.folder_not_readable_error), dir); - } else if(!path.canWrite()) { + } else if (!path.canWrite()) { message = String.format(context.getString(R.string.folder_not_writable_error), dir); } - if(message == null) { + if (message == null) { Log.d(TAG, "Setting data folder: " + dir); UserPreferences.setDataFolder(dir); setDataFolderText(); @@ -210,6 +248,16 @@ public class StoragePreferencesFragment extends PreferenceFragmentCompat { ab.show(); } } + + if (resultCode == Activity.RESULT_OK && requestCode == CHOOSE_OPML_EXPORT_PATH) { + Uri uri = data.getData(); + export(new OpmlWriter(), uri); + } + + if (resultCode == Activity.RESULT_OK && requestCode == CHOOSE_HTML_EXPORT_PATH) { + Uri uri = data.getData(); + export(new HtmlWriter(), uri); + } } private void setDataFolderText() { @@ -231,6 +279,50 @@ public class StoragePreferencesFragment extends PreferenceFragmentCompat { activity.startActivityForResult(intent, DirectoryChooserActivity.RESULT_CODE_DIR_SELECTED); } + private void openOpmlExportPathPicker() { + if (Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN_MR2) { + Intent intentPickAction = new Intent(Intent.ACTION_CREATE_DOCUMENT) + .addCategory(Intent.CATEGORY_OPENABLE) + .setType(CONTENT_TYPE_OPML) + .putExtra(Intent.EXTRA_TITLE, DEFAULT_OPML_OUTPUT_NAME); + + // Creates an implicit intent to launch a file manager which lets + // the user choose a specific directory to export to. + try { + startActivityForResult(intentPickAction, CHOOSE_OPML_EXPORT_PATH); + return; + } catch (ActivityNotFoundException e) { + Log.e(TAG, "No activity found. Should never happen..."); + } + } + + // If we are using a SDK lower than API 21 or the implicit intent failed + // fallback to the legacy export process + export(new OpmlWriter()); + } + + private void openHtmlExportPathPicker() { + if (Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN_MR2) { + Intent intentPickAction = new Intent(Intent.ACTION_CREATE_DOCUMENT) + .addCategory(Intent.CATEGORY_OPENABLE) + .setType(CONTENT_TYPE_HTML) + .putExtra(Intent.EXTRA_TITLE, DEFAULT_HTML_OUTPUT_NAME); + + // Creates an implicit intent to launch a file manager which lets + // the user choose a specific directory to export to. + try { + startActivityForResult(intentPickAction, CHOOSE_HTML_EXPORT_PATH); + return; + } catch (ActivityNotFoundException e) { + Log.e(TAG, "No activity found. Should never happen..."); + } + } + + // If we are using a SDK lower than API 21 or the implicit intent failed + // fallback to the legacy export process + export(new HtmlWriter()); + } + private void showChooseDataFolderDialog() { ChooseDataFolderDialog.showDialog( getActivity(), new ChooseDataFolderDialog.RunnableWithString() { diff --git a/app/src/main/java/de/danoeh/antennapod/preferences/PreferenceUpgrader.java b/app/src/main/java/de/danoeh/antennapod/preferences/PreferenceUpgrader.java index 4d6fbcb7b..a33faeb69 100644 --- a/app/src/main/java/de/danoeh/antennapod/preferences/PreferenceUpgrader.java +++ b/app/src/main/java/de/danoeh/antennapod/preferences/PreferenceUpgrader.java @@ -42,10 +42,8 @@ public class PreferenceUpgrader { } } if (oldVersion < 1070300) { - if (UserPreferences.getMediaPlayer().equals("builtin")) { - prefs.edit().putString(UserPreferences.PREF_MEDIA_PLAYER, - UserPreferences.PREF_MEDIA_PLAYER_EXOPLAYER).apply(); - } + prefs.edit().putString(UserPreferences.PREF_MEDIA_PLAYER, + UserPreferences.PREF_MEDIA_PLAYER_EXOPLAYER).apply(); if (prefs.getBoolean("prefEnableAutoDownloadOnMobile", false)) { UserPreferences.setAllowMobileAutoDownload(true); @@ -64,5 +62,8 @@ public class PreferenceUpgrader { break; } } + if (oldVersion < 1070400) { + UserPreferences.setQueueLocked(false); + } } } diff --git a/app/src/main/java/de/danoeh/antennapod/spa/SPAUtil.java b/app/src/main/java/de/danoeh/antennapod/spa/SPAUtil.java index 03958508d..5fa6588d9 100644 --- a/app/src/main/java/de/danoeh/antennapod/spa/SPAUtil.java +++ b/app/src/main/java/de/danoeh/antennapod/spa/SPAUtil.java @@ -46,7 +46,7 @@ public class SPAUtil { SharedPreferences.Editor editor = prefs.edit(); editor.putBoolean(PREF_HAS_QUERIED_SP_APPS, true); - editor.commit(); + editor.apply(); return true; } else { @@ -63,7 +63,7 @@ public class SPAUtil { SharedPreferences.Editor editor = PreferenceManager .getDefaultSharedPreferences(c.getApplicationContext()).edit(); editor.putBoolean(PREF_HAS_QUERIED_SP_APPS, false); - editor.commit(); + editor.apply(); } } } diff --git a/app/src/main/res/layout/addfeed.xml b/app/src/main/res/layout/addfeed.xml index ef8251a06..a7f7d9f12 100644 --- a/app/src/main/res/layout/addfeed.xml +++ b/app/src/main/res/layout/addfeed.xml @@ -10,6 +10,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" + android:focusableInTouchMode="true" android:padding="8dp"> <android.support.v7.widget.CardView diff --git a/app/src/main/res/layout/checkbox_do_not_show_again.xml b/app/src/main/res/layout/checkbox_do_not_show_again.xml new file mode 100644 index 000000000..15f26e8b4 --- /dev/null +++ b/app/src/main/res/layout/checkbox_do_not_show_again.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical" + android:paddingLeft="16dp" + android:paddingRight="16dp" + android:paddingTop="8dp" + android:paddingBottom="8dp"> + + <CheckBox + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:id="@+id/checkbox_do_not_show_again" + android:text="@string/checkbox_do_not_show_again"/> + +</LinearLayout>
\ No newline at end of file diff --git a/app/src/main/res/layout/crash_report.xml b/app/src/main/res/layout/crash_report.xml new file mode 100644 index 000000000..e97e85265 --- /dev/null +++ b/app/src/main/res/layout/crash_report.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical" + android:padding="16dp"> + <Button + android:id="@+id/btn_open_bug_tracker" + android:text="@string/open_bug_tracker" + android:layout_width="match_parent" + android:layout_height="wrap_content"/> + + <Button + android:id="@+id/btn_copy_log" + android:text="@string/copy_to_clipboard" + android:layout_width="match_parent" + android:layout_height="wrap_content"/> + + <TextView + android:layout_marginTop="8dp" + android:id="@+id/crash_report_logs" + android:textIsSelectable="true" + android:textSize="12sp" + android:layout_width="match_parent" + android:layout_height="0dp" + android:layout_weight="1"/> + +</LinearLayout> diff --git a/app/src/main/res/layout/downloaded_episodeslist_item.xml b/app/src/main/res/layout/downloaded_episodeslist_item.xml index 65a08251f..3f8065466 100644 --- a/app/src/main/res/layout/downloaded_episodeslist_item.xml +++ b/app/src/main/res/layout/downloaded_episodeslist_item.xml @@ -17,7 +17,7 @@ android:layout_marginTop="@dimen/listitem_threeline_verticalpadding" android:contentDescription="@string/cover_label" android:scaleType="centerCrop" - tools:src="@drawable/ic_stat_antenna_default" + tools:src="@drawable/ic_antenna" tools:background="@android:color/holo_green_dark"/> diff --git a/app/src/main/res/layout/episodes_apply_action_fragment.xml b/app/src/main/res/layout/episodes_apply_action_fragment.xml index d6e18bb37..ad453afbe 100644 --- a/app/src/main/res/layout/episodes_apply_action_fragment.xml +++ b/app/src/main/res/layout/episodes_apply_action_fragment.xml @@ -29,12 +29,9 @@ android:layout_alignParentBottom="true" android:layout_alignParentEnd="true" android:layout_alignParentRight="true" - android:elevation="@dimen/sd_close_elevation" - tools:ignore="UnusedAttribute"> - <!-- android:elevation: - 1. Needs to match the speed dial's minimal elevation, - or the speed dial can't be clicked at all - --> + android:elevation="@dimen/sd_open_elevation" + tools:ignore="UnusedAttribute" > + <com.leinardi.android.speeddial.SpeedDialView android:id="@+id/fabSD" android:layout_width="match_parent" diff --git a/app/src/main/res/layout/feeditem_fragment.xml b/app/src/main/res/layout/feeditem_fragment.xml index 78c0b3b16..b047b3da0 100644 --- a/app/src/main/res/layout/feeditem_fragment.xml +++ b/app/src/main/res/layout/feeditem_fragment.xml @@ -36,7 +36,8 @@ android:layout_marginBottom="16dp" android:contentDescription="@string/cover_label" android:gravity="center_vertical" - tools:src="@drawable/ic_stat_antenna_default" + android:foreground="?attr/selectableItemBackground" + tools:src="@drawable/ic_antenna" tools:background="@android:color/holo_green_dark" /> <TextView @@ -47,6 +48,7 @@ android:layout_alignTop="@id/imgvCover" android:layout_toRightOf="@id/imgvCover" android:layout_toEndOf="@id/imgvCover" + android:foreground="?attr/selectableItemBackground" tools:text="Podcast title" tools:background="@android:color/holo_green_dark" /> diff --git a/app/src/main/res/layout/feeditemlist_header.xml b/app/src/main/res/layout/feeditemlist_header.xml index e1f451e9e..596135d88 100644 --- a/app/src/main/res/layout/feeditemlist_header.xml +++ b/app/src/main/res/layout/feeditemlist_header.xml @@ -27,7 +27,7 @@ android:layout_marginStart="16dp" android:layout_marginTop="16dp" android:contentDescription="@string/cover_label" - tools:src="@drawable/ic_stat_antenna_default" + tools:src="@drawable/ic_antenna" tools:background="@android:color/holo_green_dark"/> <ImageButton diff --git a/app/src/main/res/layout/gpodnet_podcast_listitem.xml b/app/src/main/res/layout/gpodnet_podcast_listitem.xml index 27a8bbdca..6e02fa090 100644 --- a/app/src/main/res/layout/gpodnet_podcast_listitem.xml +++ b/app/src/main/res/layout/gpodnet_podcast_listitem.xml @@ -23,7 +23,7 @@ android:contentDescription="@string/cover_label" android:cropToPadding="true" android:scaleType="fitXY" - tools:src="@drawable/ic_stat_antenna_default" + tools:src="@drawable/ic_antenna" tools:background="@android:color/holo_green_dark" /> <LinearLayout diff --git a/app/src/main/res/layout/itunes_podcast_listitem.xml b/app/src/main/res/layout/itunes_podcast_listitem.xml index 4848563b1..b2411c5df 100644 --- a/app/src/main/res/layout/itunes_podcast_listitem.xml +++ b/app/src/main/res/layout/itunes_podcast_listitem.xml @@ -25,7 +25,7 @@ android:cropToPadding="true" android:scaleType="fitXY" tools:background="@android:color/holo_green_dark" - tools:src="@drawable/ic_stat_antenna_default" /> + tools:src="@drawable/ic_antenna" /> <LinearLayout android:layout_width="match_parent" diff --git a/app/src/main/res/layout/nav_feedlistitem.xml b/app/src/main/res/layout/nav_feedlistitem.xml index 816870d1c..52833b3cd 100644 --- a/app/src/main/res/layout/nav_feedlistitem.xml +++ b/app/src/main/res/layout/nav_feedlistitem.xml @@ -25,7 +25,7 @@ android:scaleType="centerCrop" android:layout_marginTop="4dp" android:layout_marginBottom="4dp" - tools:src="@drawable/ic_stat_antenna_default" + tools:src="@drawable/ic_antenna" tools:background="@android:color/holo_green_dark"/> <TextView diff --git a/app/src/main/res/layout/queue_listitem.xml b/app/src/main/res/layout/queue_listitem.xml index 6b41b68d5..1dcc34bce 100644 --- a/app/src/main/res/layout/queue_listitem.xml +++ b/app/src/main/res/layout/queue_listitem.xml @@ -51,7 +51,7 @@ android:layout_height="@dimen/thumbnail_length_queue_item" android:layout_centerVertical="true" android:contentDescription="@string/cover_label" - tools:src="@drawable/ic_stat_antenna_default" + tools:src="@drawable/ic_antenna" tools:background="@android:color/holo_green_dark"/> </RelativeLayout> diff --git a/app/src/main/res/layout/searchlist_item.xml b/app/src/main/res/layout/searchlist_item.xml index 50374c737..4a055fea9 100644 --- a/app/src/main/res/layout/searchlist_item.xml +++ b/app/src/main/res/layout/searchlist_item.xml @@ -18,7 +18,7 @@ android:layout_marginStart="@dimen/listitem_threeline_horizontalpadding" android:contentDescription="@string/cover_label" android:scaleType="centerCrop" - tools:src="@drawable/ic_stat_antenna_default" + tools:src="@drawable/ic_antenna" tools:background="@android:color/holo_green_dark"/> <LinearLayout diff --git a/app/src/main/res/layout/statistics_listitem.xml b/app/src/main/res/layout/statistics_listitem.xml index b186add9e..f52aa73e0 100644 --- a/app/src/main/res/layout/statistics_listitem.xml +++ b/app/src/main/res/layout/statistics_listitem.xml @@ -23,7 +23,7 @@ android:scaleType="centerCrop" android:layout_marginTop="4dp" android:layout_marginBottom="4dp" - tools:src="@drawable/ic_stat_antenna_default" + tools:src="@drawable/ic_antenna" tools:background="@android:color/holo_green_dark"/> <TextView diff --git a/app/src/main/templates/about.html b/app/src/main/templates/about.html index 4cd34bd48..fd70ab549 100644 --- a/app/src/main/templates/about.html +++ b/app/src/main/templates/about.html @@ -83,7 +83,8 @@ Created by Daniel Oeh<br /> Copyright © 2012-@year@<br /> AntennaPod Contributors <a href="CONTRIBUTORS.txt">(View)</a><br /> -Licensed under the MIT License <a href="LICENSE.txt">(View)</a> +Licensed under the MIT License <a href="LICENSE.txt">(View)</a><br /> +Privacy Policy <a href="https://antennapod.org/privacy.html">(View)</a> </div> <h1>Used libraries</h1> |