diff options
author | H. Lehmann <ByteHamster@users.noreply.github.com> | 2019-08-11 14:57:44 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2019-08-11 14:57:44 +0200 |
commit | 1315c9e20b30e62b8022f831a0bcf5779b5bb2cb (patch) | |
tree | 65dd884ea83389d366d971ca5635e6e9f6c95dc4 /app/src/main/java/de/danoeh/antennapod | |
parent | ab864cd171a563177a8f565fc2744e88ba669516 (diff) | |
parent | ce64c412ac02041d877e4770381f29710a06ba17 (diff) | |
download | AntennaPod-1315c9e20b30e62b8022f831a0bcf5779b5bb2cb.zip |
Merge branch 'develop' into make_multidex_on_debug_build_only
Diffstat (limited to 'app/src/main/java/de/danoeh/antennapod')
88 files changed, 4688 insertions, 3873 deletions
diff --git a/app/src/main/java/de/danoeh/antennapod/PodcastApp.java b/app/src/main/java/de/danoeh/antennapod/PodcastApp.java index fde9af16f..cb2f597d6 100644 --- a/app/src/main/java/de/danoeh/antennapod/PodcastApp.java +++ b/app/src/main/java/de/danoeh/antennapod/PodcastApp.java @@ -8,9 +8,11 @@ import com.joanzapata.iconify.Iconify; import com.joanzapata.iconify.fonts.FontAwesomeModule; import com.joanzapata.iconify.fonts.MaterialModule; +import de.danoeh.antennapod.core.ApCoreEventBusIndex; import de.danoeh.antennapod.core.ClientConfig; import de.danoeh.antennapod.core.feed.EventDistributor; import de.danoeh.antennapod.spa.SPAUtil; +import org.greenrobot.eventbus.EventBus; /** Main application class. */ public class PodcastApp extends Application { @@ -58,6 +60,10 @@ public class PodcastApp extends Application { Iconify.with(new MaterialModule()); SPAUtil.sendSPAppsQueryFeedsIntent(this); + EventBus.builder() + .addIndex(new ApEventBusIndex()) + .addIndex(new ApCoreEventBusIndex()) + .installDefaultEventBus(); } } diff --git a/app/src/main/java/de/danoeh/antennapod/activity/AboutActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/AboutActivity.java index ecfdf24b0..1bcdada44 100644 --- a/app/src/main/java/de/danoeh/antennapod/activity/AboutActivity.java +++ b/app/src/main/java/de/danoeh/antennapod/activity/AboutActivity.java @@ -1,7 +1,9 @@ package de.danoeh.antennapod.activity; +import android.content.Intent; import android.content.res.TypedArray; import android.graphics.Color; +import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.support.v7.app.AppCompatActivity; @@ -46,22 +48,23 @@ public class AboutActivity extends AppCompatActivity { webViewContainer = findViewById(R.id.webViewContainer); webView = findViewById(R.id.webViewAbout); webView.getSettings().setCacheMode(WebSettings.LOAD_NO_CACHE); - if (UserPreferences.getTheme() == R.style.Theme_AntennaPod_Dark) { - if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) { - webView.setLayerType(View.LAYER_TYPE_SOFTWARE, null); - } - webView.setBackgroundColor(Color.TRANSPARENT); + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) { + webView.setLayerType(View.LAYER_TYPE_SOFTWARE, null); } + webView.setBackgroundColor(Color.TRANSPARENT); webView.setWebViewClient(new WebViewClient() { @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { - if (!url.startsWith("http")) { + if (url.startsWith("http")) { + Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); + startActivity(browserIntent); + return true; + } else { url = url.replace("file:///android_asset/", ""); loadAsset(url); return true; } - return false; } }); 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 67dda01cf..2bcd7a461 100644 --- a/app/src/main/java/de/danoeh/antennapod/activity/AudioplayerActivity.java +++ b/app/src/main/java/de/danoeh/antennapod/activity/AudioplayerActivity.java @@ -129,6 +129,7 @@ public class AudioplayerActivity extends MediaplayerInfoActivity { } UserPreferences.setPlaybackSpeed(newSpeed); controller.setPlaybackSpeed(Float.parseFloat(newSpeed)); + onPositionObserverUpdate(); } else { VariableSpeedDialog.showGetPluginDialog(this); } diff --git a/app/src/main/java/de/danoeh/antennapod/activity/FeedInfoActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/FeedInfoActivity.java index bfa694e5c..26e360bd3 100644 --- a/app/src/main/java/de/danoeh/antennapod/activity/FeedInfoActivity.java +++ b/app/src/main/java/de/danoeh/antennapod/activity/FeedInfoActivity.java @@ -199,8 +199,6 @@ public class FeedInfoActivity extends AppCompatActivity { @Override public boolean onPrepareOptionsMenu(Menu menu) { super.onPrepareOptionsMenu(menu); - menu.findItem(R.id.support_item).setVisible( - feed != null && feed.getPaymentLink() != null); menu.findItem(R.id.share_link_item).setVisible(feed != null && feed.getLink() != null); menu.findItem(R.id.visit_website_item).setVisible(feed != null && feed.getLink() != null && IntentUtils.isCallable(this, new Intent(Intent.ACTION_VIEW, Uri.parse(feed.getLink())))); diff --git a/app/src/main/java/de/danoeh/antennapod/activity/FeedSettingsActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/FeedSettingsActivity.java index 4698ed90e..fbd19f88a 100644 --- a/app/src/main/java/de/danoeh/antennapod/activity/FeedSettingsActivity.java +++ b/app/src/main/java/de/danoeh/antennapod/activity/FeedSettingsActivity.java @@ -1,48 +1,26 @@ package de.danoeh.antennapod.activity; -import android.content.Context; -import android.content.DialogInterface; -import android.content.Intent; +import android.arch.lifecycle.ViewModelProviders; import android.graphics.LightingColorFilter; -import android.net.Uri; import android.os.Bundle; +import android.support.v4.app.FragmentManager; +import android.support.v4.app.FragmentTransaction; import android.support.v7.app.AppCompatActivity; -import android.text.Editable; import android.text.TextUtils; -import android.text.TextWatcher; import android.util.Log; -import android.view.Menu; -import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; -import android.widget.AdapterView; -import android.widget.AdapterView.OnItemSelectedListener; -import android.widget.CheckBox; -import android.widget.EditText; import android.widget.ImageView; -import android.widget.RadioButton; -import android.widget.Spinner; import android.widget.TextView; - import com.bumptech.glide.Glide; import com.bumptech.glide.request.RequestOptions; - import de.danoeh.antennapod.R; -import de.danoeh.antennapod.core.dialog.ConfirmationDialog; -import de.danoeh.antennapod.core.dialog.DownloadRequestErrorDialogCreator; import de.danoeh.antennapod.core.feed.Feed; -import de.danoeh.antennapod.core.feed.FeedFilter; -import de.danoeh.antennapod.core.feed.FeedPreferences; import de.danoeh.antennapod.core.glide.ApGlideSettings; import de.danoeh.antennapod.core.glide.FastBlurTransformation; import de.danoeh.antennapod.core.preferences.UserPreferences; -import de.danoeh.antennapod.core.storage.DBReader; -import de.danoeh.antennapod.core.storage.DBWriter; -import de.danoeh.antennapod.core.storage.DownloadRequestException; -import de.danoeh.antennapod.core.util.IntentUtils; -import de.danoeh.antennapod.menuhandler.FeedMenuHandler; -import io.reactivex.Maybe; -import io.reactivex.MaybeOnSubscribe; +import de.danoeh.antennapod.fragment.FeedSettingsFragment; +import de.danoeh.antennapod.viewmodel.FeedSettingsViewModel; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.Disposable; import io.reactivex.schedulers.Schedulers; @@ -51,59 +29,14 @@ import io.reactivex.schedulers.Schedulers; * Displays information about a feed. */ public class FeedSettingsActivity extends AppCompatActivity { - public static final String EXTRA_FEED_ID = "de.danoeh.antennapod.extra.feedId"; private static final String TAG = "FeedSettingsActivity"; - private boolean autoDeleteChanged = false; private Feed feed; - + private Disposable disposable; private ImageView imgvCover; private TextView txtvTitle; - private EditText etxtUsername; - private EditText etxtPassword; - private EditText etxtFilterText; - private RadioButton rdoFilterInclude; - private RadioButton rdoFilterExclude; - private CheckBox cbxAutoDownload; - private CheckBox cbxKeepUpdated; - private Spinner spnAutoDelete; - private boolean filterInclude = true; - - private Disposable disposable; - - private boolean authInfoChanged = false; - - private final TextWatcher authTextWatcher = 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) { - authInfoChanged = true; - } - }; - - private boolean filterTextChanged = false; - - private final TextWatcher filterTextWatcher = 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) { - filterTextChanged = true; - } - }; + private ImageView imgvBackground; + private TextView txtvAuthorHeader; @Override protected void onCreate(Bundle savedInstanceState) { @@ -111,142 +44,24 @@ public class FeedSettingsActivity extends AppCompatActivity { super.onCreate(savedInstanceState); setContentView(R.layout.feedsettings); getSupportActionBar().setDisplayHomeAsUpEnabled(true); - long feedId = getIntent().getLongExtra(EXTRA_FEED_ID, -1); imgvCover = findViewById(R.id.imgvCover); txtvTitle = findViewById(R.id.txtvTitle); - TextView txtvAuthorHeader = findViewById(R.id.txtvAuthor); - ImageView imgvBackground = findViewById(R.id.imgvBackground); + txtvAuthorHeader = findViewById(R.id.txtvAuthor); + imgvBackground = findViewById(R.id.imgvBackground); findViewById(R.id.butShowInfo).setVisibility(View.INVISIBLE); findViewById(R.id.butShowSettings).setVisibility(View.INVISIBLE); // https://github.com/bumptech/glide/issues/529 imgvBackground.setColorFilter(new LightingColorFilter(0xff828282, 0x000000)); - cbxAutoDownload = findViewById(R.id.cbxAutoDownload); - cbxKeepUpdated = findViewById(R.id.cbxKeepUpdated); - spnAutoDelete = findViewById(R.id.spnAutoDelete); - etxtUsername = findViewById(R.id.etxtUsername); - etxtPassword = findViewById(R.id.etxtPassword); - etxtFilterText = findViewById(R.id.etxtEpisodeFilterText); - rdoFilterInclude = findViewById(R.id.radio_filter_include); - rdoFilterInclude.setOnClickListener(v -> { - filterInclude = true; - filterTextChanged = true; - }); - rdoFilterExclude = findViewById(R.id.radio_filter_exclude); - rdoFilterExclude.setOnClickListener(v -> { - filterInclude = false; - filterTextChanged = true; - }); - - disposable = Maybe.create((MaybeOnSubscribe<Feed>) emitter -> { - Feed feed = DBReader.getFeed(feedId); - if (feed != null) { - emitter.onSuccess(feed); - } else { - emitter.onComplete(); - } - }) + long feedId = getIntent().getLongExtra(EXTRA_FEED_ID, -1); + disposable = ViewModelProviders.of(this).get(FeedSettingsViewModel.class).getFeed(feedId) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(result -> { feed = result; - FeedPreferences prefs = feed.getPreferences(); - Glide.with(FeedSettingsActivity.this) - .load(feed.getImageLocation()) - .apply(new RequestOptions() - .placeholder(R.color.light_gray) - .error(R.color.light_gray) - .diskCacheStrategy(ApGlideSettings.AP_DISK_CACHE_STRATEGY) - .fitCenter() - .dontAnimate()) - .into(imgvCover); - Glide.with(FeedSettingsActivity.this) - .load(feed.getImageLocation()) - .apply(new RequestOptions() - .placeholder(R.color.image_readability_tint) - .error(R.color.image_readability_tint) - .diskCacheStrategy(ApGlideSettings.AP_DISK_CACHE_STRATEGY) - .transform(new FastBlurTransformation()) - .dontAnimate()) - .into(imgvBackground); - - txtvTitle.setText(feed.getTitle()); - - if (!TextUtils.isEmpty(feed.getAuthor())) { - txtvAuthorHeader.setText(feed.getAuthor()); - } - - cbxAutoDownload.setEnabled(UserPreferences.isEnableAutodownload()); - cbxAutoDownload.setChecked(prefs.getAutoDownload()); - cbxAutoDownload.setOnCheckedChangeListener((compoundButton, checked) -> { - feed.getPreferences().setAutoDownload(checked); - feed.savePreferences(); - updateAutoDownloadSettings(); - ApplyToEpisodesDialog dialog = new ApplyToEpisodesDialog(FeedSettingsActivity.this, - feed, checked); - dialog.createNewDialog().show(); - }); - cbxKeepUpdated.setChecked(prefs.getKeepUpdated()); - cbxKeepUpdated.setOnCheckedChangeListener((compoundButton, checked) -> { - feed.getPreferences().setKeepUpdated(checked); - feed.savePreferences(); - }); - spnAutoDelete.setOnItemSelectedListener(new OnItemSelectedListener() { - @Override - public void onItemSelected(AdapterView<?> parent, View view, int pos, long id) { - FeedPreferences.AutoDeleteAction auto_delete_action; - switch (parent.getSelectedItemPosition()) { - case 0: - auto_delete_action = FeedPreferences.AutoDeleteAction.GLOBAL; - break; - case 1: - auto_delete_action = FeedPreferences.AutoDeleteAction.YES; - break; - case 2: - auto_delete_action = FeedPreferences.AutoDeleteAction.NO; - break; - default: // TODO - add exceptions here - return; - } - feed.getPreferences().setAutoDeleteAction(auto_delete_action);// p - autoDeleteChanged = true; - } - - @Override - public void onNothingSelected(AdapterView<?> parent) { - // Another interface callback - } - }); - spnAutoDelete.setSelection(prefs.getAutoDeleteAction().ordinal()); - - etxtUsername.setText(prefs.getUsername()); - etxtPassword.setText(prefs.getPassword()); - - etxtUsername.addTextChangedListener(authTextWatcher); - etxtPassword.addTextChangedListener(authTextWatcher); - - FeedFilter filter = prefs.getFilter(); - if (filter.includeOnly()) { - etxtFilterText.setText(filter.getIncludeFilter()); - rdoFilterInclude.setChecked(true); - rdoFilterExclude.setChecked(false); - filterInclude = true; - } else if (filter.excludeOnly()) { - etxtFilterText.setText(filter.getExcludeFilter()); - rdoFilterInclude.setChecked(false); - rdoFilterExclude.setChecked(true); - filterInclude = false; - } else { - Log.d(TAG, "No filter set"); - rdoFilterInclude.setChecked(false); - rdoFilterExclude.setChecked(false); - etxtFilterText.setText(""); - } - etxtFilterText.addTextChangedListener(filterTextWatcher); - - supportInvalidateOptionsMenu(); - updateAutoDownloadSettings(); + showFragment(); + showHeader(); }, error -> { Log.d(TAG, Log.getStackTraceString(error)); finish(); @@ -256,35 +71,42 @@ public class FeedSettingsActivity extends AppCompatActivity { }); } - @Override - protected void onPause() { - super.onPause(); - if (feed != null) { - FeedPreferences prefs = feed.getPreferences(); - if (authInfoChanged) { - Log.d(TAG, "Auth info changed, saving credentials"); - prefs.setUsername(etxtUsername.getText().toString()); - prefs.setPassword(etxtPassword.getText().toString()); - } - if (filterTextChanged) { - Log.d(TAG, "Filter info changed, saving..."); - String filterText = etxtFilterText.getText().toString(); - String includeString = ""; - String excludeString = ""; - if (filterInclude) { - includeString = filterText; - } else { - excludeString = filterText; - } - prefs.setFilter(new FeedFilter(includeString, excludeString)); - } - if (authInfoChanged || autoDeleteChanged || filterTextChanged) { - DBWriter.setFeedPreferences(prefs); - } - authInfoChanged = false; - autoDeleteChanged = false; - filterTextChanged = false; + private void showFragment() { + FeedSettingsFragment fragment = new FeedSettingsFragment(); + fragment.setArguments(getIntent().getExtras()); + + FragmentManager fragmentManager = getSupportFragmentManager(); + FragmentTransaction fragmentTransaction = + fragmentManager.beginTransaction(); + fragmentTransaction.replace(R.id.settings_fragment_container, fragment); + fragmentTransaction.commit(); + } + + private void showHeader() { + txtvTitle.setText(feed.getTitle()); + + if (!TextUtils.isEmpty(feed.getAuthor())) { + txtvAuthorHeader.setText(feed.getAuthor()); } + + Glide.with(FeedSettingsActivity.this) + .load(feed.getImageLocation()) + .apply(new RequestOptions() + .placeholder(R.color.light_gray) + .error(R.color.light_gray) + .diskCacheStrategy(ApGlideSettings.AP_DISK_CACHE_STRATEGY) + .fitCenter() + .dontAnimate()) + .into(imgvCover); + Glide.with(FeedSettingsActivity.this) + .load(feed.getImageLocation()) + .apply(new RequestOptions() + .placeholder(R.color.image_readability_tint) + .error(R.color.image_readability_tint) + .diskCacheStrategy(ApGlideSettings.AP_DISK_CACHE_STRATEGY) + .transform(new FastBlurTransformation()) + .dontAnimate()) + .into(imgvBackground); } @Override @@ -296,69 +118,13 @@ public class FeedSettingsActivity extends AppCompatActivity { } @Override - public boolean onCreateOptionsMenu(Menu menu) { - super.onCreateOptionsMenu(menu); - MenuInflater inflater = getMenuInflater(); - inflater.inflate(R.menu.feedinfo, menu); - return true; - } - - @Override - public boolean onPrepareOptionsMenu(Menu menu) { - super.onPrepareOptionsMenu(menu); - menu.findItem(R.id.support_item).setVisible( - feed != null && feed.getPaymentLink() != null); - menu.findItem(R.id.share_link_item).setVisible(feed != null && feed.getLink() != null); - menu.findItem(R.id.visit_website_item).setVisible(feed != null && feed.getLink() != null && - IntentUtils.isCallable(this, new Intent(Intent.ACTION_VIEW, Uri.parse(feed.getLink())))); - return true; - } - - @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case android.R.id.home: finish(); return true; default: - try { - return FeedMenuHandler.onOptionsItemClicked(this, item, feed); - } catch (DownloadRequestException e) { - e.printStackTrace(); - DownloadRequestErrorDialogCreator.newRequestErrorDialog(this, - e.getMessage()); - } return super.onOptionsItemSelected(item); } } - - private void updateAutoDownloadSettings() { - if (feed != null && feed.getPreferences() != null) { - boolean enabled = feed.getPreferences().getAutoDownload() && UserPreferences.isEnableAutodownload(); - rdoFilterInclude.setEnabled(enabled); - rdoFilterExclude.setEnabled(enabled); - etxtFilterText.setEnabled(enabled); - } - } - - private static class ApplyToEpisodesDialog extends ConfirmationDialog { - - private final Feed feed; - private final boolean autoDownload; - - ApplyToEpisodesDialog(Context context, Feed feed, boolean autoDownload) { - super(context, R.string.auto_download_apply_to_items_title, - R.string.auto_download_apply_to_items_message); - this.feed = feed; - this.autoDownload = autoDownload; - setPositiveText(R.string.yes); - setNegativeText(R.string.no); - } - - @Override - public void onConfirmButtonPressed(DialogInterface dialog) { - DBWriter.setFeedsItemsAutoDownload(feed, autoDownload); - } - } - } diff --git a/app/src/main/java/de/danoeh/antennapod/activity/FlattrAuthActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/FlattrAuthActivity.java deleted file mode 100644 index 2b4384a02..000000000 --- a/app/src/main/java/de/danoeh/antennapod/activity/FlattrAuthActivity.java +++ /dev/null @@ -1,120 +0,0 @@ -package de.danoeh.antennapod.activity; - - -import android.content.Intent; -import android.net.Uri; -import android.os.Bundle; -import android.support.v7.app.AppCompatActivity; -import android.util.Log; -import android.view.Menu; -import android.view.MenuItem; -import android.view.View; -import android.widget.Button; -import android.widget.TextView; - -import org.shredzone.flattr4j.exception.FlattrException; - -import de.danoeh.antennapod.BuildConfig; -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.core.preferences.UserPreferences; -import de.danoeh.antennapod.core.util.flattr.FlattrUtils; - -/** Guides the user through the authentication process */ - -public class FlattrAuthActivity extends AppCompatActivity { - private static final String TAG = "FlattrAuthActivity"; - - private TextView txtvExplanation; - private Button butAuthenticate; - private Button butReturn; - - private boolean authSuccessful; - - private static FlattrAuthActivity singleton; - - @Override - protected void onCreate(Bundle savedInstanceState) { - setTheme(UserPreferences.getTheme()); - super.onCreate(savedInstanceState); - singleton = this; - authSuccessful = false; - if (BuildConfig.DEBUG) Log.d(TAG, "Activity created"); - getSupportActionBar().setDisplayHomeAsUpEnabled(true); - setContentView(R.layout.flattr_auth); - txtvExplanation = findViewById(R.id.txtvExplanation); - butAuthenticate = findViewById(R.id.but_authenticate); - butReturn = findViewById(R.id.but_return_home); - - butReturn.setOnClickListener(v -> { - Intent intent = new Intent(FlattrAuthActivity.this, MainActivity.class); - intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); - startActivity(intent); - }); - - butAuthenticate.setOnClickListener(v -> { - try { - FlattrUtils.startAuthProcess(FlattrAuthActivity.this); - } catch (FlattrException e) { - e.printStackTrace(); - } - }); - } - - public static FlattrAuthActivity getInstance() { - return singleton; - } - - @Override - protected void onResume() { - super.onResume(); - if (BuildConfig.DEBUG) Log.d(TAG, "Activity resumed"); - Uri uri = getIntent().getData(); - if (uri != null) { - if (BuildConfig.DEBUG) Log.d(TAG, "Received uri"); - FlattrUtils.handleCallback(this, uri); - } - } - - public void handleAuthenticationSuccess() { - authSuccessful = true; - txtvExplanation.setText(R.string.flattr_auth_success); - butAuthenticate.setEnabled(false); - butReturn.setVisibility(View.VISIBLE); - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - super.onCreateOptionsMenu(menu); - return true; - } - - - - @Override - protected void onPause() { - super.onPause(); - if (authSuccessful) { - finish(); - } - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case android.R.id.home: - if (authSuccessful) { - Intent intent = new Intent(this, PreferenceActivity.class); - intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); - startActivity(intent); - } else { - finish(); - } - break; - default: - return false; - } - return true; - } - - -} diff --git a/app/src/main/java/de/danoeh/antennapod/activity/ImportExportActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/ImportExportActivity.java index e6c9c37cc..9795c1240 100644 --- a/app/src/main/java/de/danoeh/antennapod/activity/ImportExportActivity.java +++ b/app/src/main/java/de/danoeh/antennapod/activity/ImportExportActivity.java @@ -23,6 +23,7 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.nio.channels.FileChannel; +import java.util.Arrays; import de.danoeh.antennapod.R; import de.danoeh.antennapod.core.preferences.UserPreferences; @@ -109,9 +110,14 @@ public class ImportExportActivity extends AppCompatActivity { } private void restoreFrom(Uri inputUri) { - File currentDB = getDatabasePath(PodDBAdapter.DATABASE_NAME); InputStream inputStream = null; try { + if (!validateDB(inputUri)) { + displayBadFileDialog(); + return; + } + + File currentDB = getDatabasePath(PodDBAdapter.DATABASE_NAME); inputStream = getContentResolver().openInputStream(inputUri); FileUtils.copyInputStreamToFile(inputStream, currentDB); displayImportSuccessDialog(); @@ -123,6 +129,28 @@ public class ImportExportActivity extends AppCompatActivity { } } + private static final byte[] SQLITE3_MAGIC = "SQLite format 3\0".getBytes(); + private boolean validateDB(Uri inputUri) throws IOException { + try (InputStream inputStream = getContentResolver().openInputStream(inputUri)) { + byte[] magicBuf = new byte[SQLITE3_MAGIC.length]; + if (inputStream.read(magicBuf) == magicBuf.length) { + return Arrays.equals(SQLITE3_MAGIC, magicBuf); + } + } + + return false; + } + + private void displayBadFileDialog() { + AlertDialog.Builder d = new AlertDialog.Builder(ImportExportActivity.this); + d.setMessage(R.string.import_bad_file) + .setCancelable(false) + .setPositiveButton(android.R.string.ok, ((dialogInterface, i) -> { + // do nothing + })) + .show(); + } + private void displayImportSuccessDialog() { AlertDialog.Builder d = new AlertDialog.Builder(ImportExportActivity.this); d.setMessage(R.string.import_ok); 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 9f4fbe271..728019196 100644 --- a/app/src/main/java/de/danoeh/antennapod/activity/MainActivity.java +++ b/app/src/main/java/de/danoeh/antennapod/activity/MainActivity.java @@ -31,6 +31,7 @@ import android.widget.Toast; import com.bumptech.glide.Glide; +import de.danoeh.antennapod.preferences.PreferenceUpgrader; import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.Validate; @@ -43,7 +44,6 @@ import de.danoeh.antennapod.core.dialog.ConfirmationDialog; import de.danoeh.antennapod.core.event.MessageEvent; import de.danoeh.antennapod.core.event.ProgressEvent; import de.danoeh.antennapod.core.event.QueueEvent; -import de.danoeh.antennapod.core.event.ServiceEvent; import de.danoeh.antennapod.core.feed.EventDistributor; import de.danoeh.antennapod.core.feed.Feed; import de.danoeh.antennapod.core.preferences.PlaybackPreferences; @@ -55,7 +55,6 @@ import de.danoeh.antennapod.core.util.FeedItemUtil; import de.danoeh.antennapod.core.util.Flavors; import de.danoeh.antennapod.core.util.IntentUtils; import de.danoeh.antennapod.core.util.StorageUtils; -import de.danoeh.antennapod.core.util.download.AutoUpdateManager; import de.danoeh.antennapod.core.util.gui.NotificationUtils; import de.danoeh.antennapod.dialog.RatingDialog; import de.danoeh.antennapod.dialog.RenameFeedDialog; @@ -68,11 +67,13 @@ import de.danoeh.antennapod.fragment.PlaybackHistoryFragment; import de.danoeh.antennapod.fragment.QueueFragment; import de.danoeh.antennapod.fragment.SubscriptionFragment; import de.danoeh.antennapod.menuhandler.NavDrawerActivity; -import de.greenrobot.event.EventBus; import io.reactivex.Observable; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.Disposable; import io.reactivex.schedulers.Schedulers; +import org.greenrobot.eventbus.EventBus; +import org.greenrobot.eventbus.Subscribe; +import org.greenrobot.eventbus.ThreadMode; /** * The activity that is shown when the user launches the app. @@ -206,8 +207,7 @@ public class MainActivity extends CastEnabledActivity implements NavDrawerActivi transaction.commit(); checkFirstLaunch(); - NotificationUtils.createChannels(this); - UserPreferences.restartUpdateAlarm(false); + PreferenceUpgrader.checkUpgrades(this); } private void saveLastNavFragment(String tag) { @@ -474,7 +474,6 @@ public class MainActivity extends CastEnabledActivity implements NavDrawerActivi protected void onResume() { super.onResume(); StorageUtils.checkStorageAvailability(this); - AutoUpdateManager.checkShouldRefreshFeeds(getApplicationContext()); Intent intent = getIntent(); if (intent.hasExtra(EXTRA_FEED_ID) || @@ -579,17 +578,17 @@ public class MainActivity extends CastEnabledActivity implements NavDrawerActivi } Feed feed = navDrawerData.feeds.get(position - navAdapter.getSubscriptionOffset()); switch(item.getItemId()) { - case R.id.mark_all_seen_item: - ConfirmationDialog markAllSeenConfirmationDialog = new ConfirmationDialog(this, - R.string.mark_all_seen_label, - R.string.mark_all_seen_confirmation_msg) { + case R.id.remove_all_new_flags_item: + ConfirmationDialog removeAllNewFlagsConfirmationDialog = new ConfirmationDialog(this, + R.string.remove_all_new_flags_label, + R.string.remove_all_new_flags_confirmation_msg) { @Override public void onConfirmButtonPressed(DialogInterface dialog) { dialog.dismiss(); - DBWriter.markFeedSeen(feed.getId()); + DBWriter.removeFeedNewFlag(feed.getId()); } }; - markAllSeenConfirmationDialog.createNewDialog().show(); + removeAllNewFlagsConfirmationDialog.createNewDialog().show(); return true; case R.id.mark_all_read_item: ConfirmationDialog markAllReadConfirmationDialog = new ConfirmationDialog(this, @@ -765,6 +764,7 @@ public class MainActivity extends CastEnabledActivity implements NavDrawerActivi }, error -> Log.e(TAG, Log.getStackTraceString(error))); } + @Subscribe public void onEvent(QueueEvent event) { Log.d(TAG, "onEvent(" + event + ")"); // we are only interested in the number of queue items, not download status or position @@ -776,15 +776,7 @@ public class MainActivity extends CastEnabledActivity implements NavDrawerActivi loadData(); } - public void onEventMainThread(ServiceEvent event) { - Log.d(TAG, "onEvent(" + event + ")"); - switch(event.action) { - case SERVICE_STARTED: - externalPlayerFragment.connectToPlaybackService(); - break; - } - } - + @Subscribe(threadMode = ThreadMode.MAIN) public void onEventMainThread(ProgressEvent event) { Log.d(TAG, "onEvent(" + event + ")"); switch(event.action) { @@ -803,6 +795,7 @@ public class MainActivity extends CastEnabledActivity implements NavDrawerActivi } } + @Subscribe(threadMode = ThreadMode.MAIN) public void onEventMainThread(MessageEvent event) { Log.d(TAG, "onEvent(" + event + ")"); View parentLayout = findViewById(R.id.drawer_layout); diff --git a/app/src/main/java/de/danoeh/antennapod/activity/MediaplayerActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/MediaplayerActivity.java index 1cddaa655..3946400a4 100644 --- a/app/src/main/java/de/danoeh/antennapod/activity/MediaplayerActivity.java +++ b/app/src/main/java/de/danoeh/antennapod/activity/MediaplayerActivity.java @@ -38,7 +38,6 @@ import com.joanzapata.iconify.fonts.FontAwesomeIcons; import java.util.Locale; import de.danoeh.antennapod.R; -import de.danoeh.antennapod.core.event.ServiceEvent; import de.danoeh.antennapod.core.feed.FeedItem; import de.danoeh.antennapod.core.feed.FeedMedia; import de.danoeh.antennapod.core.feed.MediaType; @@ -56,6 +55,7 @@ import de.danoeh.antennapod.core.util.IntentUtils; import de.danoeh.antennapod.core.util.ShareUtils; import de.danoeh.antennapod.core.util.StorageUtils; import de.danoeh.antennapod.core.util.Supplier; +import de.danoeh.antennapod.core.util.TimeSpeedConverter; import de.danoeh.antennapod.core.util.gui.PictureInPictureUtil; import de.danoeh.antennapod.core.util.playback.ExternalMedia; import de.danoeh.antennapod.core.util.playback.MediaPlayerError; @@ -79,6 +79,9 @@ public abstract class MediaplayerActivity extends CastEnabledActivity implements private static final String PREFS = "MediaPlayerActivityPreferences"; private static final String PREF_SHOW_TIME_LEFT = "showTimeLeft"; private static final int REQUEST_CODE_STORAGE = 42; + private static final float PLAYBACK_SPEED_STEP = 0.05f; + private static final float DEFAULT_MIN_PLAYBACK_SPEED = 0.5f; + private static final float DEFAULT_MAX_PLAYBACK_SPEED = 2.5f; PlaybackController controller; @@ -235,6 +238,7 @@ public abstract class MediaplayerActivity extends CastEnabledActivity implements StorageUtils.checkStorageAvailability(this); getWindow().setFormat(PixelFormat.TRANSPARENT); + setupGUI(); } @Override @@ -274,11 +278,8 @@ public abstract class MediaplayerActivity extends CastEnabledActivity implements @Override protected void onStart() { super.onStart(); - if (controller != null) { - controller.release(); - } controller = newPlaybackController(); - setupGUI(); + controller.init(); loadMediaInfo(); onPositionObserverUpdate(); } @@ -329,11 +330,6 @@ public abstract class MediaplayerActivity extends CastEnabledActivity implements Playable media = controller.getMedia(); boolean isFeedMedia = media != null && (media instanceof FeedMedia); - menu.findItem(R.id.support_item).setVisible(isFeedMedia && media.getPaymentLink() != null && - ((FeedMedia) media).getItem() != null && - ((FeedMedia) media).getItem().getFlattrStatus().flattrable() - ); - boolean hasWebsiteLink = ( getWebsiteLinkWithFallback(media) != null ); menu.findItem(R.id.visit_website_item).setVisible(hasWebsiteLink); @@ -469,7 +465,7 @@ public abstract class MediaplayerActivity extends CastEnabledActivity implements final Button butDecSpeed = (Button) dialog.findViewById(R.id.butDecSpeed); butDecSpeed.setOnClickListener(v -> { if(controller != null && controller.canSetPlaybackSpeed()) { - barPlaybackSpeed.setProgress(barPlaybackSpeed.getProgress() - 2); + barPlaybackSpeed.setProgress(barPlaybackSpeed.getProgress() - 1); } else { VariableSpeedDialog.showGetPluginDialog(this); } @@ -477,7 +473,7 @@ public abstract class MediaplayerActivity extends CastEnabledActivity implements final Button butIncSpeed = (Button) dialog.findViewById(R.id.butIncSpeed); butIncSpeed.setOnClickListener(v -> { if(controller != null && controller.canSetPlaybackSpeed()) { - barPlaybackSpeed.setProgress(barPlaybackSpeed.getProgress() + 2); + barPlaybackSpeed.setProgress(barPlaybackSpeed.getProgress() + 1); } else { VariableSpeedDialog.showGetPluginDialog(this); } @@ -492,12 +488,20 @@ public abstract class MediaplayerActivity extends CastEnabledActivity implements UserPreferences.setPlaybackSpeed(String.valueOf(currentSpeed)); } + String[] availableSpeeds = UserPreferences.getPlaybackSpeedArray(); + final float minPlaybackSpeed = availableSpeeds.length > 1 ? + Float.valueOf(availableSpeeds[0]) : DEFAULT_MIN_PLAYBACK_SPEED; + float maxPlaybackSpeed = availableSpeeds.length > 1 ? + Float.valueOf(availableSpeeds[availableSpeeds.length - 1]) : DEFAULT_MAX_PLAYBACK_SPEED; + int progressMax = (int) ((maxPlaybackSpeed - minPlaybackSpeed) / PLAYBACK_SPEED_STEP); + barPlaybackSpeed.setMax(progressMax); + txtvPlaybackSpeed.setText(String.format("%.2fx", currentSpeed)); barPlaybackSpeed.setOnSeekBarChangeListener(new OnSeekBarChangeListener() { @Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { if(controller != null && controller.canSetPlaybackSpeed()) { - float playbackSpeed = (progress + 10) / 20.0f; + float playbackSpeed = progress * PLAYBACK_SPEED_STEP + minPlaybackSpeed; controller.setPlaybackSpeed(playbackSpeed); String speedPref = String.format(Locale.US, "%.2f", playbackSpeed); UserPreferences.setPlaybackSpeed(speedPref); @@ -505,7 +509,8 @@ public abstract class MediaplayerActivity extends CastEnabledActivity implements txtvPlaybackSpeed.setText(speedStr); } else if(fromUser) { float speed = Float.valueOf(UserPreferences.getPlaybackSpeed()); - barPlaybackSpeed.post(() -> barPlaybackSpeed.setProgress((int) (20 * speed) - 10)); + barPlaybackSpeed.post(() -> barPlaybackSpeed.setProgress( + (int) ((speed - minPlaybackSpeed) / PLAYBACK_SPEED_STEP))); } } @@ -520,7 +525,7 @@ public abstract class MediaplayerActivity extends CastEnabledActivity implements public void onStopTrackingTouch(SeekBar seekBar) { } }); - barPlaybackSpeed.setProgress((int) (20 * currentSpeed) - 10); + barPlaybackSpeed.setProgress((int) ((currentSpeed - minPlaybackSpeed) / PLAYBACK_SPEED_STEP)); final SeekBar barLeftVolume = (SeekBar) dialog.findViewById(R.id.volume_left); barLeftVolume.setProgress(UserPreferences.getLeftVolumePercentage()); @@ -534,6 +539,22 @@ public abstract class MediaplayerActivity extends CastEnabledActivity implements stereoToMono.setText(stereoToMono.getText() + " [" + sonicOnly + "]"); } + if (UserPreferences.useExoplayer()) { + barRightVolume.setEnabled(false); + } + + final CheckBox skipSilence = (CheckBox) dialog.findViewById(R.id.skipSilence); + skipSilence.setChecked(UserPreferences.isSkipSilence()); + if (!UserPreferences.useExoplayer()) { + skipSilence.setEnabled(false); + String exoplayerOnly = getString(R.string.exoplayer_only); + skipSilence.setText(skipSilence.getText() + " [" + exoplayerOnly + "]"); + } + skipSilence.setOnCheckedChangeListener((buttonView, isChecked) -> { + UserPreferences.setSkipSilence(isChecked); + controller.setSkipSilence(isChecked); + }); + barLeftVolume.setOnSeekBarChangeListener(new OnSeekBarChangeListener() { @Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { @@ -577,11 +598,6 @@ public abstract class MediaplayerActivity extends CastEnabledActivity implements Uri uri = Uri.parse(getWebsiteLinkWithFallback(media)); startActivity(new Intent(Intent.ACTION_VIEW, uri)); break; - case R.id.support_item: - if (media instanceof FeedMedia) { - DBTasks.flattrItemIfLoggedIn(this, ((FeedMedia) media).getItem()); - } - break; case R.id.share_link_item: if (media instanceof FeedMedia) { ShareUtils.shareFeedItemLink(this, ((FeedMedia) media).getItem()); @@ -633,18 +649,6 @@ public abstract class MediaplayerActivity extends CastEnabledActivity implements super.onResume(); Log.d(TAG, "onResume()"); StorageUtils.checkStorageAvailability(this); - if (controller != null) { - controller.init(); - } - } - - public void onEventMainThread(ServiceEvent event) { - Log.d(TAG, "onEvent(" + event + ")"); - if (event.action == ServiceEvent.Action.SERVICE_STARTED) { - if (controller != null) { - controller.init(); - } - } } /** @@ -661,8 +665,11 @@ public abstract class MediaplayerActivity extends CastEnabledActivity implements if (controller == null || txtvPosition == null || txtvLength == null) { return; } - int currentPosition = controller.getPosition(); - int duration = controller.getDuration(); + + int currentPosition = TimeSpeedConverter.convert(controller.getPosition()); + int duration = TimeSpeedConverter.convert(controller.getDuration()); + int remainingTime = TimeSpeedConverter.convert( + controller.getDuration() - controller.getPosition()); Log.d(TAG, "currentPosition " + Converter.getDurationStringLong(currentPosition)); if (currentPosition == PlaybackService.INVALID_TIME || duration == PlaybackService.INVALID_TIME) { @@ -671,7 +678,7 @@ public abstract class MediaplayerActivity extends CastEnabledActivity implements } txtvPosition.setText(Converter.getDurationStringLong(currentPosition)); if (showTimeLeft) { - txtvLength.setText("-" + Converter.getDurationStringLong(duration - currentPosition)); + txtvLength.setText("-" + Converter.getDurationStringLong(remainingTime)); } else { txtvLength.setText(Converter.getDurationStringLong(duration)); } @@ -819,9 +826,13 @@ public abstract class MediaplayerActivity extends CastEnabledActivity implements String length; if (showTimeLeft) { - length = "-" + Converter.getDurationStringLong(media.getDuration() - media.getPosition()); + int remainingTime = TimeSpeedConverter.convert( + media.getDuration() - media.getPosition()); + + length = "-" + Converter.getDurationStringLong(remainingTime); } else { - length = Converter.getDurationStringLong(media.getDuration()); + int duration = TimeSpeedConverter.convert(media.getDuration()); + length = Converter.getDurationStringLong(duration); } txtvLength.setText(length); @@ -924,7 +935,8 @@ public abstract class MediaplayerActivity extends CastEnabledActivity implements prog = controller.onSeekBarProgressChanged(seekBar, progress, fromUser, txtvPosition); if (showTimeLeft && prog != 0) { int duration = controller.getDuration(); - String length = "-" + Converter.getDurationStringLong(duration - (int) (prog * duration)); + int timeLeft = TimeSpeedConverter.convert(duration - (int) (prog * duration)); + String length = "-" + Converter.getDurationStringLong(timeLeft); txtvLength.setText(length); } } diff --git a/app/src/main/java/de/danoeh/antennapod/activity/MediaplayerInfoActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/MediaplayerInfoActivity.java index a2389dabd..4fec1cfc5 100644 --- a/app/src/main/java/de/danoeh/antennapod/activity/MediaplayerInfoActivity.java +++ b/app/src/main/java/de/danoeh/antennapod/activity/MediaplayerInfoActivity.java @@ -7,7 +7,6 @@ import android.content.SharedPreferences; import android.content.res.Configuration; import android.os.Build; import android.os.Bundle; -import android.support.annotation.Nullable; import android.support.design.widget.AppBarLayout; import android.support.design.widget.Snackbar; import android.support.v4.app.Fragment; @@ -34,7 +33,6 @@ import com.viewpagerindicator.CirclePageIndicator; import java.util.List; import de.danoeh.antennapod.R; -import de.danoeh.antennapod.adapter.ChaptersListAdapter; import de.danoeh.antennapod.adapter.NavListAdapter; import de.danoeh.antennapod.core.asynctask.FeedRemover; import de.danoeh.antennapod.core.dialog.ConfirmationDialog; @@ -48,7 +46,6 @@ import de.danoeh.antennapod.core.service.playback.PlayerStatus; import de.danoeh.antennapod.core.storage.DBReader; import de.danoeh.antennapod.core.storage.DBWriter; import de.danoeh.antennapod.core.util.IntentUtils; -import de.danoeh.antennapod.core.util.download.AutoUpdateManager; import de.danoeh.antennapod.core.util.playback.Playable; import de.danoeh.antennapod.core.util.playback.PlaybackController; import de.danoeh.antennapod.dialog.RenameFeedDialog; @@ -62,11 +59,13 @@ import de.danoeh.antennapod.fragment.PlaybackHistoryFragment; import de.danoeh.antennapod.fragment.QueueFragment; import de.danoeh.antennapod.fragment.SubscriptionFragment; import de.danoeh.antennapod.menuhandler.NavDrawerActivity; -import de.greenrobot.event.EventBus; import io.reactivex.Observable; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.Disposable; import io.reactivex.schedulers.Schedulers; +import org.greenrobot.eventbus.EventBus; +import org.greenrobot.eventbus.Subscribe; +import org.greenrobot.eventbus.ThreadMode; /** * Activity for playing files that do not require a video surface. @@ -102,19 +101,12 @@ public abstract class MediaplayerInfoActivity extends MediaplayerActivity implem private ActionBarDrawerToggle drawerToggle; private int mPosition = -1; - private Playable media; private ViewPager pager; private MediaplayerInfoPagerAdapter pagerAdapter; private Disposable disposable; @Override - protected void onPause() { - super.onPause(); - EventBus.getDefault().unregister(this); - } - - @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); supportPostponeEnterTransition(); @@ -124,13 +116,11 @@ public abstract class MediaplayerInfoActivity extends MediaplayerActivity implem protected void onStop() { super.onStop(); Log.d(TAG, "onStop()"); - if(pagerAdapter != null) { - pagerAdapter.setController(null); - } if (disposable != null) { disposable.dispose(); } EventDistributor.getInstance().unregister(contentUpdate); + EventBus.getDefault().unregister(this); saveCurrentFragment(); } @@ -180,15 +170,8 @@ public abstract class MediaplayerInfoActivity extends MediaplayerActivity implem } @Override - protected void onResume() { - super.onResume(); - if(pagerAdapter != null && controller != null && controller.getMedia() != media) { - media = controller.getMedia(); - pagerAdapter.onMediaChanged(media); - pagerAdapter.setController(controller); - } - AutoUpdateManager.checkShouldRefreshFeeds(getApplicationContext()); - + protected void onStart() { + super.onStart(); EventDistributor.getInstance().register(contentUpdate); EventBus.getDefault().register(this); loadData(); @@ -278,8 +261,8 @@ public abstract class MediaplayerInfoActivity extends MediaplayerActivity implem butCastDisconnect = findViewById(R.id.butCastDisconnect); pager = findViewById(R.id.pager); - pagerAdapter = new MediaplayerInfoPagerAdapter(getSupportFragmentManager(), media); - pagerAdapter.setController(controller); + pager.setOffscreenPageLimit(3); + pagerAdapter = new MediaplayerInfoPagerAdapter(getSupportFragmentManager()); pager.setAdapter(pagerAdapter); CirclePageIndicator pageIndicator = findViewById(R.id.page_indicator); pageIndicator.setViewPager(pager); @@ -290,37 +273,6 @@ public abstract class MediaplayerInfoActivity extends MediaplayerActivity implem } @Override - protected void onPositionObserverUpdate() { - super.onPositionObserverUpdate(); - notifyMediaPositionChanged(); - } - - @Override - protected boolean loadMediaInfo() { - if (!super.loadMediaInfo()) { - return false; - } - if(controller != null && controller.getMedia() != media) { - media = controller.getMedia(); - pagerAdapter.onMediaChanged(media); - } - return true; - } - - private void notifyMediaPositionChanged() { - if(pagerAdapter == null) { - return; - } - ChaptersFragment chaptersFragment = pagerAdapter.getChaptersFragment(); - if(chaptersFragment != null) { - ChaptersListAdapter adapter = (ChaptersListAdapter) chaptersFragment.getListAdapter(); - if (adapter != null) { - adapter.notifyDataSetChanged(); - } - } - } - - @Override protected void onReloadNotification(int notificationCode) { if (notificationCode == PlaybackService.EXTRA_CODE_VIDEO) { Log.d(TAG, "ReloadNotification received, switching to Videoplayer now"); @@ -387,8 +339,8 @@ public abstract class MediaplayerInfoActivity extends MediaplayerActivity implem } Feed feed = navDrawerData.feeds.get(position - navAdapter.getSubscriptionOffset()); switch(item.getItemId()) { - case R.id.mark_all_seen_item: - DBWriter.markFeedSeen(feed.getId()); + case R.id.remove_all_new_flags_item: + DBWriter.removeFeedNewFlag(feed.getId()); return true; case R.id.mark_all_read_item: DBWriter.markFeedRead(feed.getId()); @@ -483,6 +435,7 @@ public abstract class MediaplayerInfoActivity extends MediaplayerActivity implem }, error -> Log.e(TAG, Log.getStackTraceString(error))); } + @Subscribe(threadMode = ThreadMode.MAIN) public void onEventMainThread(MessageEvent event) { Log.d(TAG, "onEvent(" + event + ")"); View parentLayout = findViewById(R.id.drawer_layout); @@ -566,50 +519,11 @@ public abstract class MediaplayerInfoActivity extends MediaplayerActivity implem } }; - public interface MediaplayerInfoContentFragment { - void onMediaChanged(Playable media); - } - private static class MediaplayerInfoPagerAdapter extends FragmentStatePagerAdapter { - private static final String TAG = "MPInfoPagerAdapter"; - private Playable media; - private PlaybackController controller; - - public MediaplayerInfoPagerAdapter(FragmentManager fm, Playable media) { + public MediaplayerInfoPagerAdapter(FragmentManager fm) { super(fm); - this.media = media; - } - - private CoverFragment coverFragment; - private ItemDescriptionFragment itemDescriptionFragment; - private ChaptersFragment chaptersFragment; - - public void onMediaChanged(Playable media) { - Log.d(TAG, "media changing to " + ((media != null) ? media.getEpisodeTitle() : "null")); - this.media = media; - if(coverFragment != null) { - coverFragment.onMediaChanged(media); - } - if(itemDescriptionFragment != null) { - itemDescriptionFragment.onMediaChanged(media); - } - if(chaptersFragment != null) { - chaptersFragment.onMediaChanged(media); - } - } - - public void setController(PlaybackController controller) { - this.controller = controller; - if(chaptersFragment != null) { - chaptersFragment.setController(controller); - } - } - - @Nullable - public ChaptersFragment getChaptersFragment() { - return chaptersFragment; } @Override @@ -617,21 +531,11 @@ public abstract class MediaplayerInfoActivity extends MediaplayerActivity implem Log.d(TAG, "getItem(" + position + ")"); switch (position) { case POS_COVER: - if(coverFragment == null) { - coverFragment = CoverFragment.newInstance(media); - } - return coverFragment; + return new CoverFragment(); case POS_DESCR: - if(itemDescriptionFragment == null) { - itemDescriptionFragment = ItemDescriptionFragment.newInstance(media, true, true); - } - return itemDescriptionFragment; + return new ItemDescriptionFragment(); case POS_CHAPTERS: - if(chaptersFragment == null) { - chaptersFragment = ChaptersFragment.newInstance(media); - chaptersFragment.setController(controller); - } - return chaptersFragment; + return new ChaptersFragment(); default: return null; } 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 73da9a834..ea7687bc9 100644 --- a/app/src/main/java/de/danoeh/antennapod/activity/OnlineFeedViewActivity.java +++ b/app/src/main/java/de/danoeh/antennapod/activity/OnlineFeedViewActivity.java @@ -4,7 +4,10 @@ import android.app.Dialog; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; +import android.graphics.LightingColorFilter; +import android.os.Build; import android.os.Bundle; +import android.support.annotation.NonNull; import android.support.annotation.UiThread; import android.support.v4.app.NavUtils; import android.support.v7.app.ActionBar; @@ -27,9 +30,13 @@ import android.widget.Spinner; import android.widget.TextView; import com.bumptech.glide.Glide; - import com.bumptech.glide.request.RequestOptions; + +import de.danoeh.antennapod.core.glide.FastBlurTransformation; import org.apache.commons.lang3.StringUtils; +import org.greenrobot.eventbus.EventBus; +import org.greenrobot.eventbus.Subscribe; +import org.greenrobot.eventbus.ThreadMode; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; @@ -57,15 +64,16 @@ import de.danoeh.antennapod.core.storage.DBReader; import de.danoeh.antennapod.core.storage.DownloadRequestException; import de.danoeh.antennapod.core.storage.DownloadRequester; import de.danoeh.antennapod.core.syndication.handler.FeedHandler; +import de.danoeh.antennapod.core.syndication.handler.FeedHandlerResult; import de.danoeh.antennapod.core.syndication.handler.UnsupportedFeedtypeException; import de.danoeh.antennapod.core.util.DownloadError; import de.danoeh.antennapod.core.util.FileNameGenerator; +import de.danoeh.antennapod.core.util.Optional; import de.danoeh.antennapod.core.util.StorageUtils; import de.danoeh.antennapod.core.util.URLChecker; import de.danoeh.antennapod.core.util.syndication.FeedDiscoverer; import de.danoeh.antennapod.core.util.syndication.HtmlToPlainText; import de.danoeh.antennapod.dialog.AuthenticationDialog; -import de.greenrobot.event.EventBus; import io.reactivex.Observable; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.Disposable; @@ -120,6 +128,7 @@ public class OnlineFeedViewActivity extends AppCompatActivity { } }; + @Subscribe(threadMode = ThreadMode.MAIN) public void onEventMainThread(DownloadEvent event) { Log.d(TAG, "onEventMainThread() called with: " + "event = [" + event + "]"); setSubscribeButtonState(feed); @@ -140,7 +149,7 @@ public class OnlineFeedViewActivity extends AppCompatActivity { StorageUtils.checkStorageAvailability(this); - final String feedUrl; + String feedUrl = null; if (getIntent().hasExtra(ARG_FEEDURL)) { feedUrl = getIntent().getStringExtra(ARG_FEEDURL); } else if (TextUtils.equals(getIntent().getAction(), Intent.ACTION_SEND) @@ -150,16 +159,23 @@ public class OnlineFeedViewActivity extends AppCompatActivity { if (actionBar != null) { actionBar.setTitle(R.string.add_feed_label); } - } else { - throw new IllegalArgumentException("Activity must be started with feedurl argument!"); } - Log.d(TAG, "Activity was started with url " + feedUrl); - setLoadingLayout(); - if (savedInstanceState == null) { - startFeedDownload(feedUrl, null, null); + if (feedUrl == null) { + Log.e(TAG, "feedUrl is null."); + new AlertDialog.Builder(OnlineFeedViewActivity.this). + setNeutralButton(android.R.string.ok, + (dialog, which) -> finish()). + setTitle(R.string.error_label). + setMessage(R.string.null_value_podcast_error).create().show(); } else { - startFeedDownload(feedUrl, savedInstanceState.getString("username"), savedInstanceState.getString("password")); + Log.d(TAG, "Activity was started with url " + feedUrl); + setLoadingLayout(); + if (savedInstanceState == null) { + startFeedDownload(feedUrl, null, null); + } else { + startFeedDownload(feedUrl, savedInstanceState.getString("username"), savedInstanceState.getString("password")); + } } } @@ -183,24 +199,19 @@ public class OnlineFeedViewActivity extends AppCompatActivity { } @Override - protected void onResume() { - super.onResume(); + protected void onStart() { + super.onStart(); isPaused = false; EventDistributor.getInstance().register(listener); EventBus.getDefault().register(this); } @Override - protected void onPause() { - super.onPause(); + protected void onStop() { + super.onStop(); isPaused = true; EventDistributor.getInstance().unregister(listener); EventBus.getDefault().unregister(this); - } - - @Override - protected void onStop() { - super.onStop(); if (downloader != null && !downloader.isFinished()) { downloader.cancel(); } @@ -280,12 +291,7 @@ public class OnlineFeedViewActivity extends AppCompatActivity { error -> Log.e(TAG, Log.getStackTraceString(error))); } - private void checkDownloadResult(DownloadStatus status) { - if (status == null) { - Log.wtf(TAG, "DownloadStatus returned by Downloader was null"); - finish(); - return; - } + private void checkDownloadResult(@NonNull DownloadStatus status) { if (status.isCancelled()) { return; } @@ -312,30 +318,12 @@ public class OnlineFeedViewActivity extends AppCompatActivity { } Log.d(TAG, "Parsing feed"); - parser = Observable.fromCallable(() -> { - FeedHandler handler = new FeedHandler(); - try { - return handler.parseFeed(feed); - } catch (UnsupportedFeedtypeException e) { - Log.d(TAG, "Unsupported feed type detected"); - if ("html".equalsIgnoreCase(e.getRootElement())) { - showFeedDiscoveryDialog(new File(feed.getFile_url()), feed.getDownload_url()); - return null; - } else { - throw e; - } - } catch (Exception e) { - Log.e(TAG, Log.getStackTraceString(e)); - throw e; - } finally { - boolean rc = new File(feed.getFile_url()).delete(); - Log.d(TAG, "Deleted feed source file. Result: " + rc); - } - }) + parser = Observable.fromCallable(this::doParseFeed) .subscribeOn(Schedulers.computation()) .observeOn(AndroidSchedulers.mainThread()) - .subscribe(result -> { - if(result != null) { + .subscribe(optionalResult -> { + if(optionalResult.isPresent()) { + FeedHandlerResult result = optionalResult.get(); beforeShowFeedInformation(result.feed); showFeedInformation(result.feed, result.alternateFeedUrls); } @@ -347,6 +335,33 @@ public class OnlineFeedViewActivity extends AppCompatActivity { }); } + @NonNull + private Optional<FeedHandlerResult> doParseFeed() throws Exception { + FeedHandler handler = new FeedHandler(); + try { + return Optional.of(handler.parseFeed(feed)); + } catch (UnsupportedFeedtypeException e) { + Log.d(TAG, "Unsupported feed type detected"); + if ("html".equalsIgnoreCase(e.getRootElement())) { + boolean dialogShown = showFeedDiscoveryDialog(new File(feed.getFile_url()), feed.getDownload_url()); + if (dialogShown) { + return Optional.empty(); + } else { + Log.d(TAG, "Supplied feed is an HTML web page that has no references to any feed"); + throw e; + } + } else { + throw e; + } + } catch (Exception e) { + Log.e(TAG, Log.getStackTraceString(e)); + throw e; + } finally { + boolean rc = new File(feed.getFile_url()).delete(); + Log.d(TAG, "Deleted feed source file. Result: " + rc); + } + } + /** * Called after the feed has been downloaded and parsed and before showFeedInformation is called. * This method is executed on a background thread @@ -381,6 +396,7 @@ public class OnlineFeedViewActivity extends AppCompatActivity { this.selectedDownloadUrl = feed.getDownload_url(); EventDistributor.getInstance().register(listener); ListView listView = findViewById(R.id.listview); + listView.setSelector(android.R.color.transparent); LayoutInflater inflater = LayoutInflater.from(this); View header = inflater.inflate(R.layout.onlinefeedview_header, listView, false); listView.addHeaderView(header); @@ -388,6 +404,10 @@ public class OnlineFeedViewActivity extends AppCompatActivity { listView.setAdapter(new FeedItemlistDescriptionAdapter(this, 0, feed.getItems())); ImageView cover = header.findViewById(R.id.imgvCover); + ImageView headerBackground = header.findViewById(R.id.imgvBackground); + header.findViewById(R.id.butShowInfo).setVisibility(View.INVISIBLE); + header.findViewById(R.id.butShowSettings).setVisibility(View.INVISIBLE); + headerBackground.setColorFilter(new LightingColorFilter(0xff828282, 0x000000)); TextView title = header.findViewById(R.id.txtvTitle); TextView author = header.findViewById(R.id.txtvAuthor); TextView description = header.findViewById(R.id.txtvDescription); @@ -405,6 +425,15 @@ public class OnlineFeedViewActivity extends AppCompatActivity { .fitCenter() .dontAnimate()) .into(cover); + Glide.with(this) + .load(feed.getImageUrl()) + .apply(new RequestOptions() + .placeholder(R.color.image_readability_tint) + .error(R.color.image_readability_tint) + .diskCacheStrategy(ApGlideSettings.AP_DISK_CACHE_STRATEGY) + .transform(new FastBlurTransformation()) + .dontAnimate()) + .into(headerBackground); } title.setText(feed.getTitle()); @@ -433,6 +462,17 @@ public class OnlineFeedViewActivity extends AppCompatActivity { } }); + final int MAX_LINES_COLLAPSED = 10; + description.setMaxLines(MAX_LINES_COLLAPSED); + description.setOnClickListener(v -> { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN + && description.getMaxLines() > MAX_LINES_COLLAPSED) { + description.setMaxLines(MAX_LINES_COLLAPSED); + } else { + description.setMaxLines(2000); + } + }); + if (alternateFeedUrls.isEmpty()) { spAlternateUrls.setVisibility(View.GONE); } else { @@ -530,21 +570,25 @@ public class OnlineFeedViewActivity extends AppCompatActivity { } } - private void showFeedDiscoveryDialog(File feedFile, String baseUrl) { + /** + * + * @return true if a FeedDiscoveryDialog is shown, false otherwise (e.g., due to no feed found). + */ + private boolean showFeedDiscoveryDialog(File feedFile, String baseUrl) { FeedDiscoverer fd = new FeedDiscoverer(); final Map<String, String> urlsMap; try { urlsMap = fd.findLinks(feedFile, baseUrl); if (urlsMap == null || urlsMap.isEmpty()) { - return; + return false; } } catch (IOException e) { e.printStackTrace(); - return; + return false; } if (isPaused || isFinishing()) { - return; + return false; } final List<String> titles = new ArrayList<>(); @@ -580,6 +624,7 @@ public class OnlineFeedViewActivity extends AppCompatActivity { } dialog = ab.show(); }); + return true; } private class FeedViewAuthenticationDialog extends AuthenticationDialog { 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 a63d3b735..158e3b7a4 100644 --- a/app/src/main/java/de/danoeh/antennapod/activity/OpmlImportFromPathActivity.java +++ b/app/src/main/java/de/danoeh/antennapod/activity/OpmlImportFromPathActivity.java @@ -25,7 +25,6 @@ public class OpmlImportFromPathActivity extends OpmlImportBaseActivity { private static final int CHOOSE_OPML_FILE = 1; - private Intent intentPickAction; private Intent intentGetContentAction; @Override @@ -36,50 +35,30 @@ public class OpmlImportFromPathActivity extends OpmlImportBaseActivity { getSupportActionBar().setDisplayHomeAsUpEnabled(true); setContentView(R.layout.opml_import); - final TextView txtvHeaderExplanation1 = findViewById(R.id.txtvHeadingExplanation1); - final TextView txtvExplanation1 = findViewById(R.id.txtvExplanation1); - final TextView txtvHeaderExplanation2 = findViewById(R.id.txtvHeadingExplanation2); - final TextView txtvExplanation2 = findViewById(R.id.txtvExplanation2); - final TextView txtvHeaderExplanation3 = findViewById(R.id.txtvHeadingExplanation3); + final TextView txtvHeaderExplanation = findViewById(R.id.txtvHeadingExplanation); + final TextView txtvExplanation = findViewById(R.id.txtvExplanation); + final TextView txtvHeaderExplanationOpenWith = findViewById(R.id.txtvHeadingExplanationOpenWith); Button butChooseFilesystem = findViewById(R.id.butChooseFileFromFilesystem); - butChooseFilesystem.setOnClickListener(v -> chooseFileFromFilesystem()); - - Button butChooseExternal = findViewById(R.id.butChooseFileFromExternal); - butChooseExternal.setOnClickListener(v -> chooseFileFromExternal()); + butChooseFilesystem.setOnClickListener(v -> chooseFileFromExternal()); int nextOption = 1; String optionLabel = getString(R.string.opml_import_option); - intentPickAction = new Intent(Intent.ACTION_PICK); - - if(!IntentUtils.isCallable(getApplicationContext(), intentPickAction)) { - intentPickAction.setData(null); - if(!IntentUtils.isCallable(getApplicationContext(), intentPickAction)) { - 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(String.format(optionLabel, nextOption)); - nextOption++; - } - intentGetContentAction = new Intent(Intent.ACTION_GET_CONTENT); intentGetContentAction.addCategory(Intent.CATEGORY_OPENABLE); intentGetContentAction.setType("*/*"); - if(!IntentUtils.isCallable(getApplicationContext(), intentGetContentAction)) { - txtvHeaderExplanation2.setVisibility(View.GONE); - txtvExplanation2.setVisibility(View.GONE); - findViewById(R.id.divider2).setVisibility(View.GONE); - butChooseExternal.setVisibility(View.GONE); - } else { - txtvHeaderExplanation2.setText(String.format(optionLabel, nextOption)); + + if (IntentUtils.isCallable(getApplicationContext(), intentGetContentAction)) { + txtvHeaderExplanation.setText(String.format(optionLabel, nextOption)); nextOption++; + } else { + txtvHeaderExplanation.setVisibility(View.GONE); + txtvExplanation.setVisibility(View.GONE); + findViewById(R.id.divider).setVisibility(View.GONE); + butChooseFilesystem.setVisibility(View.GONE); } - txtvHeaderExplanation3.setText(String.format(optionLabel, nextOption)); + txtvHeaderExplanationOpenWith.setText(String.format(optionLabel, nextOption)); } @Override @@ -106,18 +85,6 @@ public class OpmlImportFromPathActivity extends OpmlImportBaseActivity { } } - /* - * Creates an implicit intent to launch a file manager which lets - * the user choose a specific OPML-file to import from. - */ - private void chooseFileFromFilesystem() { - try { - startActivityForResult(intentPickAction, CHOOSE_OPML_FILE); - } catch (ActivityNotFoundException e) { - Log.e(TAG, "No activity found. Should never happen..."); - } - } - private void chooseFileFromExternal() { try { startActivityForResult(intentGetContentAction, CHOOSE_OPML_FILE); diff --git a/app/src/main/java/de/danoeh/antennapod/activity/PreferenceActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/PreferenceActivity.java index 452e91bd3..7e0ae173f 100644 --- a/app/src/main/java/de/danoeh/antennapod/activity/PreferenceActivity.java +++ b/app/src/main/java/de/danoeh/antennapod/activity/PreferenceActivity.java @@ -1,12 +1,9 @@ package de.danoeh.antennapod.activity; -import android.content.Intent; import android.os.Bundle; import android.support.v7.app.ActionBar; import android.support.v7.app.AppCompatActivity; -import android.support.v7.preference.Preference; import android.support.v7.preference.PreferenceFragmentCompat; -import android.support.v7.preference.PreferenceScreen; import android.view.Menu; import android.view.MenuItem; import android.view.ViewGroup; @@ -15,56 +12,24 @@ import android.widget.FrameLayout; import com.bytehamster.lib.preferencesearch.SearchPreferenceResult; import com.bytehamster.lib.preferencesearch.SearchPreferenceResultListener; -import java.lang.ref.WeakReference; - import de.danoeh.antennapod.R; import de.danoeh.antennapod.core.preferences.UserPreferences; -import de.danoeh.antennapod.preferences.PreferenceController; +import de.danoeh.antennapod.fragment.preferences.AutoDownloadPreferencesFragment; +import de.danoeh.antennapod.fragment.preferences.GpodderPreferencesFragment; +import de.danoeh.antennapod.fragment.preferences.IntegrationsPreferencesFragment; +import de.danoeh.antennapod.fragment.preferences.MainPreferencesFragment; +import de.danoeh.antennapod.fragment.preferences.NetworkPreferencesFragment; +import de.danoeh.antennapod.fragment.preferences.PlaybackPreferencesFragment; +import de.danoeh.antennapod.fragment.preferences.StoragePreferencesFragment; +import de.danoeh.antennapod.fragment.preferences.UserInterfacePreferencesFragment; /** * PreferenceActivity for API 11+. In order to change the behavior of the preference UI, see * PreferenceController. */ public class PreferenceActivity extends AppCompatActivity implements SearchPreferenceResultListener { - - public static final String PARAM_RESOURCE = "resource"; - private static WeakReference<PreferenceActivity> instance; - private PreferenceController preferenceController; - private final PreferenceController.PreferenceUI preferenceUI = new PreferenceController.PreferenceUI() { - private PreferenceFragmentCompat fragment; - - @Override - public void setFragment(PreferenceFragmentCompat fragment) { - this.fragment = fragment; - } - - @Override - public PreferenceFragmentCompat getFragment() { - return fragment; - } - - @Override - public Preference findPreference(CharSequence key) { - return fragment.findPreference(key); - } - - @Override - public PreferenceScreen getPreferenceScreen() { - return fragment.getPreferenceScreen(); - } - - @Override - public AppCompatActivity getActivity() { - return PreferenceActivity.this; - } - }; - @Override protected void onCreate(Bundle savedInstanceState) { - // This must be the FIRST thing we do, otherwise other code may not have the - // reference it needs - instance = new WeakReference<>(this); - setTheme(UserPreferences.getTheme()); super.onCreate(savedInstanceState); @@ -73,38 +38,63 @@ public class PreferenceActivity extends AppCompatActivity implements SearchPrefe ab.setDisplayHomeAsUpEnabled(true); } - // set up layout FrameLayout root = new FrameLayout(this); root.setId(R.id.content); root.setLayoutParams(new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); setContentView(root); - // we need to create the PreferenceController before the MainFragment - // since the MainFragment depends on the preferenceController already being created - preferenceController = new PreferenceController(preferenceUI); + getSupportFragmentManager().beginTransaction().replace(R.id.content, new MainPreferencesFragment()).commit(); - showPreferenceScreen(R.xml.preferences, false); } - private void showPreferenceScreen(int screen, boolean addHistory) { - PreferenceFragmentCompat prefFragment = new MainFragment(); - preferenceUI.setFragment(prefFragment); - Bundle args = new Bundle(); - args.putInt(PARAM_RESOURCE, screen); - prefFragment.setArguments(args); - if (addHistory) { - getSupportFragmentManager().beginTransaction().replace(R.id.content, prefFragment) - .addToBackStack(getString(PreferenceController.getTitleOfPage(screen))).commit(); - } else { - getSupportFragmentManager().beginTransaction().replace(R.id.content, prefFragment).commit(); + private PreferenceFragmentCompat getPreferenceScreen(int screen) { + PreferenceFragmentCompat prefFragment = null; + + if (screen == R.xml.preferences_user_interface) { + prefFragment = new UserInterfacePreferencesFragment(); + } else if (screen == R.xml.preferences_integrations) { + prefFragment = new IntegrationsPreferencesFragment(); + } else if (screen == R.xml.preferences_network) { + prefFragment = new NetworkPreferencesFragment(); + } else if (screen == R.xml.preferences_storage) { + prefFragment = new StoragePreferencesFragment(); + } else if (screen == R.xml.preferences_autodownload) { + prefFragment = new AutoDownloadPreferencesFragment(); + } else if (screen == R.xml.preferences_gpodder) { + prefFragment = new GpodderPreferencesFragment(); + } else if (screen == R.xml.preferences_playback) { + prefFragment = new PlaybackPreferencesFragment(); } + return prefFragment; } - @Override - protected void onActivityResult(int requestCode, int resultCode, Intent data) { - super.onActivityResult(requestCode, resultCode, data); - preferenceController.onActivityResult(requestCode, resultCode, data); + public static int getTitleOfPage(int preferences) { + switch (preferences) { + case R.xml.preferences_network: + return R.string.network_pref; + case R.xml.preferences_autodownload: + return R.string.pref_automatic_download_title; + case R.xml.preferences_playback: + return R.string.playback_pref; + case R.xml.preferences_storage: + return R.string.storage_pref; + case R.xml.preferences_user_interface: + return R.string.user_interface_label; + case R.xml.preferences_integrations: + return R.string.integrations_label; + case R.xml.preferences_gpodder: + return R.string.gpodnet_main_label; + default: + return R.string.settings_label; + } + } + + public PreferenceFragmentCompat openScreen(int screen) { + PreferenceFragmentCompat fragment = getPreferenceScreen(screen); + getSupportFragmentManager().beginTransaction().replace(R.id.content, fragment) + .addToBackStack(getString(getTitleOfPage(screen))).commit(); + return fragment; } @Override @@ -115,72 +105,20 @@ public class PreferenceActivity extends AppCompatActivity implements SearchPrefe @Override public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case android.R.id.home: - if (getSupportFragmentManager().getBackStackEntryCount() == 0) { - finish(); - } else { - getSupportFragmentManager().popBackStack(); - } - return true; - default: - return false; + if (item.getItemId() == android.R.id.home) { + if (getSupportFragmentManager().getBackStackEntryCount() == 0) { + finish(); + } else { + getSupportFragmentManager().popBackStack(); + } + return true; } + return false; } @Override public void onSearchResultClicked(SearchPreferenceResult result) { - showPreferenceScreen(result.getResourceFile(), true); - result.highlight(preferenceUI.getFragment()); - } - - public static class MainFragment extends PreferenceFragmentCompat { - private int screen; - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setRetainInstance(true); - } - - @Override - public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { - screen = getArguments().getInt(PARAM_RESOURCE); - addPreferencesFromResource(screen); - PreferenceActivity activity = instance.get(); - if (activity != null && activity.preferenceController != null) { - activity.preferenceUI.setFragment(this); - activity.preferenceController.onCreate(screen); - } - } - - @Override - public void onResume() { - super.onResume(); - PreferenceActivity activity = instance.get(); - if(activity != null && activity.preferenceController != null) { - activity.setTitle(PreferenceController.getTitleOfPage(screen)); - activity.preferenceUI.setFragment(this); - activity.preferenceController.onResume(screen); - } - } - - @Override - public void onPause() { - PreferenceActivity activity = instance.get(); - if (screen == R.xml.preferences_gpodder) { - activity.preferenceController.unregisterGpodnet(); - } - super.onPause(); - } - - @Override - public void onStop() { - PreferenceActivity activity = instance.get(); - if (screen == R.xml.preferences_storage) { - activity.preferenceController.unsubscribeExportSubscription(); - } - super.onStop(); - } + PreferenceFragmentCompat fragment = openScreen(result.getResourceFile()); + result.highlight(fragment); } } diff --git a/app/src/main/java/de/danoeh/antennapod/activity/gpoddernet/GpodnetAuthenticationActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/gpoddernet/GpodnetAuthenticationActivity.java index 8fcdb4371..2d7898d5b 100644 --- a/app/src/main/java/de/danoeh/antennapod/activity/gpoddernet/GpodnetAuthenticationActivity.java +++ b/app/src/main/java/de/danoeh/antennapod/activity/gpoddernet/GpodnetAuthenticationActivity.java @@ -25,6 +25,8 @@ import android.widget.ViewFlipper; import java.util.ArrayList; import java.util.List; import java.util.concurrent.atomic.AtomicReference; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import de.danoeh.antennapod.BuildConfig; import de.danoeh.antennapod.R; @@ -102,10 +104,6 @@ public class GpodnetAuthenticationActivity extends AppCompatActivity { return super.onOptionsItemSelected(item); } - @Override - public void onConfigurationChanged(Configuration newConfig) { - } - private void setupLoginView(View view) { final EditText username = view.findViewById(R.id.etxtUsername); final EditText password = view.findViewById(R.id.etxtPassword); @@ -123,6 +121,11 @@ public class GpodnetAuthenticationActivity extends AppCompatActivity { final String usernameStr = username.getText().toString(); final String passwordStr = password.getText().toString(); + if (usernameHasUnwantedChars(usernameStr)) { + txtvError.setText(R.string.gpodnetsync_username_characters_error); + txtvError.setVisibility(View.VISIBLE); + return; + } if (BuildConfig.DEBUG) Log.d(TAG, "Checking login credentials"); AsyncTask<GpodnetService, Void, Void> authTask = new AsyncTask<GpodnetService, Void, Void>() { @@ -395,4 +398,10 @@ public class GpodnetAuthenticationActivity extends AppCompatActivity { finish(); } } + + private boolean usernameHasUnwantedChars(String username) { + Pattern special = Pattern.compile("[!@#$%&*()+=|<>?{}\\[\\]~]"); + Matcher containsUnwantedChars = special.matcher(username); + return containsUnwantedChars.find(); + } } diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/ActionButtonCallback.java b/app/src/main/java/de/danoeh/antennapod/adapter/ActionButtonCallback.java deleted file mode 100644 index e6b42efcb..000000000 --- a/app/src/main/java/de/danoeh/antennapod/adapter/ActionButtonCallback.java +++ /dev/null @@ -1,9 +0,0 @@ -package de.danoeh.antennapod.adapter; - -import de.danoeh.antennapod.core.feed.FeedItem; -import de.danoeh.antennapod.core.util.LongList; - -interface ActionButtonCallback { - /** Is called when the action button of a list item has been pressed. */ - void onActionButtonPressed(FeedItem item, LongList queueIds); -} diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/ActionButtonUtils.java b/app/src/main/java/de/danoeh/antennapod/adapter/ActionButtonUtils.java deleted file mode 100644 index a915692d1..000000000 --- a/app/src/main/java/de/danoeh/antennapod/adapter/ActionButtonUtils.java +++ /dev/null @@ -1,97 +0,0 @@ -package de.danoeh.antennapod.adapter; - -import android.content.Context; -import android.content.res.TypedArray; -import android.view.View; -import android.widget.ImageButton; - -import org.apache.commons.lang3.Validate; - -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.core.feed.FeedItem; -import de.danoeh.antennapod.core.feed.FeedMedia; -import de.danoeh.antennapod.core.storage.DownloadRequester; - -/** - * Utility methods for the action button that is displayed on the right hand side - * of a listitem. - */ -class ActionButtonUtils { - - private final int[] labels; - private final TypedArray drawables; - private final Context context; - - public ActionButtonUtils(Context context) { - Validate.notNull(context); - - this.context = context.getApplicationContext(); - drawables = context.obtainStyledAttributes(new int[] { - R.attr.av_play, - R.attr.navigation_cancel, - R.attr.av_download, - R.attr.av_pause, - R.attr.navigation_accept, - R.attr.content_new - }); - labels = new int[] { - R.string.play_label, - R.string.cancel_download_label, - R.string.download_label, - R.string.mark_read_label, - R.string.add_to_queue_label - }; - } - - /** - * Sets the displayed bitmap and content description of the given - * action button so that it matches the state of the FeedItem. - */ - @SuppressWarnings("ResourceType") - public void configureActionButton(ImageButton butSecondary, FeedItem item, boolean isInQueue) { - Validate.isTrue(butSecondary != null && item != null, "butSecondary or item was null"); - - final FeedMedia media = item.getMedia(); - if (media != null) { - final boolean isDownloadingMedia = DownloadRequester.getInstance().isDownloadingFile(media); - if (!media.isDownloaded()) { - if (isDownloadingMedia) { - // item is being downloaded - butSecondary.setVisibility(View.VISIBLE); - butSecondary.setImageDrawable(drawables.getDrawable(1)); - butSecondary.setContentDescription(context.getString(labels[1])); - } else { - // item is not downloaded and not being downloaded - if(DefaultActionButtonCallback.userAllowedMobileDownloads() || - !DefaultActionButtonCallback.userChoseAddToQueue() || isInQueue) { - butSecondary.setVisibility(View.VISIBLE); - butSecondary.setImageDrawable(drawables.getDrawable(2)); - butSecondary.setContentDescription(context.getString(labels[2])); - } else { - // mobile download not allowed yet, item is not in queue and user chose add to queue - butSecondary.setVisibility(View.VISIBLE); - butSecondary.setImageDrawable(drawables.getDrawable(5)); - butSecondary.setContentDescription(context.getString(labels[4])); - } - } - } else { - // item is downloaded - butSecondary.setVisibility(View.VISIBLE); - if (media.isCurrentlyPlaying()) { - butSecondary.setImageDrawable(drawables.getDrawable(3)); - } else { - butSecondary.setImageDrawable(drawables.getDrawable(0)); - } - butSecondary.setContentDescription(context.getString(labels[0])); - } - } else { - if (item.isPlayed()) { - butSecondary.setVisibility(View.INVISIBLE); - } else { - butSecondary.setVisibility(View.VISIBLE); - butSecondary.setImageDrawable(drawables.getDrawable(4)); - butSecondary.setContentDescription(context.getString(labels[3])); - } - } - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/AdapterUtils.java b/app/src/main/java/de/danoeh/antennapod/adapter/AdapterUtils.java index 5c58d00f2..315b3a173 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/AdapterUtils.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/AdapterUtils.java @@ -49,7 +49,7 @@ class AdapterUtils { } else if (!media.isDownloaded()) { if (media.getSize() > 0) { txtvPos.setText(Converter.byteToString(media.getSize())); - } else if(NetworkUtils.isDownloadAllowed() && !media.checkedOnSizeButUnknown()) { + } else if(NetworkUtils.isEpisodeHeadDownloadAllowed() && !media.checkedOnSizeButUnknown()) { txtvPos.setText("{fa-spinner}"); Iconify.addIcons(txtvPos); NetworkUtils.getFeedMediaSizeObservable(media) diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/AllEpisodesRecycleAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/AllEpisodesRecycleAdapter.java index 0b2b81edb..2eff33339 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/AllEpisodesRecycleAdapter.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/AllEpisodesRecycleAdapter.java @@ -26,6 +26,7 @@ import java.lang.ref.WeakReference; import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.MainActivity; +import de.danoeh.antennapod.adapter.actionbutton.ItemActionButton; import de.danoeh.antennapod.core.feed.FeedItem; import de.danoeh.antennapod.core.feed.FeedMedia; import de.danoeh.antennapod.core.storage.DownloadRequester; @@ -46,8 +47,6 @@ public class AllEpisodesRecycleAdapter extends RecyclerView.Adapter<AllEpisodesR private final WeakReference<MainActivity> mainActivityRef; private final ItemAccess itemAccess; - private final ActionButtonCallback actionButtonCallback; - private final ActionButtonUtils actionButtonUtils; private final boolean showOnlyNewEpisodes; private FeedItem selectedItem; @@ -57,13 +56,10 @@ public class AllEpisodesRecycleAdapter extends RecyclerView.Adapter<AllEpisodesR public AllEpisodesRecycleAdapter(MainActivity mainActivity, ItemAccess itemAccess, - ActionButtonCallback actionButtonCallback, boolean showOnlyNewEpisodes) { super(); this.mainActivityRef = new WeakReference<>(mainActivity); this.itemAccess = itemAccess; - this.actionButtonUtils = new ActionButtonUtils(mainActivity); - this.actionButtonCallback = actionButtonCallback; this.showOnlyNewEpisodes = showOnlyNewEpisodes; playingBackGroundColor = ThemeUtils.getColorFromAttr(mainActivity, R.attr.currently_playing_background); @@ -134,7 +130,7 @@ public class AllEpisodesRecycleAdapter extends RecyclerView.Adapter<AllEpisodesR holder.txtvDuration.setText(Converter.getDurationStringLong(media.getDuration())); } else if (media.getSize() > 0) { holder.txtvDuration.setText(Converter.byteToString(media.getSize())); - } else if(NetworkUtils.isDownloadAllowed() && !media.checkedOnSizeButUnknown()) { + } else if(NetworkUtils.isEpisodeHeadDownloadAllowed() && !media.checkedOnSizeButUnknown()) { holder.txtvDuration.setText("{fa-spinner}"); Iconify.addIcons(holder.txtvDuration); NetworkUtils.getFeedMediaSizeObservable(media) @@ -186,10 +182,11 @@ public class AllEpisodesRecycleAdapter extends RecyclerView.Adapter<AllEpisodesR holder.queueStatus.setVisibility(View.INVISIBLE); } - actionButtonUtils.configureActionButton(holder.butSecondary, item, isInQueue); + ItemActionButton actionButton = ItemActionButton.forItem(item, isInQueue); + actionButton.configure(holder.butSecondary, mainActivityRef.get()); + holder.butSecondary.setFocusable(false); holder.butSecondary.setTag(item); - holder.butSecondary.setOnClickListener(secondaryActionListener); new CoverLoader(mainActivityRef.get()) .withUri(item.getImageLocation()) @@ -215,14 +212,6 @@ public class AllEpisodesRecycleAdapter extends RecyclerView.Adapter<AllEpisodesR return itemAccess.getCount(); } - private final View.OnClickListener secondaryActionListener = new View.OnClickListener() { - @Override - public void onClick(View v) { - FeedItem item = (FeedItem) v.getTag(); - actionButtonCallback.onActionButtonPressed(item, itemAccess.getQueueIds()); - } - }; - public class Holder extends RecyclerView.ViewHolder implements View.OnClickListener, View.OnCreateContextMenuListener, @@ -290,7 +279,7 @@ public class AllEpisodesRecycleAdapter extends RecyclerView.Adapter<AllEpisodesR }; FeedItemMenuHandler.onPrepareMenu(contextMenuInterface, item, true, null); - contextMenuInterface.setItemVisibility(R.id.mark_as_seen_item, item.isNew()); + contextMenuInterface.setItemVisibility(R.id.remove_new_flag_item, item.isNew()); } } diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/CoverLoader.java b/app/src/main/java/de/danoeh/antennapod/adapter/CoverLoader.java index 54ecdae77..79dc1f96e 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/CoverLoader.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/CoverLoader.java @@ -66,7 +66,7 @@ public class CoverLoader { options = options.error(errorResource); } - RequestBuilder builder = Glide.with(activity) + RequestBuilder<Drawable> builder = Glide.with(activity) .load(uri) .apply(options); diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/DataFolderAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/DataFolderAdapter.java new file mode 100644 index 000000000..909fd6459 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/adapter/DataFolderAdapter.java @@ -0,0 +1,148 @@ +package de.danoeh.antennapod.adapter; + +import android.app.Dialog; +import android.content.Context; +import android.support.annotation.NonNull; +import android.support.v4.content.ContextCompat; +import android.support.v7.widget.RecyclerView; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.RadioButton; +import android.widget.TextView; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; + +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.core.preferences.UserPreferences; +import de.danoeh.antennapod.core.util.Converter; +import de.danoeh.antennapod.core.util.StorageUtils; +import de.danoeh.antennapod.dialog.ChooseDataFolderDialog; +import me.zhanghai.android.materialprogressbar.MaterialProgressBar; + + +public class DataFolderAdapter extends RecyclerView.Adapter<DataFolderAdapter.ViewHolder> { + private final ChooseDataFolderDialog.RunnableWithString selectionHandler; + private final String currentPath; + private final List<StoragePath> entries; + private final String freeSpaceString; + private Dialog dialog; + + public DataFolderAdapter(Context context, ChooseDataFolderDialog.RunnableWithString selectionHandler) { + this.entries = getStorageEntries(context); + this.currentPath = getCurrentPath(); + this.selectionHandler = selectionHandler; + this.freeSpaceString = context.getString(R.string.choose_data_directory_available_space); + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + LayoutInflater inflater = LayoutInflater.from(parent.getContext()); + View entryView = inflater.inflate(R.layout.choose_data_folder_dialog_entry, parent, false); + return new ViewHolder(entryView); + } + + @Override + public void onBindViewHolder(@NonNull ViewHolder holder, int position) { + StoragePath storagePath = entries.get(position); + String freeSpace = Converter.byteToString(storagePath.getAvailableSpace()); + String totalSpace = Converter.byteToString(storagePath.getTotalSpace()); + + holder.path.setText(storagePath.getShortPath()); + holder.size.setText(String.format(freeSpaceString, freeSpace, totalSpace)); + holder.progressBar.setProgress(storagePath.getUsagePercentage()); + holder.root.setOnClickListener((View v) -> selectAndDismiss(storagePath)); + holder.radioButton.setOnClickListener((View v) -> selectAndDismiss(storagePath)); + if (storagePath.getFullPath().equals(currentPath)) { + holder.radioButton.toggle(); + } + } + + @Override + public int getItemCount() { + if (currentPath == null) { + return 0; + } else { + return entries.size(); + } + } + + public void setDialog(Dialog dialog) { + this.dialog = dialog; + } + + private String getCurrentPath() { + File dataFolder = UserPreferences.getDataFolder(null); + if (dataFolder != null) return dataFolder.getAbsolutePath(); + return null; + } + + private List<StoragePath> getStorageEntries(Context context) { + File[] mediaDirs = ContextCompat.getExternalFilesDirs(context, null); + final List<StoragePath> entries = new ArrayList<>(mediaDirs.length); + for (File dir : mediaDirs) { + if (isNotWritable(dir)) continue; + + entries.add(new StoragePath(dir.getAbsolutePath())); + } + return entries; + } + + private boolean isNotWritable(File dir) { + return dir == null || !dir.exists() || !dir.canRead() || !dir.canWrite(); + } + + private void selectAndDismiss(StoragePath storagePath) { + selectionHandler.run(storagePath.getFullPath()); + dialog.dismiss(); + } + + class ViewHolder extends RecyclerView.ViewHolder { + private View root; + private TextView path; + private TextView size; + private RadioButton radioButton; + private MaterialProgressBar progressBar; + + ViewHolder(View itemView) { + super(itemView); + root = itemView.findViewById(R.id.root); + path = itemView.findViewById(R.id.path); + size = itemView.findViewById(R.id.size); + radioButton = itemView.findViewById(R.id.radio_button); + progressBar = itemView.findViewById(R.id.used_space); + } + } + + class StoragePath { + private final String path; + + StoragePath(String path) { + this.path = path; + } + + String getShortPath() { + int prefixIndex = path.indexOf("Android"); + return (prefixIndex > 0) ? path.substring(0, prefixIndex) : path; + } + + String getFullPath() { + return this.path; + } + + long getAvailableSpace() { + return StorageUtils.getFreeSpaceAvailable(path); + } + + long getTotalSpace() { + return StorageUtils.getTotalSpaceAvailable(path); + } + + int getUsagePercentage() { + return 100 - (int) (100 * getAvailableSpace() / (float) getTotalSpace()); + } + } +}
\ No newline at end of file diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/DefaultActionButtonCallback.java b/app/src/main/java/de/danoeh/antennapod/adapter/DefaultActionButtonCallback.java deleted file mode 100644 index 1286d9dc7..000000000 --- a/app/src/main/java/de/danoeh/antennapod/adapter/DefaultActionButtonCallback.java +++ /dev/null @@ -1,139 +0,0 @@ -package de.danoeh.antennapod.adapter; - -import android.content.Context; -import android.support.annotation.NonNull; -import android.content.Intent; -import android.widget.Toast; - -import com.afollestad.materialdialogs.MaterialDialog; - -import org.apache.commons.lang3.Validate; - -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.preferences.UserPreferences; -import de.danoeh.antennapod.core.service.playback.PlaybackService; -import de.danoeh.antennapod.core.storage.DBReader; -import de.danoeh.antennapod.core.storage.DBTasks; -import de.danoeh.antennapod.core.storage.DBWriter; -import de.danoeh.antennapod.core.storage.DownloadRequestException; -import de.danoeh.antennapod.core.storage.DownloadRequester; -import de.danoeh.antennapod.core.util.IntentUtils; -import de.danoeh.antennapod.core.util.LongList; -import de.danoeh.antennapod.core.util.NetworkUtils; -import de.danoeh.antennapod.core.util.playback.PlaybackServiceStarter; - -/** - * Default implementation of an ActionButtonCallback - */ -public class DefaultActionButtonCallback implements ActionButtonCallback { - - private static final String TAG = "DefaultActionButtonCallback"; - - private final Context context; - - private static final int TEN_MINUTES_IN_MILLIS = 60 * 1000 * 10; - - // remember timestamp when user allowed downloading via mobile connection - private static long allowMobileDownloadsTimestamp; - private static long onlyAddToQueueTimeStamp; - - public DefaultActionButtonCallback(Context context) { - Validate.notNull(context); - this.context = context; - } - - public static boolean userAllowedMobileDownloads() { - return System.currentTimeMillis() - allowMobileDownloadsTimestamp < TEN_MINUTES_IN_MILLIS; - } - - public static boolean userChoseAddToQueue() { - return System.currentTimeMillis() - onlyAddToQueueTimeStamp < TEN_MINUTES_IN_MILLIS; - } - - @Override - public void onActionButtonPressed(final FeedItem item, final LongList queueIds) { - - if (item.hasMedia()) { - final FeedMedia media = item.getMedia(); - boolean isDownloading = DownloadRequester.getInstance().isDownloadingFile(media); - if (!isDownloading && !media.isDownloaded()) { - if (NetworkUtils.isDownloadAllowed() || userAllowedMobileDownloads()) { - try { - DBTasks.downloadFeedItems(context, item); - Toast.makeText(context, R.string.status_downloading_label, Toast.LENGTH_SHORT).show(); - } catch (DownloadRequestException e) { - e.printStackTrace(); - DownloadRequestErrorDialogCreator.newRequestErrorDialog(context, e.getMessage()); - } - } else if(userChoseAddToQueue() && !queueIds.contains(item.getId())) { - DBWriter.addQueueItem(context, item); - Toast.makeText(context, R.string.added_to_queue_label, Toast.LENGTH_SHORT).show(); - } else { - confirmMobileDownload(context, item); - } - } else if (isDownloading) { - DownloadRequester.getInstance().cancelDownload(context, media); - if(UserPreferences.isEnableAutodownload()) { - DBWriter.setFeedItemAutoDownload(media.getItem(), false); - Toast.makeText(context, R.string.download_canceled_autodownload_enabled_msg, Toast.LENGTH_LONG).show(); - } else { - Toast.makeText(context, R.string.download_canceled_msg, Toast.LENGTH_LONG).show(); - } - } else { // media is downloaded - if (media.isCurrentlyPlaying()) { - new PlaybackServiceStarter(context, media) - .startWhenPrepared(true) - .shouldStream(false) - .start(); - IntentUtils.sendLocalBroadcast(context, PlaybackService.ACTION_PAUSE_PLAY_CURRENT_EPISODE); - } else if (media.isCurrentlyPaused()) { - new PlaybackServiceStarter(context, media) - .startWhenPrepared(true) - .shouldStream(false) - .start(); - IntentUtils.sendLocalBroadcast(context, PlaybackService.ACTION_RESUME_PLAY_CURRENT_EPISODE); - } else { - DBTasks.playMedia(context, media, false, true, false); - } - } - } else { - if (!item.isPlayed()) { - DBWriter.markItemPlayed(item, FeedItem.PLAYED, true); - } - } - } - - private void confirmMobileDownload(final Context context, final FeedItem item) { - MaterialDialog.Builder builder = new MaterialDialog.Builder(context); - builder - .title(R.string.confirm_mobile_download_dialog_title) - .content(R.string.confirm_mobile_download_dialog_message) - .positiveText(context.getText(R.string.confirm_mobile_download_dialog_enable_temporarily)) - .onPositive((dialog, which) -> { - allowMobileDownloadsTimestamp = System.currentTimeMillis(); - try { - DBTasks.downloadFeedItems(context, item); - Toast.makeText(context, R.string.status_downloading_label, Toast.LENGTH_SHORT).show(); - } catch (DownloadRequestException e) { - e.printStackTrace(); - DownloadRequestErrorDialogCreator.newRequestErrorDialog(context, e.getMessage()); - } - }); - LongList queueIds = DBReader.getQueueIDList(); - if(!queueIds.contains(item.getId())) { - builder - .content(R.string.confirm_mobile_download_dialog_message_not_in_queue) - .neutralText(R.string.confirm_mobile_download_dialog_only_add_to_queue) - .onNeutral((dialog, which) -> { - onlyAddToQueueTimeStamp = System.currentTimeMillis(); - DBWriter.addQueueItem(context, item); - Toast.makeText(context, R.string.added_to_queue_label, Toast.LENGTH_SHORT).show(); - }); - } - builder.show(); - } - -} diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/FeedDiscoverAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/FeedDiscoverAdapter.java new file mode 100644 index 000000000..df7ec46e0 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/adapter/FeedDiscoverAdapter.java @@ -0,0 +1,76 @@ +package de.danoeh.antennapod.adapter; + +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.ImageView; +import com.bumptech.glide.Glide; +import com.bumptech.glide.request.RequestOptions; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.activity.MainActivity; +import de.danoeh.antennapod.discovery.PodcastSearchResult; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.List; + +public class FeedDiscoverAdapter extends BaseAdapter { + + private final WeakReference<MainActivity> mainActivityRef; + private final List<PodcastSearchResult> data = new ArrayList<>(); + + public FeedDiscoverAdapter(MainActivity mainActivity) { + this.mainActivityRef = new WeakReference<>(mainActivity); + } + + public void updateData(List<PodcastSearchResult> newData) { + data.clear(); + data.addAll(newData); + notifyDataSetChanged(); + } + + @Override + public int getCount() { + return data.size(); + } + + @Override + public PodcastSearchResult getItem(int position) { + return data.get(position); + } + + @Override + public long getItemId(int position) { + return 0; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + Holder holder; + + if (convertView == null) { + convertView = View.inflate(mainActivityRef.get(), R.layout.quick_feed_discovery_item, null); + holder = new Holder(); + holder.imageView = convertView.findViewById(R.id.discovery_cover); + convertView.setTag(holder); + } else { + holder = (Holder) convertView.getTag(); + } + + + final PodcastSearchResult podcast = getItem(position); + Glide.with(mainActivityRef.get()) + .load(podcast.imageUrl) + .apply(new RequestOptions() + .placeholder(R.color.light_gray) + .fitCenter() + .dontAnimate()) + .into(holder.imageView); + + return convertView; + } + + static class Holder { + ImageView imageView; + } +} 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 738a0a636..a365b1b2e 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/FeedItemlistAdapter.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/FeedItemlistAdapter.java @@ -7,7 +7,6 @@ import android.support.v4.content.ContextCompat; import android.text.Layout; import android.view.LayoutInflater; import android.view.View; -import android.view.View.OnClickListener; import android.view.ViewGroup; import android.widget.Adapter; import android.widget.BaseAdapter; @@ -18,6 +17,7 @@ import android.widget.ProgressBar; import android.widget.TextView; import de.danoeh.antennapod.R; +import de.danoeh.antennapod.adapter.actionbutton.ItemActionButton; import de.danoeh.antennapod.core.feed.FeedItem; import de.danoeh.antennapod.core.feed.FeedMedia; import de.danoeh.antennapod.core.feed.MediaType; @@ -31,14 +31,12 @@ import de.danoeh.antennapod.core.util.ThemeUtils; */ public class FeedItemlistAdapter extends BaseAdapter { - private final ActionButtonCallback callback; 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 final ActionButtonUtils actionButtonUtils; private static final int SELECTION_NONE = -1; @@ -47,16 +45,13 @@ public class FeedItemlistAdapter extends BaseAdapter { public FeedItemlistAdapter(Context context, ItemAccess itemAccess, - ActionButtonCallback callback, boolean showFeedtitle, boolean makePlayedItemsTransparent) { super(); - this.callback = callback; this.context = context; this.itemAccess = itemAccess; this.showFeedtitle = showFeedtitle; this.selectedItemIndex = SELECTION_NONE; - this.actionButtonUtils = new ActionButtonUtils(context); this.makePlayedItemsTransparent = makePlayedItemsTransparent; playingBackGroundColor = ThemeUtils.getColorFromAttr(context, R.attr.currently_playing_background); @@ -199,10 +194,11 @@ public class FeedItemlistAdapter extends BaseAdapter { } } - actionButtonUtils.configureActionButton(holder.butAction, item, isInQueue); + ItemActionButton actionButton = ItemActionButton.forItem(item, isInQueue); + actionButton.configure(holder.butAction, context); + holder.butAction.setFocusable(false); holder.butAction.setTag(item); - holder.butAction.setOnClickListener(butActionListener); } else { convertView.setVisibility(View.GONE); @@ -210,14 +206,6 @@ public class FeedItemlistAdapter extends BaseAdapter { return convertView; } - private final OnClickListener butActionListener = new OnClickListener() { - @Override - public void onClick(View v) { - FeedItem item = (FeedItem) v.getTag(); - callback.onActionButtonPressed(item, itemAccess.getQueueIds()); - } - }; - static class Holder { LinearLayout container; TextView title; 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 fbf6b804a..be8e52cfc 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/NavListAdapter.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/NavListAdapter.java @@ -8,6 +8,7 @@ import android.graphics.Typeface; import android.graphics.drawable.Drawable; import android.preference.PreferenceManager; import android.support.v7.app.AlertDialog; +import android.util.TypedValue; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -214,10 +215,17 @@ public class NavListAdapter extends BaseAdapter } if (v != null && viewType != VIEW_TYPE_SECTION_DIVIDER) { TextView txtvTitle = v.findViewById(R.id.txtvTitle); + TypedValue typedValue = new TypedValue(); + if (position == itemAccess.getSelectedItemIndex()) { txtvTitle.setTypeface(null, Typeface.BOLD); + v.getContext().getTheme().resolveAttribute(de.danoeh.antennapod.core.R.attr.drawer_activated_color, typedValue, true); + v.setBackgroundResource(typedValue.resourceId); + } else { txtvTitle.setTypeface(null, Typeface.NORMAL); + v.getContext().getTheme().resolveAttribute(de.danoeh.antennapod.core.R.attr.nav_drawer_background, typedValue, true); + v.setBackgroundResource(typedValue.resourceId); } } return v; diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/QueueRecyclerAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/QueueRecyclerAdapter.java index df8cafb9d..382abfb32 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/QueueRecyclerAdapter.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/QueueRecyclerAdapter.java @@ -1,5 +1,6 @@ package de.danoeh.antennapod.adapter; +import android.content.Context; import android.os.Build; import android.support.annotation.Nullable; import android.support.v4.content.ContextCompat; @@ -22,8 +23,6 @@ import android.widget.ImageView; import android.widget.ProgressBar; import android.widget.TextView; -import com.bumptech.glide.Glide; -import com.bumptech.glide.request.RequestOptions; import com.joanzapata.iconify.Iconify; import org.apache.commons.lang3.ArrayUtils; @@ -32,9 +31,9 @@ import java.lang.ref.WeakReference; import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.MainActivity; +import de.danoeh.antennapod.adapter.actionbutton.ItemActionButton; import de.danoeh.antennapod.core.feed.FeedItem; import de.danoeh.antennapod.core.feed.FeedMedia; -import de.danoeh.antennapod.core.glide.ApGlideSettings; import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.storage.DownloadRequester; import de.danoeh.antennapod.core.util.Converter; @@ -54,8 +53,6 @@ public class QueueRecyclerAdapter extends RecyclerView.Adapter<QueueRecyclerAdap private final WeakReference<MainActivity> mainActivity; private final ItemAccess itemAccess; - private final ActionButtonCallback actionButtonCallback; - private final ActionButtonUtils actionButtonUtils; private final ItemTouchHelper itemTouchHelper; private boolean locked; @@ -67,13 +64,10 @@ public class QueueRecyclerAdapter extends RecyclerView.Adapter<QueueRecyclerAdap public QueueRecyclerAdapter(MainActivity mainActivity, ItemAccess itemAccess, - ActionButtonCallback actionButtonCallback, ItemTouchHelper itemTouchHelper) { super(); this.mainActivity = new WeakReference<>(mainActivity); this.itemAccess = itemAccess; - this.actionButtonUtils = new ActionButtonUtils(mainActivity); - this.actionButtonCallback = actionButtonCallback; this.itemTouchHelper = itemTouchHelper; locked = UserPreferences.isQueueLocked(); @@ -258,7 +252,7 @@ public class QueueRecyclerAdapter extends RecyclerView.Adapter<QueueRecyclerAdap } else { if(media.getSize() > 0) { progressLeft.setText(Converter.byteToString(media.getSize())); - } else if(NetworkUtils.isDownloadAllowed() && !media.checkedOnSizeButUnknown()) { + } else if(NetworkUtils.isEpisodeHeadDownloadAllowed() && !media.checkedOnSizeButUnknown()) { progressLeft.setText("{fa-spinner}"); Iconify.addIcons(progressLeft); NetworkUtils.getFeedMediaSizeObservable(media) @@ -287,10 +281,11 @@ public class QueueRecyclerAdapter extends RecyclerView.Adapter<QueueRecyclerAdap } } - actionButtonUtils.configureActionButton(butSecondary, item, true); + ItemActionButton actionButton = ItemActionButton.forItem(item, true); + actionButton.configure(butSecondary, mainActivity.get()); + butSecondary.setFocusable(false); butSecondary.setTag(item); - butSecondary.setOnClickListener(secondaryActionListener); new CoverLoader(mainActivity.get()) .withUri(item.getImageLocation()) @@ -302,15 +297,6 @@ public class QueueRecyclerAdapter extends RecyclerView.Adapter<QueueRecyclerAdap } - private final View.OnClickListener secondaryActionListener = new View.OnClickListener() { - @Override - public void onClick(View v) { - FeedItem item = (FeedItem) v.getTag(); - actionButtonCallback.onActionButtonPressed(item, itemAccess.getQueueIds()); - } - }; - - public interface ItemAccess { FeedItem getItem(int position); int getCount(); diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/AddToQueueActionButton.java b/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/AddToQueueActionButton.java new file mode 100644 index 000000000..3299db3ab --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/AddToQueueActionButton.java @@ -0,0 +1,32 @@ +package de.danoeh.antennapod.adapter.actionbutton; + +import android.content.Context; +import android.support.annotation.AttrRes; +import android.support.annotation.StringRes; + +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.core.feed.FeedItem; + +class AddToQueueActionButton extends ItemActionButton { + + AddToQueueActionButton(FeedItem item) { + super(item); + } + + @Override + @StringRes + public int getLabel() { + return R.string.add_to_queue_label; + } + + @Override + @AttrRes + public int getDrawable() { + return R.attr.content_new; + } + + @Override + public void onClick(Context context) { + MobileDownloadHelper.confirmMobileDownload(context, item); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/CancelDownloadActionButton.java b/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/CancelDownloadActionButton.java new file mode 100644 index 000000000..1275a799b --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/CancelDownloadActionButton.java @@ -0,0 +1,44 @@ +package de.danoeh.antennapod.adapter.actionbutton; + +import android.content.Context; +import android.support.annotation.AttrRes; +import android.support.annotation.StringRes; +import android.widget.Toast; + +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.core.feed.FeedMedia; +import de.danoeh.antennapod.core.preferences.UserPreferences; +import de.danoeh.antennapod.core.storage.DBWriter; +import de.danoeh.antennapod.core.storage.DownloadRequester; + +class CancelDownloadActionButton extends ItemActionButton { + + CancelDownloadActionButton(FeedItem item) { + super(item); + } + + @Override + @StringRes + public int getLabel() { + return R.string.cancel_download_label; + } + + @Override + @AttrRes + public int getDrawable() { + return R.attr.navigation_cancel; + } + + @Override + public void onClick(Context context) { + FeedMedia media = item.getMedia(); + DownloadRequester.getInstance().cancelDownload(context, media); + if (UserPreferences.isEnableAutodownload()) { + DBWriter.setFeedItemAutoDownload(media.getItem(), false); + Toast.makeText(context, R.string.download_canceled_autodownload_enabled_msg, Toast.LENGTH_LONG).show(); + } else { + Toast.makeText(context, R.string.download_canceled_msg, Toast.LENGTH_LONG).show(); + } + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/DownloadActionButton.java b/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/DownloadActionButton.java new file mode 100644 index 000000000..c1559528e --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/DownloadActionButton.java @@ -0,0 +1,74 @@ +package de.danoeh.antennapod.adapter.actionbutton; + +import android.content.Context; +import android.support.annotation.AttrRes; +import android.support.annotation.NonNull; +import android.support.annotation.StringRes; +import android.widget.Toast; + +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.storage.DBTasks; +import de.danoeh.antennapod.core.storage.DBWriter; +import de.danoeh.antennapod.core.storage.DownloadRequestException; +import de.danoeh.antennapod.core.storage.DownloadRequester; +import de.danoeh.antennapod.core.util.NetworkUtils; + +class DownloadActionButton extends ItemActionButton { + private boolean isInQueue; + + DownloadActionButton(FeedItem item, boolean isInQueue) { + super(item); + this.isInQueue = isInQueue; + } + + @Override + @StringRes + public int getLabel() { + return R.string.download_label; + } + + @Override + @AttrRes + public int getDrawable() { + return R.attr.av_download; + } + + @Override + public void onClick(Context context) { + final FeedMedia media = item.getMedia(); + if (media == null || shouldNotDownload(media)) { + return; + } + + if (NetworkUtils.isEpisodeDownloadAllowed() || MobileDownloadHelper.userAllowedMobileDownloads()) { + downloadEpisode(context); + } else if (MobileDownloadHelper.userChoseAddToQueue() && !isInQueue) { + addEpisodeToQueue(context); + } else { + MobileDownloadHelper.confirmMobileDownload(context, item); + } + } + + private boolean shouldNotDownload(@NonNull FeedMedia media) { + boolean isDownloading = DownloadRequester.getInstance().isDownloadingFile(media); + return isDownloading || media.isDownloaded(); + } + + private void addEpisodeToQueue(Context context) { + DBWriter.addQueueItem(context, item); + Toast.makeText(context, R.string.added_to_queue_label, Toast.LENGTH_SHORT).show(); + } + + private void downloadEpisode(Context context) { + try { + DBTasks.downloadFeedItems(context, item); + Toast.makeText(context, R.string.status_downloading_label, Toast.LENGTH_SHORT).show(); + } catch (DownloadRequestException e) { + e.printStackTrace(); + DownloadRequestErrorDialogCreator.newRequestErrorDialog(context, e.getMessage()); + } + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/ItemActionButton.java b/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/ItemActionButton.java new file mode 100644 index 000000000..da5ebf6e1 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/ItemActionButton.java @@ -0,0 +1,63 @@ +package de.danoeh.antennapod.adapter.actionbutton; + +import android.content.Context; +import android.content.res.TypedArray; +import android.support.annotation.AttrRes; +import android.support.annotation.NonNull; +import android.support.annotation.StringRes; +import android.view.View; +import android.widget.ImageButton; + +import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.core.feed.FeedMedia; +import de.danoeh.antennapod.core.storage.DownloadRequester; + +public abstract class ItemActionButton { + FeedItem item; + + ItemActionButton(FeedItem item) { + this.item = item; + } + + @StringRes + abstract public int getLabel(); + + @AttrRes + abstract public int getDrawable(); + + abstract public void onClick(Context context); + + public int getVisibility() { + return View.VISIBLE; + } + + @NonNull + public static ItemActionButton forItem(@NonNull FeedItem item, boolean isInQueue) { + final FeedMedia media = item.getMedia(); + if (media == null) { + return new MarkAsPlayedActionButton(item); + } + + final boolean isDownloadingMedia = DownloadRequester.getInstance().isDownloadingFile(media); + if (media.isDownloaded()) { + return new PlayActionButton(item); + } else if (isDownloadingMedia) { + return new CancelDownloadActionButton(item); + } else if (MobileDownloadHelper.userAllowedMobileDownloads() || !MobileDownloadHelper.userChoseAddToQueue() || isInQueue) { + return new DownloadActionButton(item, isInQueue); + } else { + return new AddToQueueActionButton(item); + } + } + + public void configure(@NonNull ImageButton button, Context context) { + TypedArray drawables = context.obtainStyledAttributes(new int[]{getDrawable()}); + + button.setVisibility(getVisibility()); + button.setContentDescription(context.getString(getLabel())); + button.setImageDrawable(drawables.getDrawable(0)); + button.setOnClickListener((view) -> onClick(context)); + + drawables.recycle(); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/MarkAsPlayedActionButton.java b/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/MarkAsPlayedActionButton.java new file mode 100644 index 000000000..4d906cee5 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/MarkAsPlayedActionButton.java @@ -0,0 +1,41 @@ +package de.danoeh.antennapod.adapter.actionbutton; + +import android.content.Context; +import android.support.annotation.AttrRes; +import android.support.annotation.StringRes; +import android.view.View; + +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.core.storage.DBWriter; + +class MarkAsPlayedActionButton extends ItemActionButton { + + MarkAsPlayedActionButton(FeedItem item) { + super(item); + } + + @Override + @StringRes + public int getLabel() { + return R.string.mark_read_label; + } + + @Override + @AttrRes + public int getDrawable() { + return R.attr.navigation_accept; + } + + @Override + public void onClick(Context context) { + if (!item.isPlayed()) { + DBWriter.markItemPlayed(item, FeedItem.PLAYED, true); + } + } + + @Override + public int getVisibility() { + return (item.isPlayed()) ? View.INVISIBLE : View.VISIBLE; + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/MobileDownloadHelper.java b/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/MobileDownloadHelper.java new file mode 100644 index 000000000..f8d2a139e --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/MobileDownloadHelper.java @@ -0,0 +1,60 @@ +package de.danoeh.antennapod.adapter.actionbutton; + +import android.content.Context; +import android.widget.Toast; + +import com.afollestad.materialdialogs.MaterialDialog; + +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.core.dialog.DownloadRequestErrorDialogCreator; +import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.core.storage.DBReader; +import de.danoeh.antennapod.core.storage.DBTasks; +import de.danoeh.antennapod.core.storage.DBWriter; +import de.danoeh.antennapod.core.storage.DownloadRequestException; + +class MobileDownloadHelper { + private static long addToQueueTimestamp; + private static long allowMobileDownloadTimestamp; + private static final int TEN_MINUTES_IN_MILLIS = 10 * 60 * 1000; + + static boolean userChoseAddToQueue() { + return System.currentTimeMillis() - addToQueueTimestamp < TEN_MINUTES_IN_MILLIS; + } + + static boolean userAllowedMobileDownloads() { + return System.currentTimeMillis() - allowMobileDownloadTimestamp < TEN_MINUTES_IN_MILLIS; + } + + static void confirmMobileDownload(final Context context, final FeedItem item) { + MaterialDialog.Builder builder = new MaterialDialog.Builder(context) + .title(R.string.confirm_mobile_download_dialog_title) + .content(R.string.confirm_mobile_download_dialog_message) + .positiveText(context.getText(R.string.confirm_mobile_download_dialog_enable_temporarily)) + .onPositive((dialog, which) -> downloadFeedItems(context, item)); + if (!DBReader.getQueueIDList().contains(item.getId())) { + builder + .content(R.string.confirm_mobile_download_dialog_message_not_in_queue) + .neutralText(R.string.confirm_mobile_download_dialog_only_add_to_queue) + .onNeutral((dialog, which) -> addToQueue(context, item)); + } + builder.show(); + } + + private static void addToQueue(Context context, FeedItem item) { + addToQueueTimestamp = System.currentTimeMillis(); + DBWriter.addQueueItem(context, item); + Toast.makeText(context, R.string.added_to_queue_label, Toast.LENGTH_SHORT).show(); + } + + private static void downloadFeedItems(Context context, FeedItem item) { + allowMobileDownloadTimestamp = System.currentTimeMillis(); + try { + DBTasks.downloadFeedItems(context, item); + Toast.makeText(context, R.string.status_downloading_label, Toast.LENGTH_SHORT).show(); + } catch (DownloadRequestException e) { + e.printStackTrace(); + DownloadRequestErrorDialogCreator.newRequestErrorDialog(context, e.getMessage()); + } + } +}
\ No newline at end of file diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/PlayActionButton.java b/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/PlayActionButton.java new file mode 100644 index 000000000..3992c7240 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/PlayActionButton.java @@ -0,0 +1,63 @@ +package de.danoeh.antennapod.adapter.actionbutton; + +import android.content.Context; +import android.support.annotation.AttrRes; +import android.support.annotation.StringRes; + +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.core.feed.FeedMedia; +import de.danoeh.antennapod.core.storage.DBTasks; +import de.danoeh.antennapod.core.util.IntentUtils; +import de.danoeh.antennapod.core.util.playback.PlaybackServiceStarter; + +import static de.danoeh.antennapod.core.service.playback.PlaybackService.ACTION_PAUSE_PLAY_CURRENT_EPISODE; +import static de.danoeh.antennapod.core.service.playback.PlaybackService.ACTION_RESUME_PLAY_CURRENT_EPISODE; + +class PlayActionButton extends ItemActionButton { + + PlayActionButton(FeedItem item) { + super(item); + } + + @Override + @StringRes + public int getLabel() { + return R.string.play_label; + } + + @Override + @AttrRes + public int getDrawable() { + FeedMedia media = item.getMedia(); + if (media != null && media.isCurrentlyPlaying()) { + return R.attr.av_pause; + } else { + return R.attr.av_play; + } + } + + @Override + public void onClick(Context context) { + FeedMedia media = item.getMedia(); + if (media == null) { + return; + } + + if (media.isPlaying()) { + togglePlayPause(context, media); + } else { + DBTasks.playMedia(context, media, false, true, false); + } + } + + private void togglePlayPause(Context context, FeedMedia media) { + new PlaybackServiceStarter(context, media) + .startWhenPrepared(true) + .shouldStream(false) + .start(); + + String pauseOrResume = media.isCurrentlyPlaying() ? ACTION_PAUSE_PLAY_CURRENT_EPISODE : ACTION_RESUME_PLAY_CURRENT_EPISODE; + IntentUtils.sendLocalBroadcast(context, pauseOrResume); + } +} 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 index 2cf17c85f..f5213e4ab 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/itunes/ItunesAdapter.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/itunes/ItunesAdapter.java @@ -2,7 +2,6 @@ package de.danoeh.antennapod.adapter.itunes; import android.content.Context; import android.support.annotation.NonNull; -import android.support.annotation.Nullable; import android.view.View; import android.view.ViewGroup; import android.widget.ArrayAdapter; @@ -13,18 +12,14 @@ import com.bumptech.glide.Glide; import com.bumptech.glide.load.engine.DiskCacheStrategy; import com.bumptech.glide.request.RequestOptions; -import de.danoeh.antennapod.core.glide.ApGlideSettings; -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; +import de.danoeh.antennapod.discovery.PodcastSearchResult; import java.util.List; import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.MainActivity; -import de.mfietz.fyydlin.SearchHit; -public class ItunesAdapter extends ArrayAdapter<ItunesAdapter.Podcast> { +public class ItunesAdapter extends ArrayAdapter<PodcastSearchResult> { /** * Related Context */ @@ -33,7 +28,7 @@ public class ItunesAdapter extends ArrayAdapter<ItunesAdapter.Podcast> { /** * List holding the podcasts found in the search */ - private final List<Podcast> data; + private final List<PodcastSearchResult> data; /** * Constructor. @@ -41,7 +36,7 @@ public class ItunesAdapter extends ArrayAdapter<ItunesAdapter.Podcast> { * @param context Related context * @param objects Search result */ - public ItunesAdapter(Context context, List<Podcast> objects) { + public ItunesAdapter(Context context, List<PodcastSearchResult> objects) { super(context, 0, objects); this.data = objects; this.context = context; @@ -51,7 +46,7 @@ public class ItunesAdapter extends ArrayAdapter<ItunesAdapter.Podcast> { @Override public View getView(int position, View convertView, @NonNull ViewGroup parent) { //Current podcast - Podcast podcast = data.get(position); + PodcastSearchResult podcast = data.get(position); //ViewHolder PodcastViewHolder viewHolder; @@ -94,75 +89,6 @@ public class ItunesAdapter extends ArrayAdapter<ItunesAdapter.Podcast> { } /** - * 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 - */ - @Nullable - public final String imageUrl; - /** - * URL of the podcast feed - */ - @Nullable - public final String feedUrl; - - - private Podcast(String title, @Nullable String imageUrl, @Nullable String feedUrl) { - this.title = title; - this.imageUrl = imageUrl; - this.feedUrl = feedUrl; - } - - /** - * Constructs a Podcast instance from a iTunes search result - * - * @param json object holding the podcast information - * @throws JSONException - */ - public static Podcast fromSearch(JSONObject json) { - String title = json.optString("collectionName", ""); - String imageUrl = json.optString("artworkUrl100", null); - String feedUrl = json.optString("feedUrl", null); - return new Podcast(title, imageUrl, feedUrl); - } - - public static Podcast fromSearch(SearchHit searchHit) { - return new Podcast(searchHit.getTitle(), searchHit.getThumbImageURL(), searchHit.getXmlUrl()); - } - - /** - * Constructs a Podcast instance from iTunes toplist entry - * - * @param json object holding the podcast information - * @throws JSONException - */ - public static Podcast fromToplist(JSONObject json) throws JSONException { - String title = json.getJSONObject("title").getString("label"); - String imageUrl = null; - JSONArray images = json.getJSONArray("im:image"); - for(int i=0; imageUrl == null && i < images.length(); i++) { - JSONObject image = images.getJSONObject(i); - String height = image.getJSONObject("attributes").getString("height"); - if(Integer.parseInt(height) >= 100) { - imageUrl = image.getString("label"); - } - } - String feedUrl = "https://itunes.apple.com/lookup?id=" + - json.getJSONObject("id").getJSONObject("attributes").getString("im:id"); - return new Podcast(title, imageUrl, feedUrl); - } - - } - - /** * View holder object for the GridView */ static class PodcastViewHolder { diff --git a/app/src/main/java/de/danoeh/antennapod/asynctask/ExportWorker.java b/app/src/main/java/de/danoeh/antennapod/asynctask/ExportWorker.java index 7c3c570a0..219725b01 100644 --- a/app/src/main/java/de/danoeh/antennapod/asynctask/ExportWorker.java +++ b/app/src/main/java/de/danoeh/antennapod/asynctask/ExportWorker.java @@ -23,15 +23,15 @@ public class ExportWorker { private static final String TAG = "ExportWorker"; private static final String DEFAULT_OUTPUT_NAME = "antennapod-feeds"; - private final ExportWriter exportWriter; - private final File output; + private final @NonNull ExportWriter exportWriter; + private final @NonNull File output; - public ExportWorker(ExportWriter exportWriter) { + public ExportWorker(@NonNull ExportWriter exportWriter) { this(exportWriter, new File(UserPreferences.getDataFolder(EXPORT_DIR), DEFAULT_OUTPUT_NAME + "." + exportWriter.fileExtension())); } - private ExportWorker(ExportWriter exportWriter, @NonNull File output) { + private ExportWorker(@NonNull ExportWriter exportWriter, @NonNull File output) { this.exportWriter = exportWriter; this.output = output; } diff --git a/app/src/main/java/de/danoeh/antennapod/config/ClientConfigurator.java b/app/src/main/java/de/danoeh/antennapod/config/ClientConfigurator.java index 4138738f6..3dd7c350d 100644 --- a/app/src/main/java/de/danoeh/antennapod/config/ClientConfigurator.java +++ b/app/src/main/java/de/danoeh/antennapod/config/ClientConfigurator.java @@ -16,7 +16,6 @@ class ClientConfigurator { ClientConfig.downloadServiceCallbacks = new DownloadServiceCallbacksImpl(); ClientConfig.gpodnetCallbacks = new GpodnetCallbacksImpl(); ClientConfig.playbackServiceCallbacks = new PlaybackServiceCallbacksImpl(); - ClientConfig.flattrCallbacks = new FlattrCallbacksImpl(); ClientConfig.dbTasksCallbacks = new DBTasksCallbacksImpl(); ClientConfig.castCallbacks = new CastCallbackImpl(); } diff --git a/app/src/main/java/de/danoeh/antennapod/config/FlattrCallbacksImpl.java b/app/src/main/java/de/danoeh/antennapod/config/FlattrCallbacksImpl.java deleted file mode 100644 index 3817db6de..000000000 --- a/app/src/main/java/de/danoeh/antennapod/config/FlattrCallbacksImpl.java +++ /dev/null @@ -1,53 +0,0 @@ -package de.danoeh.antennapod.config; - - -import android.app.PendingIntent; -import android.content.Context; -import android.content.Intent; -import android.util.Log; - -import org.shredzone.flattr4j.oauth.AccessToken; - -import de.danoeh.antennapod.BuildConfig; -import de.danoeh.antennapod.activity.FlattrAuthActivity; -import de.danoeh.antennapod.activity.MainActivity; -import de.danoeh.antennapod.core.FlattrCallbacks; - -public class FlattrCallbacksImpl implements FlattrCallbacks { - private static final String TAG = "FlattrCallbacksImpl"; - - @Override - public boolean flattrEnabled() { - return true; - } - - @Override - public Intent getFlattrAuthenticationActivityIntent(Context context) { - return new Intent(context, FlattrAuthActivity.class); - } - - @Override - public PendingIntent getFlattrFailedNotificationContentIntent(Context context) { - return PendingIntent.getActivity(context, 0, new Intent(context, MainActivity.class), 0); - } - - @Override - public String getFlattrAppKey() { - return BuildConfig.FLATTR_APP_KEY; - } - - @Override - public String getFlattrAppSecret() { - return BuildConfig.FLATTR_APP_SECRET; - } - - @Override - public void handleFlattrAuthenticationSuccess(AccessToken token) { - FlattrAuthActivity instance = FlattrAuthActivity.getInstance(); - if (instance != null) { - instance.handleAuthenticationSuccess(); - } else { - Log.e(TAG, "FlattrAuthActivity instance was null"); - } - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/AutoFlattrPreferenceDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/AutoFlattrPreferenceDialog.java deleted file mode 100644 index c28342374..000000000 --- a/app/src/main/java/de/danoeh/antennapod/dialog/AutoFlattrPreferenceDialog.java +++ /dev/null @@ -1,97 +0,0 @@ -package de.danoeh.antennapod.dialog; - -import android.annotation.SuppressLint; -import android.app.Activity; -import android.content.Context; -import android.support.v7.app.AlertDialog; -import android.view.View; -import android.widget.CheckBox; -import android.widget.SeekBar; -import android.widget.TextView; - -import org.apache.commons.lang3.Validate; - -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.core.preferences.UserPreferences; - -/** - * Creates a new AlertDialog that displays preferences for auto-flattring to the user. - */ -public class AutoFlattrPreferenceDialog { - - private AutoFlattrPreferenceDialog() { - } - - public static void newAutoFlattrPreferenceDialog(final Activity activity, final AutoFlattrPreferenceDialogInterface callback) { - Validate.notNull(activity); - Validate.notNull(callback); - - AlertDialog.Builder builder = new AlertDialog.Builder(activity); - - @SuppressLint("InflateParams") View view = activity.getLayoutInflater().inflate(R.layout.autoflattr_preference_dialog, null); - final CheckBox chkAutoFlattr = view.findViewById(R.id.chkAutoFlattr); - final SeekBar skbPercent = view.findViewById(R.id.skbPercent); - final TextView txtvStatus = view.findViewById(R.id.txtvStatus); - - chkAutoFlattr.setChecked(UserPreferences.isAutoFlattr()); - skbPercent.setEnabled(chkAutoFlattr.isChecked()); - txtvStatus.setEnabled(chkAutoFlattr.isChecked()); - - final int initialValue = (int) (UserPreferences.getAutoFlattrPlayedDurationThreshold() * 100.0f); - setStatusMsgText(activity, txtvStatus, initialValue); - skbPercent.setProgress(initialValue); - - chkAutoFlattr.setOnClickListener(v -> { - skbPercent.setEnabled(chkAutoFlattr.isChecked()); - txtvStatus.setEnabled(chkAutoFlattr.isChecked()); - }); - - skbPercent.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { - @Override - public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { - setStatusMsgText(activity, txtvStatus, progress); - } - - @Override - public void onStartTrackingTouch(SeekBar seekBar) { - - } - - @Override - public void onStopTrackingTouch(SeekBar seekBar) { - - } - }); - - builder.setTitle(R.string.pref_auto_flattr_title) - .setView(view) - .setPositiveButton(R.string.confirm_label, (dialog, which) -> { - float progDouble = ((float) skbPercent.getProgress()) / 100.0f; - callback.onConfirmed(chkAutoFlattr.isChecked(), progDouble); - dialog.dismiss(); - }) - .setNegativeButton(R.string.cancel_label, (dialog, which) -> { - callback.onCancelled(); - dialog.dismiss(); - }) - .setCancelable(false).show(); - } - - private static void setStatusMsgText(Context context, TextView txtvStatus, int progress) { - if (progress == 0) { - txtvStatus.setText(R.string.auto_flattr_ater_beginning); - } else if (progress == 100) { - txtvStatus.setText(R.string.auto_flattr_ater_end); - } else { - txtvStatus.setText(context.getString(R.string.auto_flattr_after_percent, progress)); - } - } - - public interface AutoFlattrPreferenceDialogInterface { - void onCancelled(); - - void onConfirmed(boolean autoFlattrEnabled, float autoFlattrValue); - } - - -} diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/ChooseDataFolderDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/ChooseDataFolderDialog.java index e8faa7c29..c185a5557 100644 --- a/app/src/main/java/de/danoeh/antennapod/dialog/ChooseDataFolderDialog.java +++ b/app/src/main/java/de/danoeh/antennapod/dialog/ChooseDataFolderDialog.java @@ -1,20 +1,11 @@ package de.danoeh.antennapod.dialog; import android.content.Context; -import android.os.Build; -import android.support.v4.content.ContextCompat; -import android.text.Html; import com.afollestad.materialdialogs.MaterialDialog; -import java.io.File; -import java.util.ArrayList; -import java.util.List; - import de.danoeh.antennapod.R; -import de.danoeh.antennapod.core.preferences.UserPreferences; -import de.danoeh.antennapod.core.util.Converter; -import de.danoeh.antennapod.core.util.StorageUtils; +import de.danoeh.antennapod.adapter.DataFolderAdapter; public class ChooseDataFolderDialog { @@ -31,39 +22,9 @@ public class ChooseDataFolderDialog { private ChooseDataFolderDialog() {} public static void showDialog(final Context context, RunnableWithString handlerFunc) { - File dataFolder = UserPreferences.getDataFolder(null); - if (dataFolder == null) { - new MaterialDialog.Builder(context) - .title(R.string.error_label) - .content(R.string.external_storage_error_msg) - .neutralText(android.R.string.ok) - .show(); - return; - } - String dataFolderPath = dataFolder.getAbsolutePath(); - int selectedIndex = -1; - int index = 0; - File[] mediaDirs = ContextCompat.getExternalFilesDirs(context, null); - final List<String> folders = new ArrayList<>(mediaDirs.length); - final List<CharSequence> choices = new ArrayList<>(mediaDirs.length); - for (File dir : mediaDirs) { - if(dir == null || !dir.exists() || !dir.canRead() || !dir.canWrite()) { - continue; - } - String path = dir.getAbsolutePath(); - folders.add(path); - if(dataFolderPath.equals(path)) { - selectedIndex = index; - } - int prefixIndex = path.indexOf("Android"); - String choice = (prefixIndex > 0) ? path.substring(0, prefixIndex) : path; - long bytes = StorageUtils.getFreeSpaceAvailable(path); - String item = String.format( - "<small>%1$s [%2$s]</small>", choice, Converter.byteToString(bytes)); - choices.add(fromHtmlVersioned(item)); - index++; - } - if (choices.isEmpty()) { + DataFolderAdapter adapter = new DataFolderAdapter(context, handlerFunc); + + if (adapter.getItemCount() == 0) { new MaterialDialog.Builder(context) .title(R.string.error_label) .content(R.string.external_storage_error_msg) @@ -71,27 +32,16 @@ public class ChooseDataFolderDialog { .show(); return; } + MaterialDialog dialog = new MaterialDialog.Builder(context) .title(R.string.choose_data_directory) .content(R.string.choose_data_directory_message) - .items(choices) - .itemsCallbackSingleChoice(selectedIndex, (dialog1, itemView, which, text) -> { - String folder = folders.get(which); - handlerFunc.run(folder); - return true; - }) + .adapter(adapter, null) .negativeText(R.string.cancel_label) .cancelable(true) .build(); + adapter.setDialog(dialog); dialog.show(); } - @SuppressWarnings("deprecation") - private static CharSequence fromHtmlVersioned(final String html) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { - return Html.fromHtml(html); - } - return Html.fromHtml(html, Html.FROM_HTML_MODE_LEGACY); - } - }
\ No newline at end of file diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/EpisodeFilterDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/EpisodeFilterDialog.java new file mode 100644 index 000000000..ba778be56 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/dialog/EpisodeFilterDialog.java @@ -0,0 +1,71 @@ +package de.danoeh.antennapod.dialog; + +import android.app.Dialog; +import android.content.Context; +import android.os.Bundle; +import android.widget.Button; +import android.widget.EditText; +import android.widget.RadioButton; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.core.feed.FeedFilter; + +/** + * Displays a dialog with a text box for filtering episodes and two radio buttons for exclusion/inclusion + */ +public abstract class EpisodeFilterDialog extends Dialog { + + private final FeedFilter initialFilter; + + public EpisodeFilterDialog(Context context, FeedFilter filter) { + super(context); + this.initialFilter = filter; + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.episode_filter_dialog); + final EditText etxtEpisodeFilterText = findViewById(R.id.etxtEpisodeFilterText); + final RadioButton radioInclude = findViewById(R.id.radio_filter_include); + final RadioButton radioExclude = findViewById(R.id.radio_filter_exclude); + final Button butConfirm = findViewById(R.id.butConfirm); + final Button butCancel = findViewById(R.id.butCancel); + + setTitle(R.string.episode_filters_label); + setOnCancelListener(dialog -> onCancelled()); + + if (initialFilter.includeOnly()) { + radioInclude.setChecked(true); + etxtEpisodeFilterText.setText(initialFilter.getIncludeFilter()); + } else if(initialFilter.excludeOnly()) { + radioExclude.setChecked(true); + etxtEpisodeFilterText.setText(initialFilter.getExcludeFilter()); + } else { + radioExclude.setChecked(false); + radioInclude.setChecked(false); + etxtEpisodeFilterText.setText(""); + } + + + butCancel.setOnClickListener(v -> cancel()); + butConfirm.setOnClickListener(v -> { + + String includeString = ""; + String excludeString = ""; + if (radioInclude.isChecked()) { + includeString = etxtEpisodeFilterText.getText().toString(); + } else { + excludeString = etxtEpisodeFilterText.getText().toString(); + } + + onConfirmed(new FeedFilter(includeString, excludeString)); + dismiss(); + }); + } + + protected void onCancelled() { + + } + + protected abstract void onConfirmed(FeedFilter filter); +} 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 07a64cde8..7697aaa69 100644 --- a/app/src/main/java/de/danoeh/antennapod/dialog/EpisodesApplyActionFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/dialog/EpisodesApplyActionFragment.java @@ -3,10 +3,20 @@ 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; +import android.support.annotation.PluralsRes; +import android.support.annotation.StringRes; +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; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; @@ -14,11 +24,12 @@ import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.ArrayAdapter; -import android.widget.Button; import android.widget.ListView; -import android.widget.Toast; + +import com.leinardi.android.speeddial.SpeedDialView; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; @@ -35,22 +46,42 @@ public class EpisodesApplyActionFragment extends Fragment { public static final String TAG = "EpisodeActionFragment"; - public static final int ACTION_QUEUE = 1; - private static final int ACTION_MARK_PLAYED = 2; - private static final int ACTION_MARK_UNPLAYED = 4; - private static final int ACTION_DOWNLOAD = 8; - public static final int ACTION_REMOVE = 16; - private static final int ACTION_ALL = ACTION_QUEUE | ACTION_MARK_PLAYED | ACTION_MARK_UNPLAYED - | ACTION_DOWNLOAD | ACTION_REMOVE; + public static final int ACTION_ADD_TO_QUEUE = 1; + public static final int ACTION_REMOVE_FROM_QUEUE = 2; + private static final int ACTION_MARK_PLAYED = 4; + private static final int ACTION_MARK_UNPLAYED = 8; + private static final int ACTION_DOWNLOAD = 16; + public static final int ACTION_DELETE = 32; + private static final int ACTION_ALL = ACTION_ADD_TO_QUEUE | ACTION_REMOVE_FROM_QUEUE + | ACTION_MARK_PLAYED | ACTION_MARK_UNPLAYED | ACTION_DOWNLOAD | ACTION_DELETE; + + /** + * Specify an action (defined by #flag) 's UI bindings. + * + * Includes: the menu / action item and the actual logic + */ + private class ActionBinding { + int flag; + @IdRes + final int actionItemId; + @NonNull + final Runnable action; + + ActionBinding(int flag, @IdRes int actionItemId, @NonNull Runnable action) { + this.flag = flag; + this.actionItemId = actionItemId; + this.action = action; + } + } + + private final List<? extends ActionBinding> actionBindings; private ListView mListView; private ArrayAdapter<String> mAdapter; - private Button btnAddToQueue; - private Button btnMarkAsPlayed; - private Button btnMarkAsUnplayed; - private Button btnDownload; - private Button btnDelete; + private SpeedDialView mSpeedDialView; + @NonNull + private CharSequence actionBarTitleOriginal = ""; private final Map<Long,FeedItem> idMap = new ArrayMap<>(); private final List<FeedItem> episodes = new ArrayList<>(); @@ -60,6 +91,23 @@ public class EpisodesApplyActionFragment extends Fragment { private MenuItem mSelectToggle; + public EpisodesApplyActionFragment() { + actionBindings = Arrays.asList( + new ActionBinding(ACTION_ADD_TO_QUEUE, + R.id.add_to_queue_batch, this::queueChecked), + new ActionBinding(ACTION_REMOVE_FROM_QUEUE, + R.id.remove_from_queue_batch, this::removeFromQueueChecked), + new ActionBinding(ACTION_MARK_PLAYED, + R.id.mark_read_batch, this::markedCheckedPlayed), + new ActionBinding(ACTION_MARK_UNPLAYED, + R.id.mark_unread_batch, this::markedCheckedUnplayed), + new ActionBinding(ACTION_DOWNLOAD, + R.id.download_batch, this::downloadChecked), + new ActionBinding(ACTION_DELETE, + R.id.delete_batch, this::deleteChecked) + ); + } + public static EpisodesApplyActionFragment newInstance(List<FeedItem> items) { return newInstance(items, ACTION_ALL); } @@ -77,6 +125,7 @@ public class EpisodesApplyActionFragment extends Fragment { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + setRetainInstance(true); setHasOptionsMenu(true); } @@ -124,57 +173,58 @@ public class EpisodesApplyActionFragment extends Fragment { } mAdapter = new ArrayAdapter<>(getActivity(), - android.R.layout.simple_list_item_multiple_choice, titles); + R.layout.simple_list_item_multiple_choice_on_start, titles); mListView.setAdapter(mAdapter); - checkAll(); - int lastVisibleDiv = 0; - btnAddToQueue = view.findViewById(R.id.btnAddToQueue); - if((actions & ACTION_QUEUE) != 0) { - btnAddToQueue.setOnClickListener(v -> queueChecked()); - lastVisibleDiv = R.id.divider1; - } else { - btnAddToQueue.setVisibility(View.GONE); - view.findViewById(R.id.divider1).setVisibility(View.GONE); - } - btnMarkAsPlayed = view.findViewById(R.id.btnMarkAsPlayed); - if((actions & ACTION_MARK_PLAYED) != 0) { - btnMarkAsPlayed.setOnClickListener(v -> markedCheckedPlayed()); - lastVisibleDiv = R.id.divider2; - } else { - btnMarkAsPlayed.setVisibility(View.GONE); - view.findViewById(R.id.divider2).setVisibility(View.GONE); - } - btnMarkAsUnplayed = view.findViewById(R.id.btnMarkAsUnplayed); - if((actions & ACTION_MARK_UNPLAYED) != 0) { - btnMarkAsUnplayed.setOnClickListener(v -> markedCheckedUnplayed()); - lastVisibleDiv = R.id.divider3; - } else { - btnMarkAsUnplayed.setVisibility(View.GONE); - view.findViewById(R.id.divider3).setVisibility(View.GONE); - } - btnDownload = view.findViewById(R.id.btnDownload); - if((actions & ACTION_DOWNLOAD) != 0) { - btnDownload.setOnClickListener(v -> downloadChecked()); - lastVisibleDiv = R.id.divider4; - } else { - btnDownload.setVisibility(View.GONE); - view.findViewById(R.id.divider4).setVisibility(View.GONE); + saveActionBarTitle(); // needed when we dynamically change the title based on selection + + // Init action UI (via a FAB Speed Dial) + mSpeedDialView = view.findViewById(R.id.fabSD); + mSpeedDialView.inflate(R.menu.episodes_apply_action_speeddial); + + // show only specified actions, and bind speed dial UIs to the actual logic + for (ActionBinding binding : actionBindings) { + if ((actions & binding.flag) == 0) { + mSpeedDialView.removeActionItemById(binding.actionItemId); + } } - btnDelete = view.findViewById(R.id.btnDelete); - if((actions & ACTION_REMOVE) != 0) { - btnDelete.setOnClickListener(v -> deleteChecked()); - } else { - btnDelete.setVisibility(View.GONE); - if(lastVisibleDiv > 0) { - view.findViewById(lastVisibleDiv).setVisibility(View.GONE); + + mSpeedDialView.setOnActionSelectedListener(actionItem -> { + ActionBinding selectedBinding = null; + for (ActionBinding binding : actionBindings) { + if (actionItem.getId() == binding.actionItemId) { + selectedBinding = binding; + break; + } } + if (selectedBinding != null) { + selectedBinding.action.run(); + } else { + Log.e(TAG, "Unrecognized speed dial action item. Do nothing. id=" + actionItem.getId()); + } + return true; + }); + + if (Build.VERSION.SDK_INT == 23 || Build.VERSION.SDK_INT == 24) { + ViewCompat.setElevation(view.findViewById(R.id.fabSDScrollCtr), 8); } + showSpeedDialIfAnyChecked(); + return view; } @Override + public void onStop() { + restoreActionBarTitle(); // it might have been changed to "N selected". Restore original. + super.onStop(); + } + + private void showSpeedDialIfAnyChecked() { + mSpeedDialView.setVisibility(checkedIds.size() > 0 ? View.VISIBLE : View.GONE); + } + + @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { super.onCreateOptionsMenu(menu, inflater); inflater.inflate(R.menu.episodes_apply_action_options, menu); @@ -196,11 +246,9 @@ public class EpisodesApplyActionFragment extends Fragment { int[] icon = new int[1]; if (checkedIds.size() == episodes.size()) { - icon[0] = R.attr.ic_check_box; - } else if (checkedIds.size() == 0) { - icon[0] = R.attr.ic_check_box_outline; + icon[0] = R.attr.ic_select_none; } else { - icon[0] = R.attr.ic_indeterminate_check_box; + icon[0] = R.attr.ic_select_all; } TypedArray a = getActivity().obtainStyledAttributes(icon); @@ -212,7 +260,7 @@ public class EpisodesApplyActionFragment extends Fragment { @Override public boolean onOptionsItemSelected(MenuItem item) { - int resId = 0; + @StringRes int resId = 0; switch(item.getItemId()) { case R.id.select_options: return true; @@ -272,7 +320,8 @@ public class EpisodesApplyActionFragment extends Fragment { return true; } if(resId != 0) { - Toast.makeText(getActivity(), resId, Toast.LENGTH_SHORT).show(); + Snackbar.make(getActivity().findViewById(R.id.content), resId, Snackbar.LENGTH_SHORT) + .show(); return true; } else { return false; @@ -410,21 +459,56 @@ public class EpisodesApplyActionFragment extends Fragment { mListView.setItemChecked(i, checked); } ActivityCompat.invalidateOptionsMenu(EpisodesApplyActionFragment.this.getActivity()); + showSpeedDialIfAnyChecked(); + updateActionBarTitle(); + } + + private void saveActionBarTitle() { + ActionBar actionBar = ((AppCompatActivity) getActivity()).getSupportActionBar(); + if (actionBar != null) { + CharSequence title = actionBar.getTitle(); + if (title == null) { + title = ""; + } + actionBarTitleOriginal = title; + } + } + + private void restoreActionBarTitle() { + ActionBar actionBar = ((AppCompatActivity) getActivity()).getSupportActionBar(); + if (actionBar != null) { + actionBar.setTitle(actionBarTitleOriginal); + } + } + + private void updateActionBarTitle() { + ActionBar actionBar = ((AppCompatActivity) getActivity()).getSupportActionBar(); + if (actionBar != null) { + CharSequence title = checkedIds.size() > 0 ? + getString(R.string.num_selected_label, checkedIds.size()) : + actionBarTitleOriginal; + actionBar.setTitle(title); + } } private void queueChecked() { DBWriter.addQueueItem(getActivity(), true, checkedIds.toArray()); - close(); + close(R.plurals.added_to_queue_batch_label, checkedIds.size()); + } + + private void removeFromQueueChecked() { + DBWriter.removeQueueItem(getActivity(), true, checkedIds.toArray()); + close(R.plurals.removed_from_queue_batch_label, checkedIds.size()); } private void markedCheckedPlayed() { DBWriter.markItemPlayed(FeedItem.PLAYED, checkedIds.toArray()); - close(); + close(R.plurals.marked_read_batch_label, checkedIds.size()); } private void markedCheckedUnplayed() { DBWriter.markItemPlayed(FeedItem.UNPLAYED, checkedIds.toArray()); - close(); + close(R.plurals.marked_unread_batch_label, checkedIds.size()); } private void downloadChecked() { @@ -441,7 +525,7 @@ public class EpisodesApplyActionFragment extends Fragment { e.printStackTrace(); DownloadRequestErrorDialogCreator.newRequestErrorDialog(getActivity(), e.getMessage()); } - close(); + close(R.plurals.downloading_batch_label, checkedIds.size()); } private void deleteChecked() { @@ -451,10 +535,18 @@ public class EpisodesApplyActionFragment extends Fragment { DBWriter.deleteFeedMediaOfItem(getActivity(), episode.getMedia().getId()); } } - close(); + close(R.plurals.deleted_episode_batch_label, checkedIds.size()); } - private void close() { + private void close(@PluralsRes int msgId, int numItems) { + if (numItems > 0) { + Snackbar.make(getActivity().findViewById(R.id.content), + getResources().getQuantityString(msgId, numItems, numItems), + Snackbar.LENGTH_LONG + ) + .setAction(android.R.string.ok, v -> {}) + .show(); + } getActivity().getSupportFragmentManager().popBackStack(); } diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/ProxyDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/ProxyDialog.java index 8f2629b43..c1008a380 100644 --- a/app/src/main/java/de/danoeh/antennapod/dialog/ProxyDialog.java +++ b/app/src/main/java/de/danoeh/antennapod/dialog/ProxyDialog.java @@ -3,6 +3,7 @@ package de.danoeh.antennapod.dialog; import android.app.Dialog; import android.content.Context; import android.content.res.TypedArray; +import android.os.Build; import android.support.v4.content.ContextCompat; import android.text.Editable; import android.text.TextUtils; @@ -23,6 +24,8 @@ import java.io.IOException; import java.net.InetSocketAddress; import java.net.Proxy; import java.net.SocketAddress; +import java.util.ArrayList; +import java.util.List; import java.util.concurrent.TimeUnit; import de.danoeh.antennapod.R; @@ -92,7 +95,11 @@ public class ProxyDialog { if(!TextUtils.isEmpty(port)) { portValue = Integer.valueOf(port); } - proxy = ProxyConfig.http(host, portValue, username, password); + if (Proxy.Type.valueOf(type) == Proxy.Type.SOCKS) { + proxy = ProxyConfig.socks(host, portValue, username, password); + } else { + proxy = ProxyConfig.http(host, portValue, username, password); + } } UserPreferences.setProxyConfig(proxy); AntennapodHttpClient.reinit(); @@ -103,7 +110,13 @@ public class ProxyDialog { .build(); View view = dialog.getCustomView(); spType = view.findViewById(R.id.spType); - String[] types = { Proxy.Type.DIRECT.name(), Proxy.Type.HTTP.name() }; + + List<String> types= new ArrayList<>(); + types.add(Proxy.Type.DIRECT.name()); + types.add(Proxy.Type.HTTP.name()); + if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + types.add(Proxy.Type.SOCKS.name()); + } ArrayAdapter<String> adapter = new ArrayAdapter<>(context, android.R.layout.simple_spinner_item, types); adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/RatingDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/RatingDialog.java index ece184035..24656ed29 100644 --- a/app/src/main/java/de/danoeh/antennapod/dialog/RatingDialog.java +++ b/app/src/main/java/de/danoeh/antennapod/dialog/RatingDialog.java @@ -6,6 +6,7 @@ import android.content.Intent; import android.content.SharedPreferences; import android.net.Uri; import android.support.annotation.Nullable; +import android.support.annotation.VisibleForTesting; import android.util.Log; import com.afollestad.materialdialogs.MaterialDialog; @@ -73,7 +74,8 @@ public class RatingDialog { return mPreferences.getBoolean(KEY_RATED, false); } - private static void saveRated() { + @VisibleForTesting + public static void saveRated() { mPreferences .edit() .putBoolean(KEY_RATED, true) diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/SleepTimerDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/SleepTimerDialog.java index 4b8601ec6..dc056a3f9 100644 --- a/app/src/main/java/de/danoeh/antennapod/dialog/SleepTimerDialog.java +++ b/app/src/main/java/de/danoeh/antennapod/dialog/SleepTimerDialog.java @@ -18,7 +18,7 @@ import com.afollestad.materialdialogs.MaterialDialog; import de.danoeh.antennapod.R; import de.danoeh.antennapod.core.event.MessageEvent; import de.danoeh.antennapod.core.preferences.SleepTimerPreferences; -import de.greenrobot.event.EventBus; +import org.greenrobot.eventbus.EventBus; public abstract class SleepTimerDialog { diff --git a/app/src/main/java/de/danoeh/antennapod/discovery/CombinedSearcher.java b/app/src/main/java/de/danoeh/antennapod/discovery/CombinedSearcher.java new file mode 100644 index 000000000..18d65a03c --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/discovery/CombinedSearcher.java @@ -0,0 +1,96 @@ +package de.danoeh.antennapod.discovery; + +import android.content.Context; +import android.util.Log; +import android.util.Pair; +import io.reactivex.Single; +import io.reactivex.SingleOnSubscribe; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.Disposable; +import io.reactivex.schedulers.Schedulers; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CountDownLatch; + +public class CombinedSearcher implements PodcastSearcher { + private static final String TAG = "CombinedSearcher"; + + private final List<Pair<PodcastSearcher, Float>> searchProviders = new ArrayList<>(); + + public CombinedSearcher(Context context) { + addProvider(new FyydPodcastSearcher(), 1.f); + addProvider(new ItunesPodcastSearcher(context), 1.f); + addProvider(new GpodnetPodcastSearcher(), 0.6f); + } + + private void addProvider(PodcastSearcher provider, float priority) { + searchProviders.add(new Pair<>(provider, priority)); + } + + public Single<List<PodcastSearchResult>> search(String query) { + ArrayList<Disposable> disposables = new ArrayList<>(); + List<List<PodcastSearchResult>> singleResults = new ArrayList<>(Collections.nCopies(searchProviders.size(), null)); + CountDownLatch latch = new CountDownLatch(searchProviders.size()); + for (int i = 0; i < searchProviders.size(); i++) { + Pair<PodcastSearcher, Float> searchProviderInfo = searchProviders.get(i); + PodcastSearcher searcher = searchProviderInfo.first; + final int index = i; + disposables.add(searcher.search(query).subscribe(e -> { + singleResults.set(index, e); + latch.countDown(); + }, throwable -> { + Log.d(TAG, Log.getStackTraceString(throwable)); + latch.countDown(); + } + )); + } + + return Single.create((SingleOnSubscribe<List<PodcastSearchResult>>) subscriber -> { + latch.await(); + List<PodcastSearchResult> results = weightSearchResults(singleResults); + subscriber.onSuccess(results); + }) + .doOnDispose(() -> { + for (Disposable disposable : disposables) { + disposable.dispose(); + } + }) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()); + } + + private List<PodcastSearchResult> weightSearchResults(List<List<PodcastSearchResult>> singleResults) { + HashMap<String, Float> resultRanking = new HashMap<>(); + HashMap<String, PodcastSearchResult> urlToResult = new HashMap<>(); + for (int i = 0; i < singleResults.size(); i++) { + float providerPriority = searchProviders.get(i).second; + List<PodcastSearchResult> providerResults = singleResults.get(i); + if (providerResults == null) { + continue; + } + for (int position = 0; position < providerResults.size(); position++) { + PodcastSearchResult result = providerResults.get(position); + urlToResult.put(result.feedUrl, result); + + float ranking = 0; + if (resultRanking.containsKey(result.feedUrl)) { + ranking = resultRanking.get(result.feedUrl); + } + ranking += 1.f / (position + 1.f); + resultRanking.put(result.feedUrl, ranking * providerPriority); + } + } + List<Map.Entry<String, Float>> sortedResults = new ArrayList<>(resultRanking.entrySet()); + Collections.sort(sortedResults, (o1, o2) -> Double.compare(o2.getValue(), o1.getValue())); + + List<PodcastSearchResult> results = new ArrayList<>(); + for (Map.Entry<String, Float> res : sortedResults) { + results.add(urlToResult.get(res.getKey())); + } + return results; + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/discovery/FyydPodcastSearcher.java b/app/src/main/java/de/danoeh/antennapod/discovery/FyydPodcastSearcher.java new file mode 100644 index 000000000..529a9e3d5 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/discovery/FyydPodcastSearcher.java @@ -0,0 +1,38 @@ +package de.danoeh.antennapod.discovery; + +import de.danoeh.antennapod.core.service.download.AntennapodHttpClient; +import de.mfietz.fyydlin.FyydClient; +import de.mfietz.fyydlin.FyydResponse; +import de.mfietz.fyydlin.SearchHit; +import io.reactivex.Single; +import io.reactivex.SingleOnSubscribe; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.schedulers.Schedulers; + +import java.util.ArrayList; +import java.util.List; + +public class FyydPodcastSearcher implements PodcastSearcher { + private final FyydClient client = new FyydClient(AntennapodHttpClient.getHttpClient()); + + public Single<List<PodcastSearchResult>> search(String query) { + return Single.create((SingleOnSubscribe<List<PodcastSearchResult>>) subscriber -> { + FyydResponse response = client.searchPodcasts(query, 10) + .subscribeOn(Schedulers.io()) + .blockingGet(); + + ArrayList<PodcastSearchResult> searchResults = new ArrayList<>(); + + if (!response.getData().isEmpty()) { + for (SearchHit searchHit : response.getData()) { + PodcastSearchResult podcast = PodcastSearchResult.fromFyyd(searchHit); + searchResults.add(podcast); + } + } + + subscriber.onSuccess(searchResults); + }) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/discovery/GpodnetPodcastSearcher.java b/app/src/main/java/de/danoeh/antennapod/discovery/GpodnetPodcastSearcher.java new file mode 100644 index 000000000..6e5debb38 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/discovery/GpodnetPodcastSearcher.java @@ -0,0 +1,38 @@ +package de.danoeh.antennapod.discovery; + +import de.danoeh.antennapod.core.gpoddernet.GpodnetService; +import de.danoeh.antennapod.core.gpoddernet.GpodnetServiceException; +import de.danoeh.antennapod.core.gpoddernet.model.GpodnetPodcast; +import io.reactivex.Single; +import io.reactivex.SingleOnSubscribe; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.schedulers.Schedulers; + +import java.util.ArrayList; +import java.util.List; + +public class GpodnetPodcastSearcher implements PodcastSearcher { + public Single<List<PodcastSearchResult>> search(String query) { + return Single.create((SingleOnSubscribe<List<PodcastSearchResult>>) subscriber -> { + GpodnetService service = null; + try { + service = new GpodnetService(); + List<GpodnetPodcast> gpodnetPodcasts = service.searchPodcasts(query, 0); + List<PodcastSearchResult> results = new ArrayList<>(); + for (GpodnetPodcast podcast : gpodnetPodcasts) { + results.add(PodcastSearchResult.fromGpodder(podcast)); + } + subscriber.onSuccess(results); + } catch (GpodnetServiceException e) { + e.printStackTrace(); + subscriber.onError(e); + } finally { + if (service != null) { + service.shutdown(); + } + } + }) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/discovery/ItunesPodcastSearcher.java b/app/src/main/java/de/danoeh/antennapod/discovery/ItunesPodcastSearcher.java new file mode 100644 index 000000000..a91aae1a8 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/discovery/ItunesPodcastSearcher.java @@ -0,0 +1,74 @@ +package de.danoeh.antennapod.discovery; + +import android.content.Context; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.core.ClientConfig; +import de.danoeh.antennapod.core.service.download.AntennapodHttpClient; +import io.reactivex.Single; +import io.reactivex.SingleOnSubscribe; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.schedulers.Schedulers; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.util.ArrayList; +import java.util.List; + +public class ItunesPodcastSearcher implements PodcastSearcher { + private static final String ITUNES_API_URL = "https://itunes.apple.com/search?media=podcast&term=%s"; + private final Context context; + + public ItunesPodcastSearcher(Context context) { + this.context = context; + } + + public Single<List<PodcastSearchResult>> search(String query) { + return Single.create((SingleOnSubscribe<List<PodcastSearchResult>>) subscriber -> { + String encodedQuery; + try { + encodedQuery = URLEncoder.encode(query, "UTF-8"); + } catch (UnsupportedEncodingException e) { + // this won't ever be thrown + encodedQuery = query; + } + + String formattedUrl = String.format(ITUNES_API_URL, encodedQuery); + + OkHttpClient client = AntennapodHttpClient.getHttpClient(); + Request.Builder httpReq = new Request.Builder() + .url(formattedUrl) + .header("User-Agent", ClientConfig.USER_AGENT); + List<PodcastSearchResult> podcasts = new ArrayList<>(); + try { + Response response = client.newCall(httpReq.build()).execute(); + + if (response.isSuccessful()) { + String resultString = response.body().string(); + JSONObject result = new JSONObject(resultString); + JSONArray j = result.getJSONArray("results"); + + for (int i = 0; i < j.length(); i++) { + JSONObject podcastJson = j.getJSONObject(i); + PodcastSearchResult podcast = PodcastSearchResult.fromItunes(podcastJson); + podcasts.add(podcast); + } + } else { + String prefix = context.getString(R.string.error_msg_prefix); + subscriber.onError(new IOException(prefix + response)); + } + } catch (IOException | JSONException e) { + subscriber.onError(e); + } + subscriber.onSuccess(podcasts); + }) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/discovery/ItunesTopListLoader.java b/app/src/main/java/de/danoeh/antennapod/discovery/ItunesTopListLoader.java new file mode 100644 index 000000000..bc9133258 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/discovery/ItunesTopListLoader.java @@ -0,0 +1,110 @@ +package de.danoeh.antennapod.discovery; + +import android.content.Context; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.core.ClientConfig; +import de.danoeh.antennapod.core.service.download.AntennapodHttpClient; +import io.reactivex.Single; +import io.reactivex.SingleOnSubscribe; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.schedulers.Schedulers; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +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 java.util.Locale; + +public class ItunesTopListLoader { + private final Context context; + + public ItunesTopListLoader(Context context) { + this.context = context; + } + + public Single<List<PodcastSearchResult>> loadToplist(int limit) { + return Single.create((SingleOnSubscribe<List<PodcastSearchResult>>) emitter -> { + String lang = Locale.getDefault().getLanguage(); + OkHttpClient client = AntennapodHttpClient.getHttpClient(); + String feedString; + try { + try { + feedString = getTopListFeed(client, lang, limit); + } catch (IOException e) { + feedString = getTopListFeed(client, "us", limit); + } + List<PodcastSearchResult> podcasts = parseFeed(feedString); + emitter.onSuccess(podcasts); + } catch (IOException | JSONException e) { + emitter.onError(e); + } + }) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()); + } + + public Single<String> getFeedUrl(PodcastSearchResult podcast) { + if (!podcast.feedUrl.contains("itunes.apple.com")) { + return Single.just(podcast.feedUrl) + .observeOn(AndroidSchedulers.mainThread()); + } + return Single.create((SingleOnSubscribe<String>) emitter -> { + OkHttpClient client = AntennapodHttpClient.getHttpClient(); + Request.Builder httpReq = new Request.Builder() + .url(podcast.feedUrl) + .header("User-Agent", ClientConfig.USER_AGENT); + try { + Response response = client.newCall(httpReq.build()).execute(); + if (response.isSuccessful()) { + String resultString = response.body().string(); + JSONObject result = new JSONObject(resultString); + JSONObject results = result.getJSONArray("results").getJSONObject(0); + String feedUrl = results.getString("feedUrl"); + emitter.onSuccess(feedUrl); + } else { + String prefix = context.getString(R.string.error_msg_prefix); + emitter.onError(new IOException(prefix + response)); + } + } catch (IOException | JSONException e) { + emitter.onError(e); + } + }) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()); + } + + private String getTopListFeed(OkHttpClient client, String language, int limit) throws IOException { + String url = "https://itunes.apple.com/%s/rss/toppodcasts/limit="+limit+"/explicit=true/json"; + Request.Builder httpReq = new Request.Builder() + .header("User-Agent", ClientConfig.USER_AGENT) + .url(String.format(url, language)); + + try (Response response = client.newCall(httpReq.build()).execute()) { + if (response.isSuccessful()) { + return response.body().string(); + } + String prefix = context.getString(R.string.error_msg_prefix); + throw new IOException(prefix + response); + } + } + + private List<PodcastSearchResult> parseFeed(String jsonString) throws JSONException { + JSONObject result = new JSONObject(jsonString); + JSONObject feed = result.getJSONObject("feed"); + JSONArray entries = feed.getJSONArray("entry"); + + List<PodcastSearchResult> results = new ArrayList<>(); + for (int i=0; i < entries.length(); i++) { + JSONObject json = entries.getJSONObject(i); + results.add(PodcastSearchResult.fromItunesToplist(json)); + } + + return results; + } + +} diff --git a/app/src/main/java/de/danoeh/antennapod/discovery/PodcastSearchResult.java b/app/src/main/java/de/danoeh/antennapod/discovery/PodcastSearchResult.java new file mode 100644 index 000000000..ca9ed83d7 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/discovery/PodcastSearchResult.java @@ -0,0 +1,81 @@ +package de.danoeh.antennapod.discovery; + +import android.support.annotation.Nullable; +import de.danoeh.antennapod.core.gpoddernet.model.GpodnetPodcast; +import de.mfietz.fyydlin.SearchHit; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +public class PodcastSearchResult { + + /** + * The name of the podcast + */ + public final String title; + + /** + * URL of the podcast image + */ + @Nullable + public final String imageUrl; + /** + * URL of the podcast feed + */ + @Nullable + public final String feedUrl; + + + private PodcastSearchResult(String title, @Nullable String imageUrl, @Nullable String feedUrl) { + this.title = title; + this.imageUrl = imageUrl; + this.feedUrl = feedUrl; + } + + public static PodcastSearchResult dummy() { + return new PodcastSearchResult("", "", ""); + } + + /** + * Constructs a Podcast instance from a iTunes search result + * + * @param json object holding the podcast information + * @throws JSONException + */ + public static PodcastSearchResult fromItunes(JSONObject json) { + String title = json.optString("collectionName", ""); + String imageUrl = json.optString("artworkUrl100", null); + String feedUrl = json.optString("feedUrl", null); + return new PodcastSearchResult(title, imageUrl, feedUrl); + } + + /** + * Constructs a Podcast instance from iTunes toplist entry + * + * @param json object holding the podcast information + * @throws JSONException + */ + public static PodcastSearchResult fromItunesToplist(JSONObject json) throws JSONException { + String title = json.getJSONObject("title").getString("label"); + String imageUrl = null; + JSONArray images = json.getJSONArray("im:image"); + for(int i=0; imageUrl == null && i < images.length(); i++) { + JSONObject image = images.getJSONObject(i); + String height = image.getJSONObject("attributes").getString("height"); + if(Integer.parseInt(height) >= 100) { + imageUrl = image.getString("label"); + } + } + String feedUrl = "https://itunes.apple.com/lookup?id=" + + json.getJSONObject("id").getJSONObject("attributes").getString("im:id"); + return new PodcastSearchResult(title, imageUrl, feedUrl); + } + + public static PodcastSearchResult fromFyyd(SearchHit searchHit) { + return new PodcastSearchResult(searchHit.getTitle(), searchHit.getThumbImageURL(), searchHit.getXmlUrl()); + } + + public static PodcastSearchResult fromGpodder(GpodnetPodcast searchHit) { + return new PodcastSearchResult(searchHit.getTitle(), searchHit.getLogoUrl(), searchHit.getUrl()); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/discovery/PodcastSearcher.java b/app/src/main/java/de/danoeh/antennapod/discovery/PodcastSearcher.java new file mode 100644 index 000000000..b19d7d695 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/discovery/PodcastSearcher.java @@ -0,0 +1,10 @@ +package de.danoeh.antennapod.discovery; + +import io.reactivex.Single; +import io.reactivex.disposables.Disposable; +import io.reactivex.functions.Consumer; +import java.util.List; + +public interface PodcastSearcher { + Single<List<PodcastSearchResult>> search(String query); +} 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 ee2373da8..35bcaa76e 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/AddFeedFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/AddFeedFragment.java @@ -2,13 +2,20 @@ package de.danoeh.antennapod.fragment; import android.content.Intent; import android.os.Bundle; +import android.provider.MediaStore; import android.support.v4.app.Fragment; +import android.view.ContextMenu; +import android.view.KeyEvent; import android.view.LayoutInflater; +import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; +import android.view.inputmethod.EditorInfo; import android.widget.Button; import android.widget.EditText; +import android.widget.ImageView; +import android.widget.TextView; import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.MainActivity; import de.danoeh.antennapod.activity.OnlineFeedViewActivity; @@ -27,11 +34,28 @@ public class AddFeedFragment extends Fragment { */ private static final String ARG_FEED_URL = "feedurl"; + private EditText combinedFeedSearchBox; + private MainActivity activity; + @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { super.onCreateView(inflater, container, savedInstanceState); View root = inflater.inflate(R.layout.addfeed, container, false); + activity = (MainActivity) getActivity(); + activity.getSupportActionBar().setTitle(R.string.add_feed_label); + + setupAdvancedSearchButtons(root); + setupSeachBox(root); + + View butOpmlImport = root.findViewById(R.id.btn_opml_import); + butOpmlImport.setOnClickListener(v -> startActivity(new Intent(getActivity(), + OpmlImportFromPathActivity.class))); + + return root; + } + + private void setupSeachBox(View root) { final EditText etxtFeedurl = root.findViewById(R.id.etxtFeedurl); Bundle args = getArguments(); @@ -39,32 +63,69 @@ public class AddFeedFragment extends Fragment { etxtFeedurl.setText(args.getString(ARG_FEED_URL)); } - Button butSearchITunes = root.findViewById(R.id.butSearchItunes); - Button butBrowserGpoddernet = root.findViewById(R.id.butBrowseGpoddernet); - Button butSearchFyyd = root.findViewById(R.id.butSearchFyyd); - Button butOpmlImport = root.findViewById(R.id.butOpmlImport); - Button butConfirm = root.findViewById(R.id.butConfirm); + Button butConfirmAddUrl = root.findViewById(R.id.butConfirm); + butConfirmAddUrl.setOnClickListener(v -> { + addUrl(etxtFeedurl.getText().toString()); + }); - final MainActivity activity = (MainActivity) getActivity(); - activity.getSupportActionBar().setTitle(R.string.add_feed_label); + combinedFeedSearchBox = root.findViewById(R.id.combinedFeedSearchBox); + combinedFeedSearchBox.setOnEditorActionListener((v, actionId, event) -> { + if (actionId == EditorInfo.IME_ACTION_SEARCH) { + performSearch(); + return true; + } + return false; + }); + } - butSearchITunes.setOnClickListener(v -> activity.loadChildFragment(new ItunesSearchFragment())); + private void setupAdvancedSearchButtons(View root) { + View butAdvancedSearch = root.findViewById(R.id.advanced_search); + registerForContextMenu(butAdvancedSearch); + butAdvancedSearch.setOnClickListener(v -> butAdvancedSearch.showContextMenu()); + } - butBrowserGpoddernet.setOnClickListener(v -> activity.loadChildFragment(new GpodnetMainFragment())); + @Override + public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { + getActivity().getMenuInflater().inflate(R.menu.advanced_search, menu); + } - butSearchFyyd.setOnClickListener(v -> activity.loadChildFragment(new FyydSearchFragment())); + @Override + public boolean onContextItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.search_fyyd: + activity.loadChildFragment(new FyydSearchFragment()); + return true; + case R.id.search_gpodder: + activity.loadChildFragment(new GpodnetMainFragment()); + return true; + case R.id.search_itunes: + activity.loadChildFragment(new ItunesSearchFragment()); + return true; + } + return false; + } - butOpmlImport.setOnClickListener(v -> startActivity(new Intent(getActivity(), - OpmlImportFromPathActivity.class))); - butConfirm.setOnClickListener(v -> { - Intent intent = new Intent(getActivity(), OnlineFeedViewActivity.class); - intent.putExtra(OnlineFeedViewActivity.ARG_FEEDURL, etxtFeedurl.getText().toString()); - intent.putExtra(OnlineFeedViewActivity.ARG_TITLE, getString(R.string.add_feed_label)); - startActivity(intent); - }); + private void addUrl(String url) { + Intent intent = new Intent(getActivity(), OnlineFeedViewActivity.class); + intent.putExtra(OnlineFeedViewActivity.ARG_FEEDURL, url); + intent.putExtra(OnlineFeedViewActivity.ARG_TITLE, getString(R.string.add_feed_label)); + startActivity(intent); + } - return root; + private void performSearch() { + String query = combinedFeedSearchBox.getText().toString(); + + if (query.startsWith("http")) { + addUrl(query); + return; + } + + Bundle bundle = new Bundle(); + bundle.putString(CombinedSearchFragment.ARGUMENT_QUERY, query); + CombinedSearchFragment fragment = new CombinedSearchFragment(); + fragment.setArguments(bundle); + activity.loadChildFragment(fragment); } @Override diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/AllEpisodesFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/AllEpisodesFragment.java index ef522d3b3..62d798cf6 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/AllEpisodesFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/AllEpisodesFragment.java @@ -5,6 +5,7 @@ import android.content.DialogInterface; import android.content.SharedPreferences; import android.os.Bundle; import android.os.Handler; +import android.support.annotation.NonNull; import android.support.design.widget.Snackbar; import android.support.v4.app.Fragment; import android.support.v4.view.MenuItemCompat; @@ -24,12 +25,16 @@ import android.widget.Toast; import com.yqritc.recyclerviewflexibledivider.HorizontalDividerItemDecoration; +import org.greenrobot.eventbus.EventBus; +import org.greenrobot.eventbus.Subscribe; +import org.greenrobot.eventbus.ThreadMode; + +import java.util.ArrayList; import java.util.List; import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.MainActivity; import de.danoeh.antennapod.adapter.AllEpisodesRecycleAdapter; -import de.danoeh.antennapod.adapter.DefaultActionButtonCallback; import de.danoeh.antennapod.core.dialog.ConfirmationDialog; import de.danoeh.antennapod.core.event.DownloadEvent; import de.danoeh.antennapod.core.event.DownloaderUpdate; @@ -39,6 +44,7 @@ import de.danoeh.antennapod.core.feed.Feed; import de.danoeh.antennapod.core.feed.FeedItem; import de.danoeh.antennapod.core.feed.FeedMedia; import de.danoeh.antennapod.core.preferences.UserPreferences; +import de.danoeh.antennapod.core.service.download.DownloadRequest; import de.danoeh.antennapod.core.service.download.DownloadService; import de.danoeh.antennapod.core.service.download.Downloader; import de.danoeh.antennapod.core.storage.DBReader; @@ -49,7 +55,7 @@ import de.danoeh.antennapod.core.util.FeedItemUtil; import de.danoeh.antennapod.core.util.LongList; import de.danoeh.antennapod.menuhandler.FeedItemMenuHandler; import de.danoeh.antennapod.menuhandler.MenuItemUtils; -import de.greenrobot.event.EventBus; +import de.danoeh.antennapod.view.EmptyViewHandler; import io.reactivex.Observable; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.Disposable; @@ -74,12 +80,12 @@ public class AllEpisodesFragment extends Fragment { RecyclerView recyclerView; AllEpisodesRecycleAdapter listAdapter; private ProgressBar progLoading; + EmptyViewHandler emptyView; - List<FeedItem> episodes; - private List<Downloader> downloaderList; - - private boolean itemsLoaded = false; - private boolean viewsCreated = false; + @NonNull + List<FeedItem> episodes = new ArrayList<>(); + @NonNull + private List<Downloader> downloaderList = new ArrayList<>(); private boolean isUpdatingFeeds; boolean isMenuInvalidationAllowed = false; @@ -87,36 +93,32 @@ public class AllEpisodesFragment extends Fragment { Disposable disposable; private LinearLayoutManager layoutManager; - boolean showOnlyNewEpisodes() { return false; } - String getPrefName() { return DEFAULT_PREF_NAME; } + boolean showOnlyNewEpisodes() { + return false; + } - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setHasOptionsMenu(true); + String getPrefName() { + return DEFAULT_PREF_NAME; } @Override public void onStart() { super.onStart(); + setHasOptionsMenu(true); EventDistributor.getInstance().register(contentUpdate); - if (viewsCreated && itemsLoaded) { - onFragmentLoaded(); - } + EventBus.getDefault().register(this); + loadItems(); } @Override public void onResume() { super.onResume(); - EventBus.getDefault().registerSticky(this); - loadItems(); registerForContextMenu(recyclerView); } @Override public void onPause() { super.onPause(); - EventBus.getDefault().unregister(this); saveScrollPosition(); unregisterForContextMenu(recyclerView); } @@ -124,23 +126,18 @@ public class AllEpisodesFragment extends Fragment { @Override public void onStop() { super.onStop(); + EventBus.getDefault().unregister(this); EventDistributor.getInstance().unregister(contentUpdate); if (disposable != null) { disposable.dispose(); } } - @Override - public void onDestroyView() { - super.onDestroyView(); - resetViewState(); - } - private void saveScrollPosition() { int firstItem = layoutManager.findFirstVisibleItemPosition(); View firstItemView = layoutManager.findViewByPosition(firstItem); float topOffset; - if(firstItemView == null) { + if (firstItemView == null) { topOffset = 0; } else { topOffset = firstItemView.getTop(); @@ -167,43 +164,35 @@ public class AllEpisodesFragment extends Fragment { } } - void resetViewState() { - viewsCreated = false; - listAdapter = null; - } - - private final MenuItemUtils.UpdateRefreshMenuItemChecker updateRefreshMenuItemChecker = () -> DownloadService.isRunning && DownloadRequester.getInstance().isDownloadingFeeds(); @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { - if(!isAdded()) { + if (!isAdded()) { return; } super.onCreateOptionsMenu(menu, inflater); - if (itemsLoaded) { - inflater.inflate(R.menu.episodes, menu); - - MenuItem searchItem = menu.findItem(R.id.action_search); - final SearchView sv = (SearchView) MenuItemCompat.getActionView(searchItem); - MenuItemUtils.adjustTextColor(getActivity(), sv); - sv.setQueryHint(getString(R.string.search_hint)); - sv.setOnQueryTextListener(new SearchView.OnQueryTextListener() { - @Override - public boolean onQueryTextSubmit(String s) { - sv.clearFocus(); - ((MainActivity) getActivity()).loadChildFragment(SearchFragment.newInstance(s)); - return true; - } + inflater.inflate(R.menu.episodes, menu); + + MenuItem searchItem = menu.findItem(R.id.action_search); + final SearchView sv = (SearchView) MenuItemCompat.getActionView(searchItem); + MenuItemUtils.adjustTextColor(getActivity(), sv); + sv.setQueryHint(getString(R.string.search_hint)); + sv.setOnQueryTextListener(new SearchView.OnQueryTextListener() { + @Override + public boolean onQueryTextSubmit(String s) { + sv.clearFocus(); + ((MainActivity) requireActivity()).loadChildFragment(SearchFragment.newInstance(s)); + return true; + } - @Override - public boolean onQueryTextChange(String s) { - return false; - } - }); - isUpdatingFeeds = MenuItemUtils.updateRefreshMenuItem(menu, R.id.refresh_item, updateRefreshMenuItemChecker); - } + @Override + public boolean onQueryTextChange(String s) { + return false; + } + }); + isUpdatingFeeds = MenuItemUtils.updateRefreshMenuItem(menu, R.id.refresh_item, updateRefreshMenuItemChecker); } @Override @@ -211,11 +200,11 @@ public class AllEpisodesFragment extends Fragment { super.onPrepareOptionsMenu(menu); MenuItem markAllRead = menu.findItem(R.id.mark_all_read_item); if (markAllRead != null) { - markAllRead.setVisible(!showOnlyNewEpisodes() && episodes != null && !episodes.isEmpty()); + markAllRead.setVisible(!showOnlyNewEpisodes() && !episodes.isEmpty()); } - MenuItem markAllSeen = menu.findItem(R.id.mark_all_seen_item); - if(markAllSeen != null) { - markAllSeen.setVisible(showOnlyNewEpisodes() && episodes != null && !episodes.isEmpty()); + MenuItem removeAllNewFlags = menu.findItem(R.id.remove_all_new_flags_item); + if (removeAllNewFlags != null) { + removeAllNewFlags.setVisible(showOnlyNewEpisodes() && !episodes.isEmpty()); } } @@ -243,19 +232,19 @@ public class AllEpisodesFragment extends Fragment { }; markAllReadConfirmationDialog.createNewDialog().show(); return true; - case R.id.mark_all_seen_item: - ConfirmationDialog markAllSeenConfirmationDialog = new ConfirmationDialog(getActivity(), - R.string.mark_all_seen_label, - R.string.mark_all_seen_confirmation_msg) { + case R.id.remove_all_new_flags_item: + ConfirmationDialog removeAllNewFlagsConfirmationDialog = new ConfirmationDialog(getActivity(), + R.string.remove_all_new_flags_label, + R.string.remove_all_new_flags_confirmation_msg) { @Override public void onConfirmButtonPressed(DialogInterface dialog) { dialog.dismiss(); - DBWriter.markNewItemsSeen(); - Toast.makeText(getActivity(), R.string.mark_all_seen_msg, Toast.LENGTH_SHORT).show(); + DBWriter.removeAllNewFlags(); + Toast.makeText(getActivity(), R.string.removed_all_new_flags_msg, Toast.LENGTH_SHORT).show(); } }; - markAllSeenConfirmationDialog.createNewDialog().show(); + removeAllNewFlagsConfirmationDialog.createNewDialog().show(); return true; default: return false; @@ -269,98 +258,102 @@ public class AllEpisodesFragment extends Fragment { @Override public boolean onContextItemSelected(MenuItem item) { Log.d(TAG, "onContextItemSelected() called with: " + "item = [" + item + "]"); - if(!isVisible()) { + if (!getUserVisibleHint()) { return false; } - if(item.getItemId() == R.id.share_item) { + if (!isVisible()) { + return false; + } + if (item.getItemId() == R.id.share_item) { return true; // avoids that the position is reset when we need it in the submenu } - FeedItem selectedItem = listAdapter.getSelectedItem(); - if (selectedItem == null) { - Log.i(TAG, "Selected item was null, ignoring selection"); + if (listAdapter.getSelectedItem() == null) { + Log.i(TAG, "Selected item or listAdapter was null, ignoring selection"); return super.onContextItemSelected(item); } + FeedItem selectedItem = listAdapter.getSelectedItem(); - // Mark as seen contains UI logic specific to All/New/FavoriteSegments, + // Remove new flag contains UI logic specific to All/New/FavoriteSegments, // e.g., Undo with Snackbar, // and is handled by this class rather than the generic FeedItemMenuHandler - // Undo is useful for Mark as seen, given there is no UI to undo it otherwise, + // Undo is useful for Remove new flag, given there is no UI to undo it otherwise, // i.e., there is context menu item for Mark as new - if (R.id.mark_as_seen_item == item.getItemId()) { - markItemAsSeenWithUndo(selectedItem); + if (R.id.remove_new_flag_item == item.getItemId()) { + removeNewFlagWithUndo(selectedItem); return true; } return FeedItemMenuHandler.onMenuItemClicked(getActivity(), item.getItemId(), selectedItem); } + @NonNull @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - return onCreateViewHelper(inflater, container, savedInstanceState, - R.layout.all_episodes_fragment); - } - - View onCreateViewHelper(LayoutInflater inflater, - ViewGroup container, - Bundle savedInstanceState, - int fragmentResource) { + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { super.onCreateView(inflater, container, savedInstanceState); + View root = inflater.inflate(R.layout.all_episodes_fragment, container, false); - View root = inflater.inflate(fragmentResource, container, false); - + layoutManager = new LinearLayoutManager(getActivity()); recyclerView = root.findViewById(android.R.id.list); + recyclerView.setLayoutManager(layoutManager); + recyclerView.setHasFixedSize(true); + recyclerView.addItemDecoration(new HorizontalDividerItemDecoration.Builder(getActivity()).build()); + recyclerView.setVisibility(View.GONE); + RecyclerView.ItemAnimator animator = recyclerView.getItemAnimator(); if (animator instanceof SimpleItemAnimator) { ((SimpleItemAnimator) animator).setSupportsChangeAnimations(false); } - layoutManager = new LinearLayoutManager(getActivity()); - recyclerView.setLayoutManager(layoutManager); - recyclerView.setHasFixedSize(true); - recyclerView.addItemDecoration(new HorizontalDividerItemDecoration.Builder(getActivity()).build()); progLoading = root.findViewById(R.id.progLoading); + progLoading.setVisibility(View.VISIBLE); - if (!itemsLoaded) { - progLoading.setVisibility(View.VISIBLE); - } + emptyView = new EmptyViewHandler(getContext()); + emptyView.attachToRecyclerView(recyclerView); + emptyView.setIcon(R.attr.feed); + emptyView.setTitle(R.string.no_all_episodes_head_label); + emptyView.setMessage(R.string.no_all_episodes_label); - viewsCreated = true; - - if (itemsLoaded) { - onFragmentLoaded(); - } + createRecycleAdapter(recyclerView, emptyView); + emptyView.hide(); return root; } - private void onFragmentLoaded() { - if (listAdapter == null) { - MainActivity mainActivity = (MainActivity) getActivity(); - listAdapter = new AllEpisodesRecycleAdapter(mainActivity, itemAccess, - new DefaultActionButtonCallback(mainActivity), showOnlyNewEpisodes()); - listAdapter.setHasStableIds(true); - recyclerView.setAdapter(listAdapter); - } + private void onFragmentLoaded(List<FeedItem> episodes) { + this.episodes = episodes; listAdapter.notifyDataSetChanged(); + + if (episodes.size() == 0) { + createRecycleAdapter(recyclerView, emptyView); + } + restoreScrollPosition(); - getActivity().supportInvalidateOptionsMenu(); - updateShowOnlyEpisodesListViewState(); + requireActivity().invalidateOptionsMenu(); + } + + /** + * Currently, we need to recreate the list adapter in order to be able to undo last item via the + * snackbar. See #3084 for details. + */ + private void createRecycleAdapter(RecyclerView recyclerView, EmptyViewHandler emptyViewHandler) { + MainActivity mainActivity = (MainActivity) getActivity(); + listAdapter = new AllEpisodesRecycleAdapter(mainActivity, itemAccess, showOnlyNewEpisodes()); + listAdapter.setHasStableIds(true); + recyclerView.setAdapter(listAdapter); + emptyViewHandler.updateAdapter(listAdapter); } private final AllEpisodesRecycleAdapter.ItemAccess itemAccess = new AllEpisodesRecycleAdapter.ItemAccess() { @Override public int getCount() { - if (episodes != null) { - return episodes.size(); - } - return 0; + return episodes.size(); } @Override public FeedItem getItem(int position) { - if (episodes != null && 0 <= position && position < episodes.size()) { + if (0 <= position && position < episodes.size()) { return episodes.get(position); } return null; @@ -368,11 +361,8 @@ public class AllEpisodesFragment extends Fragment { @Override public LongList getItemsIds() { - if(episodes == null) { - return new LongList(0); - } LongList ids = new LongList(episodes.size()); - for(FeedItem episode : episodes) { + for (FeedItem episode : episodes) { ids.add(episode.getId()); } return ids; @@ -380,12 +370,11 @@ public class AllEpisodesFragment extends Fragment { @Override public int getItemDownloadProgressPercent(FeedItem item) { - if (downloaderList != null) { - for (Downloader downloader : downloaderList) { - if (downloader.getDownloadRequest().getFeedfileType() == FeedMedia.FEEDFILETYPE_FEEDMEDIA - && downloader.getDownloadRequest().getFeedfileId() == item.getMedia().getId()) { - return downloader.getDownloadRequest().getProgressPercent(); - } + for (Downloader downloader : downloaderList) { + DownloadRequest downloadRequest = downloader.getDownloadRequest(); + if (downloadRequest.getFeedfileType() == FeedMedia.FEEDFILETYPE_FEEDMEDIA + && downloadRequest.getFeedfileId() == item.getMedia().getId()) { + return downloadRequest.getProgressPercent(); } } return 0; @@ -399,11 +388,8 @@ public class AllEpisodesFragment extends Fragment { @Override public LongList getQueueIds() { LongList queueIds = new LongList(); - if(episodes == null) { - return queueIds; - } - for(FeedItem item : episodes) { - if(item.isTagged(FeedItem.TAG_QUEUE)) { + for (FeedItem item : episodes) { + if (item.isTagged(FeedItem.TAG_QUEUE)) { queueIds.add(item.getId()); } } @@ -412,34 +398,39 @@ public class AllEpisodesFragment extends Fragment { }; + @Subscribe(threadMode = ThreadMode.MAIN) public void onEventMainThread(FeedItemEvent event) { Log.d(TAG, "onEventMainThread() called with: " + "event = [" + event + "]"); - if(episodes == null || listAdapter == null) { - return; - } - for(int i=0, size = event.items.size(); i < size; i++) { - FeedItem item = event.items.get(i); + for (FeedItem item : event.items) { int pos = FeedItemUtil.indexOfItemWithId(episodes, item.getId()); - if(pos >= 0) { + if (pos >= 0) { episodes.remove(pos); - episodes.add(pos, item); - listAdapter.notifyItemChanged(pos); + if (shouldUpdatedItemRemainInList(item)) { + episodes.add(pos, item); + listAdapter.notifyItemChanged(pos); + } else { + listAdapter.notifyItemRemoved(pos); + } } } } + protected boolean shouldUpdatedItemRemainInList(FeedItem item) { + return true; + } + @Subscribe(sticky = true, threadMode = ThreadMode.MAIN) public void onEventMainThread(DownloadEvent event) { Log.d(TAG, "onEventMainThread() called with: " + "event = [" + event + "]"); DownloaderUpdate update = event.update; downloaderList = update.downloaders; if (isMenuInvalidationAllowed && isUpdatingFeeds != update.feedIds.length > 0) { - getActivity().supportInvalidateOptionsMenu(); + requireActivity().invalidateOptionsMenu(); } - if(listAdapter != null && update.mediaIds.length > 0) { - for(long mediaId : update.mediaIds) { + if (update.mediaIds.length > 0) { + for (long mediaId : update.mediaIds) { int pos = FeedItemUtil.indexOfItemWithMediaId(episodes, mediaId); - if(pos >= 0) { + if (pos >= 0) { listAdapter.notifyItemChanged(pos); } } @@ -452,49 +443,36 @@ public class AllEpisodesFragment extends Fragment { if ((arg & EVENTS) != 0) { loadItems(); if (isUpdatingFeeds != updateRefreshMenuItemChecker.isRefreshing()) { - getActivity().supportInvalidateOptionsMenu(); + requireActivity().invalidateOptionsMenu(); } } } }; - private void updateShowOnlyEpisodesListViewState() { - } - void loadItems() { if (disposable != null) { disposable.dispose(); } - if (viewsCreated && !itemsLoaded) { - recyclerView.setVisibility(View.GONE); - progLoading.setVisibility(View.VISIBLE); - } disposable = Observable.fromCallable(this::loadData) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(data -> { - recyclerView.setVisibility(View.VISIBLE); progLoading.setVisibility(View.GONE); - if (data != null) { - episodes = data; - itemsLoaded = true; - if (viewsCreated) { - onFragmentLoaded(); - } - } + onFragmentLoaded(data); }, error -> Log.e(TAG, Log.getStackTraceString(error))); } + @NonNull List<FeedItem> loadData() { return DBReader.getRecentlyPublishedEpisodes(RECENT_EPISODES_LIMIT); } - void markItemAsSeenWithUndo(FeedItem item) { + void removeNewFlagWithUndo(FeedItem item) { if (item == null) { return; } - Log.d(TAG, "markItemAsSeenWithUndo(" + item.getId() + ")"); + Log.d(TAG, "removeNewFlagWithUndo(" + item.getId() + ")"); if (disposable != null) { disposable.dispose(); } @@ -503,14 +481,14 @@ public class AllEpisodesFragment extends Fragment { DBWriter.markItemPlayed(FeedItem.UNPLAYED, item.getId()); final Handler h = new Handler(getActivity().getMainLooper()); - final Runnable r = () -> { + final Runnable r = () -> { FeedMedia media = item.getMedia(); if (media != null && media.hasAlmostEnded() && UserPreferences.isAutoDelete()) { DBWriter.deleteFeedMediaOfItem(getActivity(), media.getId()); } }; - Snackbar snackbar = Snackbar.make(getView(), getString(R.string.marked_as_seen_label), + Snackbar snackbar = Snackbar.make(getView(), getString(R.string.removed_new_flag_label), Snackbar.LENGTH_LONG); snackbar.setAction(getString(R.string.undo), v -> { DBWriter.markItemPlayed(FeedItem.NEW, item.getId()); @@ -518,7 +496,6 @@ public class AllEpisodesFragment extends Fragment { h.removeCallbacks(r); }); snackbar.show(); - h.postDelayed(r, (int)Math.ceil(snackbar.getDuration() * 1.05f)); + h.postDelayed(r, (int) Math.ceil(snackbar.getDuration() * 1.05f)); } - } 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 4d34d076d..4bebfe4c9 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/ChaptersFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/ChaptersFragment.java @@ -6,28 +6,28 @@ import android.util.Log; import android.view.View; import android.widget.ListView; +import java.util.List; +import java.util.ListIterator; + import de.danoeh.antennapod.R; -import de.danoeh.antennapod.activity.MediaplayerInfoActivity.MediaplayerInfoContentFragment; import de.danoeh.antennapod.adapter.ChaptersListAdapter; import de.danoeh.antennapod.core.feed.Chapter; import de.danoeh.antennapod.core.util.playback.Playable; import de.danoeh.antennapod.core.util.playback.PlaybackController; +import de.danoeh.antennapod.view.EmptyViewHandler; +import io.reactivex.Maybe; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.Disposable; +import io.reactivex.schedulers.Schedulers; -public class ChaptersFragment extends ListFragment implements MediaplayerInfoContentFragment { - +public class ChaptersFragment extends ListFragment { private static final String TAG = "ChaptersFragment"; - - private Playable media; - private PlaybackController controller; - private ChaptersListAdapter adapter; + private PlaybackController controller; + private Disposable disposable; + private EmptyViewHandler emptyView; - public static ChaptersFragment newInstance(Playable media) { - ChaptersFragment f = new ChaptersFragment(); - f.media = media; - return f; - } @Override public void onViewCreated(View view, Bundle savedInstanceState) { @@ -38,11 +38,13 @@ public class ChaptersFragment extends ListFragment implements MediaplayerInfoCon final int vertPadding = getResources().getDimensionPixelSize(R.dimen.list_vertical_padding); lv.setPadding(0, vertPadding, 0, vertPadding); + emptyView = new EmptyViewHandler(getContext()); + emptyView.attachToListView(lv); + emptyView.setIcon(R.attr.ic_bookmark); + emptyView.setTitle(R.string.no_chapters_head_label); + emptyView.setMessage(R.string.no_chapters_label); + adapter = new ChaptersListAdapter(getActivity(), 0, pos -> { - if(controller == null) { - Log.d(TAG, "controller is null"); - return; - } Chapter chapter = (Chapter) getListAdapter().getItem(pos); controller.seekToChapter(chapter); }); @@ -50,42 +52,83 @@ public class ChaptersFragment extends ListFragment implements MediaplayerInfoCon } @Override - public void onResume() { - super.onResume(); - adapter.setMedia(media); - adapter.notifyDataSetChanged(); - if(media == null || media.getChapters() == null) { - setEmptyText(getString(R.string.no_chapters_label)); - } else { - setEmptyText(null); + public void onStart() { + super.onStart(); + controller = new PlaybackController(getActivity(), false) { + @Override + public boolean loadMediaInfo() { + ChaptersFragment.this.loadMediaInfo(); + return true; + } + + @Override + public void onPositionObserverUpdate() { + adapter.notifyDataSetChanged(); + } + }; + controller.init(); + + loadMediaInfo(); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + + if (disposable != null) { + disposable.dispose(); } } - public void onDestroy() { - super.onDestroy(); - adapter = null; + @Override + public void onStop() { + super.onStop(); + controller.release(); controller = null; } - @Override - public void onMediaChanged(Playable media) { - if(this.media == media) { - return; + private void scrollTo(int position) { + getListView().setSelection(position); + } + + private int getCurrentChapter(Playable media) { + int currentPosition = controller.getPosition(); + + List<Chapter> chapters = media.getChapters(); + for (final ListIterator<Chapter> it = chapters.listIterator(); it.hasNext(); ) { + Chapter chapter = it.next(); + if (chapter.getStart() > currentPosition) { + return it.previousIndex() - 1; + } } - this.media = media; + return chapters.size() - 1; + } + + private void loadMediaInfo() { + if (disposable != null) { + disposable.dispose(); + } + disposable = Maybe.create(emitter -> { + Playable media = controller.getMedia(); + if (media != null) { + emitter.onSuccess(media); + } else { + emitter.onComplete(); + } + }) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(media -> onMediaChanged((Playable) media), + error -> Log.e(TAG, Log.getStackTraceString(error))); + } + + private void onMediaChanged(Playable media) { if (adapter != null) { adapter.setMedia(media); adapter.notifyDataSetChanged(); - if(media == null || media.getChapters() == null || media.getChapters().size() == 0) { - setEmptyText(getString(R.string.no_items_label)); - } else { - setEmptyText(null); + if (media != null && media.getChapters() != null && media.getChapters().size() != 0) { + scrollTo(getCurrentChapter(media)); } } } - - public void setController(PlaybackController controller) { - this.controller = controller; - } - } diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/CombinedSearchFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/CombinedSearchFragment.java new file mode 100644 index 000000000..1d9020f0d --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/fragment/CombinedSearchFragment.java @@ -0,0 +1,173 @@ +package de.danoeh.antennapod.fragment; + +import android.content.Intent; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.support.v4.view.MenuItemCompat; +import android.support.v7.widget.SearchView; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.GridView; +import android.widget.ProgressBar; +import android.widget.TextView; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.activity.OnlineFeedViewActivity; +import de.danoeh.antennapod.adapter.itunes.ItunesAdapter; +import de.danoeh.antennapod.discovery.CombinedSearcher; +import de.danoeh.antennapod.discovery.PodcastSearchResult; +import de.danoeh.antennapod.menuhandler.MenuItemUtils; +import io.reactivex.disposables.Disposable; + +import java.util.ArrayList; +import java.util.List; + +public class CombinedSearchFragment extends Fragment { + + private static final String TAG = "CombinedSearchFragment"; + public static final String ARGUMENT_QUERY = "query"; + + /** + * Adapter responsible with the search results + */ + private ItunesAdapter adapter; + private GridView gridView; + private ProgressBar progressBar; + private TextView txtvError; + private Button butRetry; + private TextView txtvEmpty; + + /** + * List of podcasts retreived from the search + */ + private List<PodcastSearchResult> searchResults = new ArrayList<>(); + private Disposable disposable; + + /** + * Constructor + */ + public CombinedSearchFragment() { + // Required empty public constructor + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setHasOptionsMenu(true); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + // Inflate the layout for this fragment + View root = inflater.inflate(R.layout.fragment_itunes_search, container, false); + gridView = root.findViewById(R.id.gridView); + adapter = new ItunesAdapter(getActivity(), new ArrayList<>()); + gridView.setAdapter(adapter); + + //Show information about the podcast when the list item is clicked + gridView.setOnItemClickListener((parent, view1, position, id) -> { + PodcastSearchResult podcast = searchResults.get(position); + Intent intent = new Intent(getActivity(), OnlineFeedViewActivity.class); + intent.putExtra(OnlineFeedViewActivity.ARG_FEEDURL, podcast.feedUrl); + intent.putExtra(OnlineFeedViewActivity.ARG_TITLE, podcast.title); + startActivity(intent); + }); + progressBar = root.findViewById(R.id.progressBar); + txtvError = root.findViewById(R.id.txtvError); + butRetry = root.findViewById(R.id.butRetry); + txtvEmpty = root.findViewById(android.R.id.empty); + + return root; + } + + @Override + public void onDestroy() { + super.onDestroy(); + if (disposable != null) { + disposable.dispose(); + } + adapter = null; + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + super.onCreateOptionsMenu(menu, inflater); + inflater.inflate(R.menu.itunes_search, menu); + MenuItem searchItem = menu.findItem(R.id.action_search); + final SearchView sv = (SearchView) MenuItemCompat.getActionView(searchItem); + MenuItemUtils.adjustTextColor(getActivity(), sv); + sv.setQueryHint(getString(R.string.search_label)); + sv.setOnQueryTextListener(new SearchView.OnQueryTextListener() { + @Override + public boolean onQueryTextSubmit(String s) { + sv.clearFocus(); + search(s); + return true; + } + + @Override + public boolean onQueryTextChange(String s) { + return false; + } + }); + MenuItemCompat.setOnActionExpandListener(searchItem, new MenuItemCompat.OnActionExpandListener() { + @Override + public boolean onMenuItemActionExpand(MenuItem item) { + return true; + } + + @Override + public boolean onMenuItemActionCollapse(MenuItem item) { + getActivity().getSupportFragmentManager().popBackStack(); + return true; + } + }); + MenuItemCompat.expandActionView(searchItem); + + if (getArguments() != null && getArguments().getString(ARGUMENT_QUERY, null) != null) { + sv.setQuery(getArguments().getString(ARGUMENT_QUERY, null), true); + } + } + + private void search(String query) { + if (disposable != null) { + disposable.dispose(); + } + + showOnlyProgressBar(); + + CombinedSearcher searcher = new CombinedSearcher(getContext()); + disposable = searcher.search(query).subscribe(result -> { + searchResults = result; + progressBar.setVisibility(View.GONE); + + adapter.clear(); + adapter.addAll(searchResults); + adapter.notifyDataSetInvalidated(); + gridView.setVisibility(!searchResults.isEmpty() ? View.VISIBLE : View.GONE); + txtvEmpty.setVisibility(searchResults.isEmpty() ? View.VISIBLE : View.GONE); + + }, error -> { + Log.e(TAG, Log.getStackTraceString(error)); + progressBar.setVisibility(View.GONE); + txtvError.setText(error.toString()); + txtvError.setVisibility(View.VISIBLE); + butRetry.setOnClickListener(v -> search(query)); + butRetry.setVisibility(View.VISIBLE); + }); + } + + private void showOnlyProgressBar() { + gridView.setVisibility(View.GONE); + txtvError.setVisibility(View.GONE); + butRetry.setVisibility(View.GONE); + txtvEmpty.setVisibility(View.GONE); + progressBar.setVisibility(View.VISIBLE); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/CompletedDownloadsFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/CompletedDownloadsFragment.java index 4bba9b255..705151062 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/CompletedDownloadsFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/CompletedDownloadsFragment.java @@ -1,7 +1,7 @@ package de.danoeh.antennapod.fragment; -import android.content.Context; import android.os.Bundle; +import android.support.annotation.NonNull; import android.support.v4.app.ListFragment; import android.util.Log; import android.view.Menu; @@ -10,6 +10,7 @@ import android.view.MenuItem; import android.view.View; import android.widget.ListView; +import java.util.ArrayList; import java.util.List; import de.danoeh.antennapod.R; @@ -21,11 +22,15 @@ import de.danoeh.antennapod.core.storage.DBReader; import de.danoeh.antennapod.core.storage.DBWriter; import de.danoeh.antennapod.core.util.FeedItemUtil; import de.danoeh.antennapod.dialog.EpisodesApplyActionFragment; +import de.danoeh.antennapod.view.EmptyViewHandler; import io.reactivex.Observable; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.Disposable; import io.reactivex.schedulers.Schedulers; +import static de.danoeh.antennapod.dialog.EpisodesApplyActionFragment.ACTION_ADD_TO_QUEUE; +import static de.danoeh.antennapod.dialog.EpisodesApplyActionFragment.ACTION_DELETE; + /** * Displays all running downloads and provides a button to delete them */ @@ -37,24 +42,27 @@ public class CompletedDownloadsFragment extends ListFragment { EventDistributor.DOWNLOADLOG_UPDATE | EventDistributor.UNREAD_ITEMS_UPDATE; - private List<FeedItem> items; + private List<FeedItem> items = new ArrayList<>(); private DownloadedEpisodesListAdapter listAdapter; - - private boolean viewCreated = false; - private Disposable disposable; @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); + public void onViewCreated(@NonNull View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); setHasOptionsMenu(true); - loadItems(); + addVerticalPadding(); + addEmptyView(); + + listAdapter = new DownloadedEpisodesListAdapter(getActivity(), itemAccess); + setListAdapter(listAdapter); + setListShown(false); } @Override public void onStart() { super.onStart(); EventDistributor.getInstance().register(contentUpdate); + loadItems(); } @Override @@ -67,99 +75,54 @@ public class CompletedDownloadsFragment extends ListFragment { } @Override - public void onDetach() { - super.onDetach(); - if (disposable != null) { - disposable.dispose(); - } - } - - @Override - public void onDestroyView() { - super.onDestroyView(); - listAdapter = null; - viewCreated = false; - if (disposable != null) { - disposable.dispose(); - } - } - - @Override - public void onAttach(Context context) { - super.onAttach(context); - if (viewCreated && items != null) { - onFragmentLoaded(); - } - } - - @Override - public void onViewCreated(View view, Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - - // add padding - final ListView lv = getListView(); - lv.setClipToPadding(false); - final int vertPadding = getResources().getDimensionPixelSize(R.dimen.list_vertical_padding); - lv.setPadding(0, vertPadding, 0, vertPadding); - - viewCreated = true; - if (items != null && getActivity() != null) { - onFragmentLoaded(); - } - } - - @Override public void onListItemClick(ListView l, View v, int position, long id) { super.onListItemClick(l, v, position, id); position -= l.getHeaderViewsCount(); long[] ids = FeedItemUtil.getIds(items); - ((MainActivity) getActivity()).loadChildFragment(ItemFragment.newInstance(ids, position)); - } - - private void onFragmentLoaded() { - if (listAdapter == null) { - listAdapter = new DownloadedEpisodesListAdapter(getActivity(), itemAccess); - setListAdapter(listAdapter); - } - setListShown(true); - listAdapter.notifyDataSetChanged(); - getActivity().supportInvalidateOptionsMenu(); + ((MainActivity) requireActivity()).loadChildFragment(ItemFragment.newInstance(ids, position)); } @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { - if(!isAdded()) { - return; - } super.onCreateOptionsMenu(menu, inflater); - if(items != null) { - inflater.inflate(R.menu.downloads_completed, menu); - menu.findItem(R.id.episode_actions).setVisible(items.size() > 0); - } + inflater.inflate(R.menu.downloads_completed, menu); + menu.findItem(R.id.episode_actions).setVisible(items.size() > 0); } @Override public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case R.id.episode_actions: - EpisodesApplyActionFragment fragment = EpisodesApplyActionFragment - .newInstance(items, EpisodesApplyActionFragment.ACTION_REMOVE | EpisodesApplyActionFragment.ACTION_QUEUE); - ((MainActivity) getActivity()).loadChildFragment(fragment); - return true; - default: - return false; + if (item.getItemId() == R.id.episode_actions) { + ((MainActivity) requireActivity()) + .loadChildFragment(EpisodesApplyActionFragment.newInstance(items, ACTION_DELETE | ACTION_ADD_TO_QUEUE)); + return true; } + return false; + } + + private void addEmptyView() { + EmptyViewHandler emptyView = new EmptyViewHandler(getActivity()); + emptyView.setIcon(R.attr.av_download); + emptyView.setTitle(R.string.no_comp_downloads_head_label); + emptyView.setMessage(R.string.no_comp_downloads_label); + emptyView.attachToListView(getListView()); + } + + private void addVerticalPadding() { + final ListView lv = getListView(); + lv.setClipToPadding(false); + final int vertPadding = getResources().getDimensionPixelSize(R.dimen.list_vertical_padding); + lv.setPadding(0, vertPadding, 0, vertPadding); } private final DownloadedEpisodesListAdapter.ItemAccess itemAccess = new DownloadedEpisodesListAdapter.ItemAccess() { @Override public int getCount() { - return (items != null) ? items.size() : 0; + return items.size(); } @Override public FeedItem getItem(int position) { - if (items != null && 0 <= position && position < items.size()) { + if (0 <= position && position < items.size()) { return items.get(position); } else { return null; @@ -168,7 +131,7 @@ public class CompletedDownloadsFragment extends ListFragment { @Override public void onFeedItemSecondaryAction(FeedItem item) { - DBWriter.deleteFeedMediaOfItem(getActivity(), item.getMedia().getId()); + DBWriter.deleteFeedMediaOfItem(requireActivity(), item.getMedia().getId()); } }; @@ -185,18 +148,18 @@ public class CompletedDownloadsFragment extends ListFragment { if (disposable != null) { disposable.dispose(); } - if (items == null && viewCreated) { - setListShown(false); - } disposable = Observable.fromCallable(DBReader::getDownloadedItems) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(result -> { items = result; - if (viewCreated && getActivity() != null) { - onFragmentLoaded(); - } - }, error -> Log.e(TAG, Log.getStackTraceString(error))); + onItemsLoaded(); + }, error -> Log.e(TAG, Log.getStackTraceString(error))); } + private void onItemsLoaded() { + setListShown(true); + listAdapter.notifyDataSetChanged(); + requireActivity().invalidateOptionsMenu(); + } } diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/CoverFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/CoverFragment.java index 5a061c7e6..db9dd9530 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/CoverFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/CoverFragment.java @@ -1,6 +1,7 @@ package de.danoeh.antennapod.fragment; import android.os.Bundle; +import android.support.annotation.NonNull; import android.support.v4.app.Fragment; import android.util.Log; import android.view.LayoutInflater; @@ -13,74 +14,68 @@ import com.bumptech.glide.Glide; import com.bumptech.glide.request.RequestOptions; import de.danoeh.antennapod.R; -import de.danoeh.antennapod.activity.MediaplayerInfoActivity.MediaplayerInfoContentFragment; import de.danoeh.antennapod.core.glide.ApGlideSettings; import de.danoeh.antennapod.core.util.playback.Playable; +import de.danoeh.antennapod.core.util.playback.PlaybackController; +import io.reactivex.Maybe; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.Disposable; +import io.reactivex.schedulers.Schedulers; /** * Displays the cover and the title of a FeedItem. */ -public class CoverFragment extends Fragment implements MediaplayerInfoContentFragment { +public class CoverFragment extends Fragment { private static final String TAG = "CoverFragment"; - private Playable media; - private View root; private TextView txtvPodcastTitle; private TextView txtvEpisodeTitle; private ImageView imgvCover; - - public static CoverFragment newInstance(Playable item) { - CoverFragment f = new CoverFragment(); - f.media = item; - return f; - } - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - if (media == null) { - Log.e(TAG, TAG + " was called without media"); - } - } + private PlaybackController controller; + private Disposable disposable; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + setRetainInstance(true); root = inflater.inflate(R.layout.cover_fragment, container, false); txtvPodcastTitle = root.findViewById(R.id.txtvPodcastTitle); txtvEpisodeTitle = root.findViewById(R.id.txtvEpisodeTitle); imgvCover = root.findViewById(R.id.imgvCover); + imgvCover.setOnClickListener(v -> onPlayPause()); return root; } private void loadMediaInfo() { - if (media != null) { - txtvPodcastTitle.setText(media.getFeedTitle()); - txtvEpisodeTitle.setText(media.getEpisodeTitle()); - Glide.with(this) - .load(media.getImageLocation()) - .apply(new RequestOptions() - .diskCacheStrategy(ApGlideSettings.AP_DISK_CACHE_STRATEGY) - .dontAnimate() - .fitCenter()) - .into(imgvCover); - } else { - Log.w(TAG, "loadMediaInfo was called while media was null"); + if (disposable != null) { + disposable.dispose(); } + disposable = Maybe.create(emitter -> { + Playable media = controller.getMedia(); + if (media != null) { + emitter.onSuccess(media); + } else { + emitter.onComplete(); + } + }) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(media -> displayMediaInfo((Playable) media), + error -> Log.e(TAG, Log.getStackTraceString(error))); } - @Override - public void onStart() { - Log.d(TAG, "On Start"); - super.onStart(); - if (media != null) { - Log.d(TAG, "Loading media info"); - loadMediaInfo(); - } else { - Log.w(TAG, "Unable to load media info: media was null"); - } + private void displayMediaInfo(@NonNull Playable media) { + txtvPodcastTitle.setText(media.getFeedTitle()); + txtvEpisodeTitle.setText(media.getEpisodeTitle()); + Glide.with(this) + .load(media.getImageLocation()) + .apply(new RequestOptions() + .diskCacheStrategy(ApGlideSettings.AP_DISK_CACHE_STRATEGY) + .dontAnimate() + .fitCenter()) + .into(imgvCover); } @Override @@ -91,14 +86,40 @@ public class CoverFragment extends Fragment implements MediaplayerInfoContentFra } @Override - public void onMediaChanged(Playable media) { - if(this.media == media) { - return; - } - this.media = media; - if (isAdded()) { - loadMediaInfo(); + public void onStart() { + super.onStart(); + controller = new PlaybackController(getActivity(), false) { + @Override + public boolean loadMediaInfo() { + CoverFragment.this.loadMediaInfo(); + return true; + } + + }; + controller.init(); + loadMediaInfo(); + } + + @Override + public void onStop() { + super.onStop(); + controller.release(); + controller = null; + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + + if (disposable != null) { + disposable.dispose(); } } + void onPlayPause() { + if (controller == null) { + return; + } + controller.playPause(); + } } 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 5ab6bac63..26b115b4b 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/DownloadLogFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/DownloadLogFragment.java @@ -14,15 +14,18 @@ import android.view.View; import android.widget.ListView; import android.widget.TextView; +import java.util.ArrayList; 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.feed.Feed; import de.danoeh.antennapod.core.feed.FeedMedia; import de.danoeh.antennapod.core.service.download.DownloadStatus; import de.danoeh.antennapod.core.storage.DBReader; import de.danoeh.antennapod.core.storage.DBWriter; +import de.danoeh.antennapod.view.EmptyViewHandler; import io.reactivex.Observable; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.Disposable; @@ -35,18 +38,13 @@ public class DownloadLogFragment extends ListFragment { private static final String TAG = "DownloadLogFragment"; - private List<DownloadStatus> downloadLog; + private List<DownloadStatus> downloadLog = new ArrayList<>(); private DownloadLogAdapter adapter; - - private boolean viewsCreated = false; - private boolean itemsLoaded = false; - private Disposable disposable; @Override public void onStart() { super.onStart(); - setHasOptionsMenu(true); EventDistributor.getInstance().register(contentUpdate); loadItems(); } @@ -55,7 +53,7 @@ public class DownloadLogFragment extends ListFragment { public void onStop() { super.onStop(); EventDistributor.getInstance().unregister(contentUpdate); - if(disposable != null) { + if (disposable != null) { disposable.dispose(); } } @@ -63,6 +61,7 @@ public class DownloadLogFragment extends ListFragment { @Override public void onViewCreated(View view, Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); + setHasOptionsMenu(true); // add padding final ListView lv = getListView(); @@ -70,17 +69,17 @@ public class DownloadLogFragment extends ListFragment { final int vertPadding = getResources().getDimensionPixelSize(R.dimen.list_vertical_padding); lv.setPadding(0, vertPadding, 0, vertPadding); - viewsCreated = true; - if (itemsLoaded) { - onFragmentLoaded(); - } + EmptyViewHandler emptyView = new EmptyViewHandler(getActivity()); + emptyView.setIcon(R.attr.av_download); + emptyView.setTitle(R.string.no_log_downloads_head_label); + emptyView.setMessage(R.string.no_log_downloads_label); + emptyView.attachToListView(getListView()); + + adapter = new DownloadLogAdapter(getActivity(), itemAccess); + setListAdapter(adapter); } private void onFragmentLoaded() { - if (adapter == null) { - adapter = new DownloadLogAdapter(getActivity(), itemAccess); - setListAdapter(adapter); - } setListShown(true); adapter.notifyDataSetChanged(); getActivity().supportInvalidateOptionsMenu(); @@ -93,10 +92,18 @@ public class DownloadLogFragment extends ListFragment { DownloadStatus status = adapter.getItem(position); String url = "unknown"; String message = getString(R.string.download_successful); - FeedMedia media = DBReader.getFeedMedia(status.getFeedfileId()); - if (media != null) { - url = media.getDownload_url(); + if (status.getFeedfileType() == FeedMedia.FEEDFILETYPE_FEEDMEDIA) { + FeedMedia media = DBReader.getFeedMedia(status.getFeedfileId()); + if (media != null) { + url = media.getDownload_url(); + } + } else if (status.getFeedfileType() == Feed.FEEDFILETYPE_FEED) { + Feed feed = DBReader.getFeed(status.getFeedfileId()); + if (feed != null) { + url = feed.getDownload_url(); + } } + if (!status.isSuccessful()) { message = status.getReasonDetailed(); } @@ -113,12 +120,12 @@ public class DownloadLogFragment extends ListFragment { @Override public int getCount() { - return (downloadLog != null) ? downloadLog.size() : 0; + return downloadLog.size(); } @Override public DownloadStatus getItem(int position) { - if (downloadLog != null && 0 <= position && position < downloadLog.size()) { + if (0 <= position && position < downloadLog.size()) { return downloadLog.get(position); } else { return null; @@ -138,27 +145,23 @@ public class DownloadLogFragment extends ListFragment { @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { - if(!isAdded()) { + if (!isAdded()) { return; } super.onCreateOptionsMenu(menu, inflater); - if (itemsLoaded) { - 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(); - } + 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) { - MenuItem menuItem = menu.findItem(R.id.clear_history_item); - if(menuItem != null) { - menuItem.setVisible(downloadLog != null && !downloadLog.isEmpty()); - } + MenuItem menuItem = menu.findItem(R.id.clear_history_item); + if (menuItem != null) { + menuItem.setVisible(!downloadLog.isEmpty()); } } @@ -178,7 +181,7 @@ public class DownloadLogFragment extends ListFragment { } private void loadItems() { - if(disposable != null) { + if (disposable != null) { disposable.dispose(); } disposable = Observable.fromCallable(DBReader::getDownloadLog) @@ -187,12 +190,8 @@ public class DownloadLogFragment extends ListFragment { .subscribe(result -> { if (result != null) { downloadLog = result; - itemsLoaded = true; - if (viewsCreated) { - onFragmentLoaded(); - } + onFragmentLoaded(); } }, error -> Log.e(TAG, Log.getStackTraceString(error))); } - } diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/ExternalPlayerFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/ExternalPlayerFragment.java index de2f04590..348c73b92 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/ExternalPlayerFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/ExternalPlayerFragment.java @@ -18,6 +18,7 @@ import com.bumptech.glide.Glide; import com.bumptech.glide.request.RequestOptions; import de.danoeh.antennapod.R; +import de.danoeh.antennapod.core.event.ServiceEvent; import de.danoeh.antennapod.core.feed.MediaType; import de.danoeh.antennapod.core.glide.ApGlideSettings; import de.danoeh.antennapod.core.service.playback.PlaybackService; @@ -90,10 +91,6 @@ public class ExternalPlayerFragment extends Fragment { loadMediaInfo(); } - public void connectToPlaybackService() { - controller.init(); - } - private PlaybackController setupPlaybackController() { return new PlaybackController(getActivity(), true) { @@ -109,12 +106,12 @@ public class ExternalPlayerFragment extends Fragment { @Override public boolean loadMediaInfo() { - ExternalPlayerFragment fragment = ExternalPlayerFragment.this; - if (fragment != null) { - return fragment.loadMediaInfo(); - } else { - return false; - } + return ExternalPlayerFragment.this.loadMediaInfo(); + } + + @Override + public void setupGUI() { + ExternalPlayerFragment.this.loadMediaInfo(); } @Override @@ -133,17 +130,29 @@ public class ExternalPlayerFragment extends Fragment { public void onResume() { super.onResume(); onPositionObserverUpdate(); + } + @Override + public void onStart() { + super.onStart(); + controller = setupPlaybackController(); controller.init(); + loadMediaInfo(); } @Override - public void onDestroy() { - super.onDestroy(); - Log.d(TAG, "Fragment is about to be destroyed"); + public void onStop() { + super.onStop(); if (controller != null) { controller.release(); + controller = null; } + } + + @Override + public void onDestroy() { + super.onDestroy(); + Log.d(TAG, "Fragment is about to be destroyed"); if (disposable != null) { disposable.dispose(); } @@ -196,7 +205,8 @@ public class ExternalPlayerFragment extends Fragment { .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(media -> updateUi((Playable) media), - error -> Log.e(TAG, Log.getStackTraceString(error))); + error -> Log.e(TAG, Log.getStackTraceString(error)), + () -> fragmentLayout.setVisibility(View.GONE)); return true; } @@ -217,7 +227,7 @@ public class ExternalPlayerFragment extends Fragment { .into(imgvCover); fragmentLayout.setVisibility(View.VISIBLE); - if (controller.isPlayingVideoLocally()) { + if (controller != null && controller.isPlayingVideoLocally()) { butPlay.setVisibility(View.GONE); } else { butPlay.setVisibility(View.VISIBLE); @@ -232,7 +242,9 @@ public class ExternalPlayerFragment extends Fragment { } private void onPositionObserverUpdate() { - if (controller.getPosition() == PlaybackService.INVALID_TIME + if (controller == null) { + return; + } else if (controller.getPosition() == PlaybackService.INVALID_TIME || controller.getDuration() == PlaybackService.INVALID_TIME) { return; } diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/FavoriteEpisodesFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/FavoriteEpisodesFragment.java index 70f82c2ec..bb029b731 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/FavoriteEpisodesFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/FavoriteEpisodesFragment.java @@ -1,6 +1,7 @@ package de.danoeh.antennapod.fragment; import android.os.Bundle; +import android.support.annotation.NonNull; import android.support.design.widget.Snackbar; import android.support.v7.widget.RecyclerView; import android.support.v7.widget.helper.ItemTouchHelper; @@ -9,6 +10,8 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import org.greenrobot.eventbus.Subscribe; + import java.util.List; import de.danoeh.antennapod.R; @@ -18,38 +21,38 @@ import de.danoeh.antennapod.core.feed.FeedItem; import de.danoeh.antennapod.core.storage.DBReader; import de.danoeh.antennapod.core.storage.DBWriter; - /** * Like 'EpisodesFragment' except that it only shows favorite episodes and * supports swiping to remove from favorites. */ - public class FavoriteEpisodesFragment extends AllEpisodesFragment { private static final String TAG = "FavoriteEpisodesFrag"; - private static final String PREF_NAME = "PrefFavoriteEpisodesFragment"; @Override - protected boolean showOnlyNewEpisodes() { return true; } + protected boolean showOnlyNewEpisodes() { + return true; + } @Override - protected String getPrefName() { return PREF_NAME; } + protected String getPrefName() { + return PREF_NAME; + } + @Subscribe public void onEvent(FavoritesEvent event) { - Log.d(TAG, "onEvent() called with: " + "event = [" + event + "]"); + Log.d(TAG, String.format("onEvent() called with: event = [%s]", event)); loadItems(); } + @NonNull @Override - protected void resetViewState() { - super.resetViewState(); - } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - View root = super.onCreateViewHelper(inflater, container, savedInstanceState, - R.layout.all_episodes_fragment); + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View root = super.onCreateView(inflater, container, savedInstanceState); + emptyView.setIcon(R.attr.ic_unfav); + emptyView.setTitle(R.string.no_fav_episodes_head_label); + emptyView.setMessage(R.string.no_fav_episodes_label); ItemTouchHelper.SimpleCallback simpleItemTouchCallback = new ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT) { @Override @@ -59,8 +62,8 @@ public class FavoriteEpisodesFragment extends AllEpisodesFragment { @Override public void onSwiped(RecyclerView.ViewHolder viewHolder, int swipeDir) { - AllEpisodesRecycleAdapter.Holder holder = (AllEpisodesRecycleAdapter.Holder)viewHolder; - Log.d(TAG, "remove(" + holder.getItemId() + ")"); + AllEpisodesRecycleAdapter.Holder holder = (AllEpisodesRecycleAdapter.Holder) viewHolder; + Log.d(TAG, String.format("remove(%s)", holder.getItemId())); if (disposable != null) { disposable.dispose(); @@ -69,8 +72,7 @@ public class FavoriteEpisodesFragment extends AllEpisodesFragment { if (item != null) { DBWriter.removeFavoriteItem(item); - Snackbar snackbar = Snackbar.make(root, getString(R.string.removed_item), - Snackbar.LENGTH_LONG); + Snackbar snackbar = Snackbar.make(root, getString(R.string.removed_item), Snackbar.LENGTH_LONG); snackbar.setAction(getString(R.string.undo), v -> DBWriter.addFavoriteItem(item)); snackbar.show(); } @@ -82,6 +84,7 @@ public class FavoriteEpisodesFragment extends AllEpisodesFragment { return root; } + @NonNull @Override protected List<FeedItem> loadData() { return DBReader.getFavoriteItemsList(); diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/FeedSettingsFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/FeedSettingsFragment.java new file mode 100644 index 000000000..4fb3d90f5 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/fragment/FeedSettingsFragment.java @@ -0,0 +1,177 @@ +package de.danoeh.antennapod.fragment; + +import android.arch.lifecycle.ViewModelProviders; +import android.content.Context; +import android.content.DialogInterface; +import android.os.Bundle; +import android.support.v14.preference.SwitchPreference; +import android.support.v7.preference.ListPreference; +import android.support.v7.preference.PreferenceFragmentCompat; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.core.dialog.ConfirmationDialog; +import de.danoeh.antennapod.core.feed.Feed; +import de.danoeh.antennapod.core.feed.FeedFilter; +import de.danoeh.antennapod.core.feed.FeedPreferences; +import de.danoeh.antennapod.core.preferences.UserPreferences; +import de.danoeh.antennapod.core.storage.DBWriter; +import de.danoeh.antennapod.dialog.AuthenticationDialog; +import de.danoeh.antennapod.dialog.EpisodeFilterDialog; +import de.danoeh.antennapod.viewmodel.FeedSettingsViewModel; + +import static de.danoeh.antennapod.activity.FeedSettingsActivity.EXTRA_FEED_ID; + +public class FeedSettingsFragment extends PreferenceFragmentCompat { + private static final CharSequence PREF_EPISODE_FILTER = "episodeFilter"; + private Feed feed; + private FeedPreferences feedPreferences; + + @Override + public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + addPreferencesFromResource(R.xml.feed_settings); + + long feedId = getArguments().getLong(EXTRA_FEED_ID); + ViewModelProviders.of(getActivity()).get(FeedSettingsViewModel.class).getFeed(feedId) + .subscribe(result -> { + feed = result; + feedPreferences = feed.getPreferences(); + + setupAutoDownloadPreference(); + setupKeepUpdatedPreference(); + setupAutoDeletePreference(); + setupAuthentificationPreference(); + setupEpisodeFilterPreference(); + + updateAutoDeleteSummary(); + updateAutoDownloadEnabled(); + }).dispose(); + } + + private void setupEpisodeFilterPreference() { + findPreference(PREF_EPISODE_FILTER).setOnPreferenceClickListener(preference -> { + new EpisodeFilterDialog(getContext(), feedPreferences.getFilter()) { + @Override + protected void onConfirmed(FeedFilter filter) { + feedPreferences.setFilter(filter); + feed.savePreferences(); + } + }.show(); + return false; + }); + } + + private void setupAuthentificationPreference() { + findPreference("authentication").setOnPreferenceClickListener(preference -> { + new AuthenticationDialog(getContext(), + R.string.authentication_label, true, false, + feedPreferences.getUsername(), feedPreferences.getPassword()) { + @Override + protected void onConfirmed(String username, String password, boolean saveUsernamePassword) { + feedPreferences.setUsername(username); + feedPreferences.setPassword(password); + feed.savePreferences(); + } + }.show(); + return false; + }); + } + + private void setupAutoDeletePreference() { + ListPreference autoDeletePreference = (ListPreference) findPreference("autoDelete"); + autoDeletePreference.setOnPreferenceChangeListener((preference, newValue) -> { + switch ((String) newValue) { + case "global": + feedPreferences.setAutoDeleteAction(FeedPreferences.AutoDeleteAction.GLOBAL); + break; + case "always": + feedPreferences.setAutoDeleteAction(FeedPreferences.AutoDeleteAction.YES); + break; + case "never": + feedPreferences.setAutoDeleteAction(FeedPreferences.AutoDeleteAction.NO); + break; + } + feed.savePreferences(); + updateAutoDeleteSummary(); + return false; + }); + } + + private void updateAutoDeleteSummary() { + ListPreference autoDeletePreference = (ListPreference) findPreference("autoDelete"); + + switch (feedPreferences.getAutoDeleteAction()) { + case GLOBAL: + autoDeletePreference.setSummary(R.string.feed_auto_download_global); + autoDeletePreference.setValue("global"); + break; + case YES: + autoDeletePreference.setSummary(R.string.feed_auto_download_always); + autoDeletePreference.setValue("always"); + break; + case NO: + autoDeletePreference.setSummary(R.string.feed_auto_download_never); + autoDeletePreference.setValue("never"); + break; + } + } + + private void setupKeepUpdatedPreference() { + SwitchPreference pref = (SwitchPreference) findPreference("keepUpdated"); + + pref.setChecked(feedPreferences.getKeepUpdated()); + pref.setOnPreferenceChangeListener((preference, newValue) -> { + boolean checked = newValue == Boolean.TRUE; + feedPreferences.setKeepUpdated(checked); + feed.savePreferences(); + pref.setChecked(checked); + return false; + }); + } + + private void setupAutoDownloadPreference() { + SwitchPreference pref = (SwitchPreference) findPreference("autoDownload"); + + pref.setEnabled(UserPreferences.isEnableAutodownload()); + if (UserPreferences.isEnableAutodownload()) { + pref.setChecked(feedPreferences.getAutoDownload()); + } else { + pref.setChecked(false); + pref.setSummary(R.string.auto_download_disabled_globally); + } + + pref.setOnPreferenceChangeListener((preference, newValue) -> { + boolean checked = newValue == Boolean.TRUE; + + feedPreferences.setAutoDownload(checked); + feed.savePreferences(); + updateAutoDownloadEnabled(); + ApplyToEpisodesDialog dialog = new ApplyToEpisodesDialog(getActivity(), checked); + dialog.createNewDialog().show(); + pref.setChecked(checked); + return false; + }); + } + + private void updateAutoDownloadEnabled() { + if (feed != null && feed.getPreferences() != null) { + boolean enabled = feed.getPreferences().getAutoDownload() && UserPreferences.isEnableAutodownload(); + findPreference(PREF_EPISODE_FILTER).setEnabled(enabled); + } + } + + private class ApplyToEpisodesDialog extends ConfirmationDialog { + private final boolean autoDownload; + + ApplyToEpisodesDialog(Context context, boolean autoDownload) { + super(context, R.string.auto_download_apply_to_items_title, + R.string.auto_download_apply_to_items_message); + this.autoDownload = autoDownload; + setPositiveText(R.string.yes); + setNegativeText(R.string.no); + } + + @Override + public void onConfirmButtonPressed(DialogInterface dialog) { + DBWriter.setFeedsItemsAutoDownload(feed, autoDownload); + } + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/FyydSearchFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/FyydSearchFragment.java index dadc596e2..9c16cfe56 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/FyydSearchFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/FyydSearchFragment.java @@ -16,24 +16,16 @@ import android.widget.Button; import android.widget.GridView; import android.widget.ProgressBar; import android.widget.TextView; - -import java.util.ArrayList; -import java.util.List; - import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.OnlineFeedViewActivity; import de.danoeh.antennapod.adapter.itunes.ItunesAdapter; -import de.danoeh.antennapod.core.service.download.AntennapodHttpClient; +import de.danoeh.antennapod.discovery.FyydPodcastSearcher; +import de.danoeh.antennapod.discovery.PodcastSearchResult; import de.danoeh.antennapod.menuhandler.MenuItemUtils; -import de.mfietz.fyydlin.FyydClient; -import de.mfietz.fyydlin.FyydResponse; -import de.mfietz.fyydlin.SearchHit; -import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.Disposable; -import io.reactivex.schedulers.Schedulers; -import static de.danoeh.antennapod.adapter.itunes.ItunesAdapter.Podcast; -import static java.util.Collections.emptyList; +import java.util.ArrayList; +import java.util.List; public class FyydSearchFragment extends Fragment { @@ -49,12 +41,10 @@ public class FyydSearchFragment extends Fragment { private Button butRetry; private TextView txtvEmpty; - private final FyydClient client = new FyydClient(AntennapodHttpClient.getHttpClient()); - /** * List of podcasts retreived from the search */ - private List<Podcast> searchResults; + private List<PodcastSearchResult> searchResults; private Disposable disposable; /** @@ -81,7 +71,7 @@ public class FyydSearchFragment extends Fragment { //Show information about the podcast when the list item is clicked gridView.setOnItemClickListener((parent, view1, position, id) -> { - Podcast podcast = searchResults.get(position); + PodcastSearchResult podcast = searchResults.get(position); Intent intent = new Intent(getActivity(), OnlineFeedViewActivity.class); intent.putExtra(OnlineFeedViewActivity.ARG_FEEDURL, podcast.feedUrl); intent.putExtra(OnlineFeedViewActivity.ARG_TITLE, podcast.title); @@ -145,20 +135,26 @@ public class FyydSearchFragment extends Fragment { disposable.dispose(); } showOnlyProgressBar(); - disposable = client.searchPodcasts(query, 10) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(result -> { - progressBar.setVisibility(View.GONE); - processSearchResult(result); - }, error -> { - Log.e(TAG, Log.getStackTraceString(error)); - progressBar.setVisibility(View.GONE); - txtvError.setText(error.toString()); - txtvError.setVisibility(View.VISIBLE); - butRetry.setOnClickListener(v -> search(query)); - butRetry.setVisibility(View.VISIBLE); - }); + + FyydPodcastSearcher searcher = new FyydPodcastSearcher(); + disposable = searcher.search(query).subscribe(result -> { + searchResults = result; + progressBar.setVisibility(View.GONE); + + adapter.clear(); + adapter.addAll(searchResults); + adapter.notifyDataSetInvalidated(); + gridView.setVisibility(!searchResults.isEmpty() ? View.VISIBLE : View.GONE); + txtvEmpty.setVisibility(searchResults.isEmpty() ? View.VISIBLE : View.GONE); + + }, error -> { + Log.e(TAG, Log.getStackTraceString(error)); + progressBar.setVisibility(View.GONE); + txtvError.setText(error.toString()); + txtvError.setVisibility(View.VISIBLE); + butRetry.setOnClickListener(v -> search(query)); + butRetry.setVisibility(View.VISIBLE); + }); } private void showOnlyProgressBar() { @@ -168,25 +164,4 @@ public class FyydSearchFragment extends Fragment { txtvEmpty.setVisibility(View.GONE); progressBar.setVisibility(View.VISIBLE); } - - private void processSearchResult(FyydResponse response) { - adapter.clear(); - if (!response.getData().isEmpty()) { - adapter.clear(); - searchResults = new ArrayList<>(); - for (SearchHit searchHit : response.getData()) { - Podcast podcast = Podcast.fromSearch(searchHit); - searchResults.add(podcast); - } - } else { - searchResults = emptyList(); - } - for(Podcast podcast : searchResults) { - adapter.add(podcast); - } - adapter.notifyDataSetInvalidated(); - gridView.setVisibility(!searchResults.isEmpty() ? View.VISIBLE : View.GONE); - txtvEmpty.setVisibility(searchResults.isEmpty() ? View.VISIBLE : View.GONE); - } - } diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/ItemDescriptionFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/ItemDescriptionFragment.java index 4ee7a06ad..5cf2c5eeb 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/ItemDescriptionFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/ItemDescriptionFragment.java @@ -10,7 +10,9 @@ import android.content.SharedPreferences; import android.content.res.TypedArray; import android.graphics.Color; import android.net.Uri; +import android.os.Build; import android.os.Bundle; +import android.support.annotation.NonNull; import android.support.v4.app.Fragment; import android.util.Log; import android.view.ContextMenu; @@ -27,16 +29,11 @@ import android.widget.Toast; import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.MediaplayerInfoActivity; -import de.danoeh.antennapod.activity.MediaplayerInfoActivity.MediaplayerInfoContentFragment; -import de.danoeh.antennapod.core.feed.FeedItem; import de.danoeh.antennapod.core.preferences.UserPreferences; -import de.danoeh.antennapod.core.storage.DBReader; import de.danoeh.antennapod.core.util.Converter; import de.danoeh.antennapod.core.util.IntentUtils; import de.danoeh.antennapod.core.util.NetworkUtils; import de.danoeh.antennapod.core.util.ShareUtils; -import de.danoeh.antennapod.core.util.ShownotesProvider; -import de.danoeh.antennapod.core.util.playback.Playable; import de.danoeh.antennapod.core.util.playback.PlaybackController; import de.danoeh.antennapod.core.util.playback.Timeline; import io.reactivex.Observable; @@ -47,7 +44,7 @@ import io.reactivex.schedulers.Schedulers; /** * Displays the description of a Playable object in a Webview. */ -public class ItemDescriptionFragment extends Fragment implements MediaplayerInfoContentFragment { +public class ItemDescriptionFragment extends Fragment { private static final String TAG = "ItemDescriptionFragment"; @@ -55,58 +52,16 @@ public class ItemDescriptionFragment extends Fragment implements MediaplayerInfo private static final String PREF_SCROLL_Y = "prefScrollY"; private static final String PREF_PLAYABLE_ID = "prefPlayableId"; - private static final String ARG_PLAYABLE = "arg.playable"; - private static final String ARG_FEEDITEM_ID = "arg.feeditem"; - - private static final String ARG_SAVE_STATE = "arg.saveState"; - private static final String ARG_HIGHLIGHT_TIMECODES = "arg.highlightTimecodes"; - private WebView webvDescription; - - private ShownotesProvider shownotesProvider; - private Playable media; - private Disposable webViewLoader; + private PlaybackController controller; /** * URL that was selected via long-press. */ private String selectedURL; - /** - * True if Fragment should save its state (e.g. scrolling position) in a - * shared preference. - */ - private boolean saveState; - - /** - * True if Fragment should highlight timecodes (e.g. time codes in the HH:MM:SS format). - */ - private boolean highlightTimecodes; - - public static ItemDescriptionFragment newInstance(Playable media, - boolean saveState, - boolean highlightTimecodes) { - ItemDescriptionFragment f = new ItemDescriptionFragment(); - Bundle args = new Bundle(); - args.putParcelable(ARG_PLAYABLE, media); - args.putBoolean(ARG_SAVE_STATE, saveState); - args.putBoolean(ARG_HIGHLIGHT_TIMECODES, highlightTimecodes); - f.setArguments(args); - return f; - } - - public static ItemDescriptionFragment newInstance(FeedItem item, boolean saveState, boolean highlightTimecodes) { - ItemDescriptionFragment f = new ItemDescriptionFragment(); - Bundle args = new Bundle(); - args.putLong(ARG_FEEDITEM_ID, item.getId()); - args.putBoolean(ARG_SAVE_STATE, saveState); - args.putBoolean(ARG_HIGHLIGHT_TIMECODES, highlightTimecodes); - f.setArguments(args); - return f; - } - @SuppressLint("NewApi") @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -126,6 +81,10 @@ public class ItemDescriptionFragment extends Fragment implements MediaplayerInfo webvDescription.getSettings().setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK); // Use cached resources, even if they have expired } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + webvDescription.getSettings().setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW); + } + webvDescription.getSettings().setUseWideViewPort(false); webvDescription.getSettings().setLayoutAlgorithm(WebSettings.LayoutAlgorithm.SINGLE_COLUMN); webvDescription.getSettings().setLoadWithOverviewMode(true); @@ -175,39 +134,6 @@ public class ItemDescriptionFragment extends Fragment implements MediaplayerInfo } } - @SuppressLint("NewApi") - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - Log.d(TAG, "Creating fragment"); - Bundle args = getArguments(); - saveState = args.getBoolean(ARG_SAVE_STATE, false); - highlightTimecodes = args.getBoolean(ARG_HIGHLIGHT_TIMECODES, false); - } - - @Override - public void onViewCreated(View view, Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - Bundle args = getArguments(); - if (args.containsKey(ARG_PLAYABLE)) { - if (media == null) { - media = args.getParcelable(ARG_PLAYABLE); - shownotesProvider = media; - } - load(); - } else if (args.containsKey(ARG_FEEDITEM_ID)) { - long id = getArguments().getLong(ARG_FEEDITEM_ID); - Observable.defer(() -> Observable.just(DBReader.getFeedItem(id))) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(feedItem -> { - shownotesProvider = feedItem; - load(); - }, error -> Log.e(TAG, Log.getStackTraceString(error))); - } - } - - private final View.OnLongClickListener webViewLongClickListener = new View.OnLongClickListener() { @Override @@ -300,22 +226,20 @@ public class ItemDescriptionFragment extends Fragment implements MediaplayerInfo if(webViewLoader != null) { webViewLoader.dispose(); } - if(shownotesProvider == null) { - return; - } webViewLoader = Observable.fromCallable(this::loadData) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(data -> { - webvDescription.loadDataWithBaseURL(null, data, "text/html", + webvDescription.loadDataWithBaseURL("https://127.0.0.1", data, "text/html", "utf-8", "about:blank"); Log.d(TAG, "Webview loaded"); }, error -> Log.e(TAG, Log.getStackTraceString(error))); } + @NonNull private String loadData() { - Timeline timeline = new Timeline(getActivity(), shownotesProvider); - return timeline.processShownotes(highlightTimecodes); + Timeline timeline = new Timeline(getActivity(), controller.getMedia()); + return timeline.processShownotes(true); } @Override @@ -325,42 +249,38 @@ public class ItemDescriptionFragment extends Fragment implements MediaplayerInfo } private void savePreference() { - if (saveState) { - Log.d(TAG, "Saving preferences"); - SharedPreferences prefs = getActivity().getSharedPreferences(PREF, - Activity.MODE_PRIVATE); - SharedPreferences.Editor editor = prefs.edit(); - if (media != null && webvDescription != null) { - Log.d(TAG, "Saving scroll position: " + webvDescription.getScrollY()); - editor.putInt(PREF_SCROLL_Y, webvDescription.getScrollY()); - editor.putString(PREF_PLAYABLE_ID, media.getIdentifier() - .toString()); - } else { - Log.d(TAG, "savePreferences was called while media or webview was null"); - editor.putInt(PREF_SCROLL_Y, -1); - editor.putString(PREF_PLAYABLE_ID, ""); - } - editor.commit(); + Log.d(TAG, "Saving preferences"); + SharedPreferences prefs = getActivity().getSharedPreferences(PREF, + Activity.MODE_PRIVATE); + SharedPreferences.Editor editor = prefs.edit(); + if (controller != null && controller.getMedia() != null && webvDescription != null) { + Log.d(TAG, "Saving scroll position: " + webvDescription.getScrollY()); + editor.putInt(PREF_SCROLL_Y, webvDescription.getScrollY()); + editor.putString(PREF_PLAYABLE_ID, controller.getMedia().getIdentifier() + .toString()); + } else { + Log.d(TAG, "savePreferences was called while media or webview was null"); + editor.putInt(PREF_SCROLL_Y, -1); + editor.putString(PREF_PLAYABLE_ID, ""); } + editor.commit(); } private boolean restoreFromPreference() { - if (saveState) { - Log.d(TAG, "Restoring from preferences"); - Activity activity = getActivity(); - if (activity != null) { - SharedPreferences prefs = activity.getSharedPreferences( - PREF, Activity.MODE_PRIVATE); - String id = prefs.getString(PREF_PLAYABLE_ID, ""); - int scrollY = prefs.getInt(PREF_SCROLL_Y, -1); - if (scrollY != -1 && media != null - && id.equals(media.getIdentifier().toString()) - && webvDescription != null) { - Log.d(TAG, "Restored scroll Position: " + scrollY); - webvDescription.scrollTo(webvDescription.getScrollX(), - scrollY); - return true; - } + Log.d(TAG, "Restoring from preferences"); + Activity activity = getActivity(); + if (activity != null) { + SharedPreferences prefs = activity.getSharedPreferences( + PREF, Activity.MODE_PRIVATE); + String id = prefs.getString(PREF_PLAYABLE_ID, ""); + int scrollY = prefs.getInt(PREF_SCROLL_Y, -1); + if (controller != null && scrollY != -1 && controller.getMedia() != null + && id.equals(controller.getMedia().getIdentifier().toString()) + && webvDescription != null) { + Log.d(TAG, "Restored scroll Position: " + scrollY); + webvDescription.scrollTo(webvDescription.getScrollX(), + scrollY); + return true; } } return false; @@ -377,15 +297,27 @@ public class ItemDescriptionFragment extends Fragment implements MediaplayerInfo } @Override - public void onMediaChanged(Playable media) { - if(this.media == media) { - return; - } - this.media = media; - this.shownotesProvider = media; - if (webvDescription != null) { - load(); - } + public void onStart() { + super.onStart(); + controller = new PlaybackController(getActivity(), false) { + @Override + public boolean loadMediaInfo() { + if (getMedia() == null) { + return false; + } + load(); + return true; + } + + }; + controller.init(); + load(); } + @Override + public void onStop() { + super.onStop(); + controller.release(); + controller = null; + } } 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 bcca281d4..432ada44e 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/ItemFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/ItemFragment.java @@ -36,13 +36,16 @@ import com.joanzapata.iconify.Iconify; import com.joanzapata.iconify.widget.IconButton; import org.apache.commons.lang3.ArrayUtils; +import org.greenrobot.eventbus.EventBus; +import org.greenrobot.eventbus.Subscribe; +import org.greenrobot.eventbus.ThreadMode; import java.util.List; import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.CastEnabledActivity; import de.danoeh.antennapod.activity.MainActivity; -import de.danoeh.antennapod.adapter.DefaultActionButtonCallback; +import de.danoeh.antennapod.adapter.actionbutton.ItemActionButton; import de.danoeh.antennapod.core.event.DownloadEvent; import de.danoeh.antennapod.core.event.DownloaderUpdate; import de.danoeh.antennapod.core.event.FeedItemEvent; @@ -61,14 +64,12 @@ import de.danoeh.antennapod.core.util.Converter; import de.danoeh.antennapod.core.util.DateUtils; import de.danoeh.antennapod.core.util.Flavors; import de.danoeh.antennapod.core.util.IntentUtils; -import de.danoeh.antennapod.core.util.LongList; import de.danoeh.antennapod.core.util.NetworkUtils; import de.danoeh.antennapod.core.util.ShareUtils; import de.danoeh.antennapod.core.util.playback.Timeline; import de.danoeh.antennapod.menuhandler.FeedItemMenuHandler; import de.danoeh.antennapod.view.OnSwipeGesture; import de.danoeh.antennapod.view.SwipeGestureDetector; -import de.greenrobot.event.EventBus; import io.reactivex.Observable; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.Disposable; @@ -196,6 +197,9 @@ public class ItemFragment extends Fragment implements OnSwipeGesture { webvDescription.getSettings().setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK); // Use cached resources, even if they have expired } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + webvDescription.getSettings().setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW); + } webvDescription.getSettings().setUseWideViewPort(false); webvDescription.getSettings().setLayoutAlgorithm( WebSettings.LayoutAlgorithm.NARROW_COLUMNS); @@ -227,9 +231,9 @@ public class ItemFragment extends Fragment implements OnSwipeGesture { if (item == null) { return; } - DefaultActionButtonCallback actionButtonCallback = new DefaultActionButtonCallback(getActivity()); - actionButtonCallback.onActionButtonPressed(item, item.isTagged(FeedItem.TAG_QUEUE) ? - LongList.of(item.getId()) : new LongList(0)); + ItemActionButton actionButton = ItemActionButton.forItem(item, item.isTagged(FeedItem.TAG_QUEUE)); + actionButton.onClick(getActivity()); + FeedMedia media = item.getMedia(); if (media != null && media.isDownloaded()) { // playback was started, dialog should close itself @@ -262,14 +266,19 @@ public class ItemFragment extends Fragment implements OnSwipeGesture { @Override public void onActivityCreated(@Nullable Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); + } + + @Override + public void onStart() { + super.onStart(); + EventDistributor.getInstance().register(contentUpdate); + EventBus.getDefault().register(this); load(); } @Override public void onResume() { super.onResume(); - EventDistributor.getInstance().register(contentUpdate); - EventBus.getDefault().registerSticky(this); if(itemsLoaded) { progbarLoading.setVisibility(View.GONE); updateAppearance(); @@ -277,8 +286,8 @@ public class ItemFragment extends Fragment implements OnSwipeGesture { } @Override - public void onPause() { - super.onPause(); + public void onStop() { + super.onStop(); EventDistributor.getInstance().unregister(contentUpdate); EventBus.getDefault().unregister(this); } @@ -297,19 +306,20 @@ public class ItemFragment extends Fragment implements OnSwipeGesture { @Override public boolean onSwipeLeftToRight() { - Log.d(TAG, "onSwipeLeftToRight()"); - feedItemPos = feedItemPos - 1; - if(feedItemPos < 0) { - feedItemPos = feedItems.length - 1; - } - load(); - return true; + return swipeFeedItem(-1); } @Override public boolean onSwipeRightToLeft() { - Log.d(TAG, "onSwipeRightToLeft()"); - feedItemPos = (feedItemPos + 1) % feedItems.length; + return swipeFeedItem(+1); + } + + private boolean swipeFeedItem(int position) { + Log.d(TAG, String.format("onSwipe() shift: %s", position)); + feedItemPos = (feedItemPos + position) % feedItems.length; + if (feedItemPos < 0) { + feedItemPos = feedItems.length - 1; + } load(); return true; } @@ -358,7 +368,7 @@ public class ItemFragment extends Fragment implements OnSwipeGesture { private void onFragmentLoaded() { if (webviewData != null) { - webvDescription.loadDataWithBaseURL(null, webviewData, "text/html", "utf-8", "about:blank"); + webvDescription.loadDataWithBaseURL("https://127.0.0.1", webviewData, "text/html", "utf-8", "about:blank"); } updateAppearance(); } @@ -537,6 +547,7 @@ public class ItemFragment extends Fragment implements OnSwipeGesture { ((MainActivity)getActivity()).loadChildFragment(fragment); } + @Subscribe(threadMode = ThreadMode.MAIN) public void onEventMainThread(FeedItemEvent event) { Log.d(TAG, "onEventMainThread() called with: " + "event = [" + event + "]"); for(FeedItem item : event.items) { @@ -547,6 +558,7 @@ public class ItemFragment extends Fragment implements OnSwipeGesture { } } + @Subscribe(sticky = true, threadMode = ThreadMode.MAIN) public void onEventMainThread(DownloadEvent event) { Log.d(TAG, "onEventMainThread() called with: " + "event = [" + event + "]"); DownloaderUpdate update = event.update; @@ -588,10 +600,12 @@ public class ItemFragment extends Fragment implements OnSwipeGesture { }, error -> Log.e(TAG, Log.getStackTraceString(error))); } + @Nullable private FeedItem loadInBackground() { FeedItem feedItem = DBReader.getFeedItem(feedItems[feedItemPos]); - if (feedItem != null) { - Timeline t = new Timeline(getActivity(), feedItem); + Context context = getContext(); + if (feedItem != null && context != null) { + Timeline t = new Timeline(context, feedItem); webviewData = t.processShownotes(false); } return feedItem; 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 d9e318069..0c75af986 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/ItemlistFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/ItemlistFragment.java @@ -6,6 +6,7 @@ import android.content.DialogInterface; import android.content.Intent; import android.graphics.LightingColorFilter; import android.os.Bundle; +import android.support.annotation.NonNull; import android.support.v4.app.ListFragment; import android.support.v4.view.MenuItemCompat; import android.support.v7.widget.SearchView; @@ -25,11 +26,13 @@ import android.widget.TextView; import com.bumptech.glide.Glide; import com.bumptech.glide.request.RequestOptions; -import com.joanzapata.iconify.IconDrawable; import com.joanzapata.iconify.Iconify; import com.joanzapata.iconify.widget.IconTextView; import org.apache.commons.lang3.Validate; +import org.greenrobot.eventbus.EventBus; +import org.greenrobot.eventbus.Subscribe; +import org.greenrobot.eventbus.ThreadMode; import java.util.List; @@ -37,7 +40,6 @@ import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.FeedInfoActivity; import de.danoeh.antennapod.activity.FeedSettingsActivity; import de.danoeh.antennapod.activity.MainActivity; -import de.danoeh.antennapod.adapter.DefaultActionButtonCallback; import de.danoeh.antennapod.adapter.FeedItemlistAdapter; import de.danoeh.antennapod.core.asynctask.FeedRemover; import de.danoeh.antennapod.core.dialog.ConfirmationDialog; @@ -61,13 +63,13 @@ import de.danoeh.antennapod.core.storage.DownloadRequestException; import de.danoeh.antennapod.core.storage.DownloadRequester; import de.danoeh.antennapod.core.util.FeedItemUtil; import de.danoeh.antennapod.core.util.LongList; +import de.danoeh.antennapod.core.util.Optional; import de.danoeh.antennapod.core.util.gui.MoreContentListFooterUtil; import de.danoeh.antennapod.dialog.EpisodesApplyActionFragment; import de.danoeh.antennapod.dialog.RenameFeedDialog; import de.danoeh.antennapod.menuhandler.FeedItemMenuHandler; import de.danoeh.antennapod.menuhandler.FeedMenuHandler; import de.danoeh.antennapod.menuhandler.MenuItemUtils; -import de.greenrobot.event.EventBus; import io.reactivex.Observable; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.Disposable; @@ -94,8 +96,6 @@ public class ItemlistFragment extends ListFragment { private long feedID; private Feed feed; - private boolean itemsLoaded = false; - private boolean viewsCreated = false; private boolean headerCreated = false; private List<Downloader> downloaderList; @@ -103,7 +103,7 @@ public class ItemlistFragment extends ListFragment { private MoreContentListFooterUtil listFooter; private boolean isUpdatingFeed; - + private TextView txtvTitle; private IconTextView txtvFailure; private ImageView imgvBackground; @@ -142,24 +142,21 @@ public class ItemlistFragment extends ListFragment { @Override public void onStart() { super.onStart(); - if (viewsCreated && itemsLoaded) { - onFragmentLoaded(); - } + EventDistributor.getInstance().register(contentUpdate); + EventBus.getDefault().register(this); + loadItems(); } @Override public void onResume() { super.onResume(); - EventDistributor.getInstance().register(contentUpdate); - EventBus.getDefault().registerSticky(this); ((MainActivity)getActivity()).getSupportActionBar().setTitle(""); updateProgressBarVisibility(); - loadItems(); } @Override - public void onPause() { - super.onPause(); + public void onStop() { + super.onStop(); EventDistributor.getInstance().unregister(contentUpdate); EventBus.getDefault().unregister(this); if(disposable != null) { @@ -175,7 +172,6 @@ public class ItemlistFragment extends ListFragment { private void resetViewState() { adapter = null; - viewsCreated = false; listFooter = null; } @@ -188,45 +184,43 @@ public class ItemlistFragment extends ListFragment { @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { - if(!isAdded()) { + if (!isAdded()) { return; } super.onCreateOptionsMenu(menu, inflater); - if (itemsLoaded) { - FeedMenuHandler.onCreateOptionsMenu(inflater, menu); - - MenuItem searchItem = menu.findItem(R.id.action_search); - final SearchView sv = (SearchView) MenuItemCompat.getActionView(searchItem); - MenuItemUtils.adjustTextColor(getActivity(), sv); - sv.setQueryHint(getString(R.string.search_hint)); - sv.setOnQueryTextListener(new SearchView.OnQueryTextListener() { - @Override - public boolean onQueryTextSubmit(String s) { - sv.clearFocus(); - if (itemsLoaded) { - ((MainActivity) getActivity()).loadChildFragment(SearchFragment.newInstance(s, feed.getId())); - } - return true; - } + FeedMenuHandler.onCreateOptionsMenu(inflater, menu); - @Override - public boolean onQueryTextChange(String s) { - return false; + MenuItem searchItem = menu.findItem(R.id.action_search); + final SearchView sv = (SearchView) MenuItemCompat.getActionView(searchItem); + MenuItemUtils.adjustTextColor(getActivity(), sv); + sv.setQueryHint(getString(R.string.search_hint)); + sv.setOnQueryTextListener(new SearchView.OnQueryTextListener() { + @Override + public boolean onQueryTextSubmit(String s) { + sv.clearFocus(); + if (feed != null) { + ((MainActivity) getActivity()).loadChildFragment(SearchFragment.newInstance(s, feed.getId())); } - }); - if(feed == null || feed.getLink() == null) { - menu.findItem(R.id.share_link_item).setVisible(false); - menu.findItem(R.id.visit_website_item).setVisible(false); + return true; } - isUpdatingFeed = MenuItemUtils.updateRefreshMenuItem(menu, R.id.refresh_item, updateRefreshMenuItemChecker); + @Override + public boolean onQueryTextChange(String s) { + return false; + } + }); + if (feed == null || feed.getLink() == null) { + menu.findItem(R.id.share_link_item).setVisible(false); + menu.findItem(R.id.visit_website_item).setVisible(false); } + + isUpdatingFeed = MenuItemUtils.updateRefreshMenuItem(menu, R.id.refresh_item, updateRefreshMenuItemChecker); } @Override public void onPrepareOptionsMenu(Menu menu) { - if (itemsLoaded) { + if (feed != null) { FeedMenuHandler.onPrepareOptionsMenu(menu, feed); } } @@ -339,11 +333,6 @@ public class ItemlistFragment extends ListFragment { super.onViewCreated(view, savedInstanceState); registerForContextMenu(getListView()); - - viewsCreated = true; - if (itemsLoaded) { - onFragmentLoaded(); - } } @Override @@ -358,6 +347,7 @@ public class ItemlistFragment extends ListFragment { activity.getSupportActionBar().setTitle(feed.getTitle()); } + @Subscribe public void onEvent(FeedEvent event) { Log.d(TAG, "onEvent() called with: " + "event = [" + event + "]"); if(event.feedId == feedID) { @@ -365,6 +355,7 @@ public class ItemlistFragment extends ListFragment { } } + @Subscribe(threadMode = ThreadMode.MAIN) public void onEventMainThread(FeedItemEvent event) { Log.d(TAG, "onEventMainThread() called with: " + "event = [" + event + "]"); if(feed == null || feed.getItems() == null || adapter == null) { @@ -379,6 +370,7 @@ public class ItemlistFragment extends ListFragment { } } + @Subscribe(sticky = true, threadMode = ThreadMode.MAIN) public void onEventMainThread(DownloadEvent event) { Log.d(TAG, "onEventMainThread() called with: " + "event = [" + event + "]"); DownloaderUpdate update = event.update; @@ -422,7 +414,7 @@ public class ItemlistFragment extends ListFragment { setListAdapter(null); setupHeaderView(); setupFooterView(); - adapter = new FeedItemlistAdapter(getActivity(), itemAccess, new DefaultActionButtonCallback(getActivity()), false, true); + adapter = new FeedItemlistAdapter(getActivity(), itemAccess, false, true); setListAdapter(adapter); } refreshHeaderView(); @@ -498,7 +490,7 @@ public class ItemlistFragment extends ListFragment { butShowInfo.setOnClickListener(v -> showFeedInfo()); imgvCover.setOnClickListener(v -> showFeedInfo()); butShowSettings.setOnClickListener(v -> { - if (viewsCreated && itemsLoaded) { + if (feed != null) { Intent startIntent = new Intent(getActivity(), FeedSettingsActivity.class); startIntent.putExtra(FeedSettingsActivity.EXTRA_FEED_ID, feed.getId()); @@ -509,7 +501,7 @@ public class ItemlistFragment extends ListFragment { } private void showFeedInfo() { - if (viewsCreated && itemsLoaded) { + if (feed != null) { Intent startIntent = new Intent(getActivity(), FeedInfoActivity.class); startIntent.putExtra(FeedInfoActivity.EXTRA_FEED_ID, feed.getId()); @@ -618,24 +610,20 @@ public class ItemlistFragment extends ListFragment { .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(result -> { - if (result != null) { - feed = result; - itemsLoaded = true; - if (viewsCreated) { - onFragmentLoaded(); - } - } + feed = result.orElse(null); + onFragmentLoaded(); }, error -> Log.e(TAG, Log.getStackTraceString(error))); } - private Feed loadData() { + @NonNull + private Optional<Feed> loadData() { Feed feed = DBReader.getFeed(feedID); - DBReader.loadAdditionalFeedItemListData(feed.getItems()); - if(feed != null && feed.getItemFilter() != null) { + if (feed != null && feed.getItemFilter() != null) { + DBReader.loadAdditionalFeedItemListData(feed.getItems()); FeedItemFilter filter = feed.getItemFilter(); feed.setItems(filter.filter(feed.getItems())); } - return feed; + return Optional.ofNullable(feed); } } diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/ItunesSearchFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/ItunesSearchFragment.java index a0e2ca22a..80767bef2 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/ItunesSearchFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/ItunesSearchFragment.java @@ -5,6 +5,7 @@ import android.os.Bundle; import android.support.v4.app.Fragment; import android.support.v4.view.MenuItemCompat; import android.support.v7.widget.SearchView; +import android.support.annotation.NonNull; import android.util.Log; import android.view.LayoutInflater; import android.view.Menu; @@ -19,13 +20,14 @@ import android.widget.TextView; import com.afollestad.materialdialogs.MaterialDialog; +import de.danoeh.antennapod.discovery.ItunesPodcastSearcher; +import de.danoeh.antennapod.discovery.ItunesTopListLoader; +import de.danoeh.antennapod.discovery.PodcastSearchResult; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.io.IOException; -import java.io.UnsupportedEncodingException; -import java.net.URLEncoder; import java.util.ArrayList; import java.util.List; import java.util.Locale; @@ -45,15 +47,11 @@ import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; -import static de.danoeh.antennapod.adapter.itunes.ItunesAdapter.Podcast; - //Searches iTunes store for given string and displays results in a list public class ItunesSearchFragment extends Fragment { private static final String TAG = "ItunesSearchFragment"; - private static final String API_URL = "https://itunes.apple.com/search?media=podcast&term=%s"; - /** * Adapter responsible with the search results @@ -68,21 +66,21 @@ public class ItunesSearchFragment extends Fragment { /** * List of podcasts retreived from the search */ - private List<Podcast> searchResults; - private List<Podcast> topList; + private List<PodcastSearchResult> searchResults; + private List<PodcastSearchResult> topList; private Disposable disposable; /** * Replace adapter data with provided search results from SearchTask. * @param result List of Podcast objects containing search results */ - private void updateData(List<Podcast> result) { + private void updateData(List<PodcastSearchResult> result) { this.searchResults = result; adapter.clear(); if (result != null && result.size() > 0) { gridView.setVisibility(View.VISIBLE); txtvEmpty.setVisibility(View.GONE); - for (Podcast p : result) { + for (PodcastSearchResult p : result) { adapter.add(p); } adapter.notifyDataSetInvalidated(); @@ -106,7 +104,7 @@ public class ItunesSearchFragment extends Fragment { } @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { // Inflate the layout for this fragment View root = inflater.inflate(R.layout.fragment_itunes_search, container, false); @@ -116,59 +114,31 @@ public class ItunesSearchFragment extends Fragment { //Show information about the podcast when the list item is clicked gridView.setOnItemClickListener((parent, view1, position, id) -> { - Podcast podcast = searchResults.get(position); - if(podcast.feedUrl == null) { + PodcastSearchResult podcast = searchResults.get(position); + if (podcast.feedUrl == null) { return; } - if (!podcast.feedUrl.contains("itunes.apple.com")) { - Intent intent = new Intent(getActivity(), OnlineFeedViewActivity.class); - intent.putExtra(OnlineFeedViewActivity.ARG_FEEDURL, podcast.feedUrl); - intent.putExtra(OnlineFeedViewActivity.ARG_TITLE, "iTunes"); - startActivity(intent); - } else { - gridView.setVisibility(View.GONE); - progressBar.setVisibility(View.VISIBLE); - disposable = Single.create((SingleOnSubscribe<String>) emitter -> { - OkHttpClient client = AntennapodHttpClient.getHttpClient(); - Request.Builder httpReq = new Request.Builder() - .url(podcast.feedUrl) - .header("User-Agent", ClientConfig.USER_AGENT); - try { - Response response = client.newCall(httpReq.build()).execute(); - if (response.isSuccessful()) { - String resultString = response.body().string(); - JSONObject result = new JSONObject(resultString); - JSONObject results = result.getJSONArray("results").getJSONObject(0); - String feedUrl = results.getString("feedUrl"); - emitter.onSuccess(feedUrl); - } else { - String prefix = getString(R.string.error_msg_prefix); - emitter.onError(new IOException(prefix + response)); - } - } catch (IOException | JSONException e) { - emitter.onError(e); - } - }) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(feedUrl -> { - progressBar.setVisibility(View.GONE); - gridView.setVisibility(View.VISIBLE); - Intent intent = new Intent(getActivity(), OnlineFeedViewActivity.class); - intent.putExtra(OnlineFeedViewActivity.ARG_FEEDURL, feedUrl); - intent.putExtra(OnlineFeedViewActivity.ARG_TITLE, "iTunes"); - startActivity(intent); - }, error -> { - Log.e(TAG, Log.getStackTraceString(error)); - progressBar.setVisibility(View.GONE); - gridView.setVisibility(View.VISIBLE); - String prefix = getString(R.string.error_msg_prefix); - new MaterialDialog.Builder(getActivity()) - .content(prefix + " " + error.getMessage()) - .neutralText(android.R.string.ok) - .show(); - }); - } + gridView.setVisibility(View.GONE); + progressBar.setVisibility(View.VISIBLE); + ItunesTopListLoader loader = new ItunesTopListLoader(getContext()); + disposable = loader.getFeedUrl(podcast) + .subscribe(feedUrl -> { + progressBar.setVisibility(View.GONE); + gridView.setVisibility(View.VISIBLE); + Intent intent = new Intent(getActivity(), OnlineFeedViewActivity.class); + intent.putExtra(OnlineFeedViewActivity.ARG_FEEDURL, feedUrl); + intent.putExtra(OnlineFeedViewActivity.ARG_TITLE, "iTunes"); + startActivity(intent); + }, error -> { + Log.e(TAG, Log.getStackTraceString(error)); + progressBar.setVisibility(View.GONE); + gridView.setVisibility(View.VISIBLE); + String prefix = getString(R.string.error_msg_prefix); + new MaterialDialog.Builder(getActivity()) + .content(prefix + " " + error.getMessage()) + .neutralText(android.R.string.ok) + .show(); + }); }); progressBar = root.findViewById(R.id.progressBar); txtvError = root.findViewById(R.id.txtvError); @@ -236,47 +206,9 @@ public class ItunesSearchFragment extends Fragment { butRetry.setVisibility(View.GONE); txtvEmpty.setVisibility(View.GONE); progressBar.setVisibility(View.VISIBLE); - disposable = Single.create((SingleOnSubscribe<List<Podcast>>) emitter -> { - String lang = Locale.getDefault().getLanguage(); - String url = "https://itunes.apple.com/" + lang + "/rss/toppodcasts/limit=25/explicit=true/json"; - OkHttpClient client = AntennapodHttpClient.getHttpClient(); - Request.Builder httpReq = new Request.Builder() - .url(url) - .header("User-Agent", ClientConfig.USER_AGENT); - List<Podcast> results = new ArrayList<>(); - try { - Response response = client.newCall(httpReq.build()).execute(); - if(!response.isSuccessful()) { - // toplist for language does not exist, fall back to united states - url = "https://itunes.apple.com/us/rss/toppodcasts/limit=25/explicit=true/json"; - httpReq = new Request.Builder() - .url(url) - .header("User-Agent", ClientConfig.USER_AGENT); - response = client.newCall(httpReq.build()).execute(); - } - if(response.isSuccessful()) { - String resultString = response.body().string(); - JSONObject result = new JSONObject(resultString); - JSONObject feed = result.getJSONObject("feed"); - JSONArray entries = feed.getJSONArray("entry"); - for(int i=0; i < entries.length(); i++) { - JSONObject json = entries.getJSONObject(i); - Podcast podcast = Podcast.fromToplist(json); - results.add(podcast); - } - } - else { - String prefix = getString(R.string.error_msg_prefix); - emitter.onError(new IOException(prefix + response)); - } - } catch (IOException | JSONException e) { - emitter.onError(e); - } - emitter.onSuccess(results); - }) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) + ItunesTopListLoader loader = new ItunesTopListLoader(getContext()); + disposable = loader.loadToplist(25) .subscribe(podcasts -> { progressBar.setVisibility(View.GONE); topList = podcasts; @@ -300,61 +232,19 @@ public class ItunesSearchFragment extends Fragment { butRetry.setVisibility(View.GONE); txtvEmpty.setVisibility(View.GONE); progressBar.setVisibility(View.VISIBLE); - disposable = Single.create((SingleOnSubscribe<List<Podcast>>) subscriber -> { - String encodedQuery = null; - try { - encodedQuery = URLEncoder.encode(query, "UTF-8"); - } catch (UnsupportedEncodingException e) { - // this won't ever be thrown - } - if (encodedQuery == null) { - encodedQuery = query; // failsafe - } - - //Spaces in the query need to be replaced with '+' character. - String formattedUrl = String.format(API_URL, query).replace(' ', '+'); - OkHttpClient client = AntennapodHttpClient.getHttpClient(); - Request.Builder httpReq = new Request.Builder() - .url(formattedUrl) - .header("User-Agent", ClientConfig.USER_AGENT); - List<Podcast> podcasts = new ArrayList<>(); - try { - Response response = client.newCall(httpReq.build()).execute(); - - if(response.isSuccessful()) { - String resultString = response.body().string(); - 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 = Podcast.fromSearch(podcastJson); - podcasts.add(podcast); - } - } - else { - String prefix = getString(R.string.error_msg_prefix); - subscriber.onError(new IOException(prefix + response)); - } - } catch (IOException | JSONException e) { - subscriber.onError(e); - } - subscriber.onSuccess(podcasts); - }) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(podcasts -> { - progressBar.setVisibility(View.GONE); - updateData(podcasts); - }, error -> { - Log.e(TAG, Log.getStackTraceString(error)); - progressBar.setVisibility(View.GONE); - txtvError.setText(error.toString()); - txtvError.setVisibility(View.VISIBLE); - butRetry.setOnClickListener(v -> search(query)); - butRetry.setVisibility(View.VISIBLE); - }); + ItunesPodcastSearcher searcher = new ItunesPodcastSearcher(getContext()); + disposable = searcher.search(query).subscribe(podcasts -> { + progressBar.setVisibility(View.GONE); + updateData(podcasts); + }, error -> { + Log.e(TAG, Log.getStackTraceString(error)); + progressBar.setVisibility(View.GONE); + txtvError.setText(error.toString()); + txtvError.setVisibility(View.VISIBLE); + butRetry.setOnClickListener(v -> search(query)); + butRetry.setVisibility(View.VISIBLE); + }); } } 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 6695ba427..1bf907aee 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/NewEpisodesFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/NewEpisodesFragment.java @@ -1,9 +1,9 @@ package de.danoeh.antennapod.fragment; import android.os.Bundle; +import android.support.annotation.NonNull; import android.support.v7.widget.RecyclerView; import android.support.v7.widget.helper.ItemTouchHelper; -import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -12,53 +12,39 @@ import java.util.List; import de.danoeh.antennapod.R; import de.danoeh.antennapod.adapter.AllEpisodesRecycleAdapter; -import de.danoeh.antennapod.core.event.FeedItemEvent; import de.danoeh.antennapod.core.feed.FeedItem; import de.danoeh.antennapod.core.storage.DBReader; -import de.danoeh.antennapod.core.util.FeedItemUtil; - /** * Like 'EpisodesFragment' except that it only shows new episodes and * supports swiping to mark as read. */ - public class NewEpisodesFragment extends AllEpisodesFragment { public static final String TAG = "NewEpisodesFragment"; - private static final String PREF_NAME = "PrefNewEpisodesFragment"; @Override - protected boolean showOnlyNewEpisodes() { return true; } - - @Override - protected String getPrefName() { return PREF_NAME; } + protected boolean showOnlyNewEpisodes() { + return true; + } @Override - protected void resetViewState() { - super.resetViewState(); + protected String getPrefName() { + return PREF_NAME; } @Override - public void onEventMainThread(FeedItemEvent event) { - Log.d(TAG, "onEventMainThread() called with: " + "event = [" + event + "]"); - if(episodes == null) { - return; - } - for(FeedItem item : event.items) { - int pos = FeedItemUtil.indexOfItemWithId(episodes, item.getId()); - if(pos >= 0 && item.isTagged(FeedItem.TAG_QUEUE)) { - episodes.remove(pos); - listAdapter.notifyItemRemoved(pos); - } - } + protected boolean shouldUpdatedItemRemainInList(FeedItem item) { + return item.isNew(); } + @NonNull @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - View root = super.onCreateViewHelper(inflater, container, savedInstanceState, - R.layout.all_episodes_fragment); + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View root = super.onCreateView(inflater, container, savedInstanceState); + emptyView.setTitle(R.string.no_new_episodes_head_label); + emptyView.setMessage(R.string.no_new_episodes_label); ItemTouchHelper.SimpleCallback simpleItemTouchCallback = new ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT) { @Override @@ -68,8 +54,8 @@ public class NewEpisodesFragment extends AllEpisodesFragment { @Override public void onSwiped(RecyclerView.ViewHolder viewHolder, int swipeDir) { - AllEpisodesRecycleAdapter.Holder holder = (AllEpisodesRecycleAdapter.Holder)viewHolder; - markItemAsSeenWithUndo(holder.getFeedItem()); + AllEpisodesRecycleAdapter.Holder holder = (AllEpisodesRecycleAdapter.Holder) viewHolder; + removeNewFlagWithUndo(holder.getFeedItem()); } @Override @@ -86,6 +72,7 @@ public class NewEpisodesFragment extends AllEpisodesFragment { super.onSelectedChanged(viewHolder, actionState); } + @Override public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) { @@ -105,9 +92,9 @@ public class NewEpisodesFragment extends AllEpisodesFragment { return root; } + @NonNull @Override protected List<FeedItem> loadData() { return DBReader.getNewItemsList(); } - } 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 c2a9200c8..e2060481f 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/PlaybackHistoryFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/PlaybackHistoryFragment.java @@ -1,8 +1,8 @@ package de.danoeh.antennapod.fragment; -import android.content.Context; import android.content.res.TypedArray; import android.os.Bundle; +import android.support.annotation.NonNull; import android.support.v4.app.ListFragment; import android.support.v4.view.MenuItemCompat; import android.util.Log; @@ -12,11 +12,14 @@ import android.view.MenuItem; import android.view.View; import android.widget.ListView; +import org.greenrobot.eventbus.EventBus; +import org.greenrobot.eventbus.Subscribe; +import org.greenrobot.eventbus.ThreadMode; + import java.util.List; import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.MainActivity; -import de.danoeh.antennapod.adapter.DefaultActionButtonCallback; import de.danoeh.antennapod.adapter.FeedItemlistAdapter; import de.danoeh.antennapod.core.event.DownloadEvent; import de.danoeh.antennapod.core.event.DownloaderUpdate; @@ -29,7 +32,7 @@ import de.danoeh.antennapod.core.storage.DBReader; import de.danoeh.antennapod.core.storage.DBWriter; import de.danoeh.antennapod.core.util.FeedItemUtil; import de.danoeh.antennapod.core.util.LongList; -import de.greenrobot.event.EventBus; +import de.danoeh.antennapod.view.EmptyViewHandler; import io.reactivex.Observable; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.Disposable; @@ -44,23 +47,10 @@ public class PlaybackHistoryFragment extends ListFragment { private List<FeedItem> playbackHistory; private FeedItemlistAdapter adapter; - - private boolean itemsLoaded = false; - private boolean viewsCreated = false; - private List<Downloader> downloaderList; - private Disposable disposable; @Override - public void onAttach(Context context) { - super.onAttach(context); - if (viewsCreated && itemsLoaded) { - onFragmentLoaded(); - } - } - - @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setRetainInstance(true); @@ -77,63 +67,43 @@ public class PlaybackHistoryFragment extends ListFragment { final int vertPadding = getResources().getDimensionPixelSize(R.dimen.list_vertical_padding); lv.setPadding(0, vertPadding, 0, vertPadding); - viewsCreated = true; - if (itemsLoaded) { - onFragmentLoaded(); - } - } - + EmptyViewHandler emptyView = new EmptyViewHandler(getActivity()); + emptyView.setIcon(R.attr.ic_history); + emptyView.setTitle(R.string.no_history_head_label); + emptyView.setMessage(R.string.no_history_label); + emptyView.attachToListView(getListView()); - @Override - public void onResume() { - super.onResume(); - EventBus.getDefault().registerSticky(this); - loadItems(); + // played items shoudln't be transparent for this fragment since, *all* items + // in this fragment will, by definition, be played. So it serves no purpose and can make + // it harder to read. + adapter = new FeedItemlistAdapter(getActivity(), itemAccess, true, false); + setListAdapter(adapter); } @Override public void onStart() { super.onStart(); EventDistributor.getInstance().register(contentUpdate); - } - - @Override - public void onPause() { - super.onPause(); - EventBus.getDefault().unregister(this); + EventBus.getDefault().register(this); + loadItems(); } @Override public void onStop() { super.onStop(); + EventBus.getDefault().unregister(this); EventDistributor.getInstance().unregister(contentUpdate); - if(disposable != null) { + if (disposable != null) { disposable.dispose(); } } - @Override - public void onDetach() { - super.onDetach(); - if(disposable != null) { - disposable.dispose(); - } - } - - @Override - public void onDestroyView() { - super.onDestroyView(); - adapter = null; - viewsCreated = false; - } - + @Subscribe(sticky = true) public void onEvent(DownloadEvent event) { Log.d(TAG, "onEvent() called with: " + "event = [" + event + "]"); DownloaderUpdate update = event.update; downloaderList = update.downloaders; - if (adapter != null) { - adapter.notifyDataSetChanged(); - } + adapter.notifyDataSetChanged(); } @Override @@ -146,27 +116,23 @@ public class PlaybackHistoryFragment extends ListFragment { @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { - if(!isAdded()) { + if (!isAdded()) { return; } super.onCreateOptionsMenu(menu, inflater); - if (itemsLoaded) { - 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(); - } + 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) { - MenuItem menuItem = menu.findItem(R.id.clear_history_item); - if (menuItem != null) { - menuItem.setVisible(playbackHistory != null && !playbackHistory.isEmpty()); - } + MenuItem menuItem = menu.findItem(R.id.clear_history_item); + if (menuItem != null) { + menuItem.setVisible(playbackHistory != null && !playbackHistory.isEmpty()); } } @@ -185,6 +151,7 @@ public class PlaybackHistoryFragment extends ListFragment { } } + @Subscribe(threadMode = ThreadMode.MAIN) public void onEventMainThread(FeedItemEvent event) { Log.d(TAG, "onEventMainThread() called with: " + "event = [" + event + "]"); if(playbackHistory == null) { @@ -211,15 +178,6 @@ public class PlaybackHistoryFragment extends ListFragment { }; private void onFragmentLoaded() { - if (adapter == null) { - // played items shoudln't be transparent for this fragment since, *all* items - // in this fragment will, by definition, be played. So it serves no purpose and can make - // it harder to read. - adapter = new FeedItemlistAdapter(getActivity(), itemAccess, - new DefaultActionButtonCallback(getActivity()), true, false); - setListAdapter(adapter); - } - setListShown(true); adapter.notifyDataSetChanged(); getActivity().supportInvalidateOptionsMenu(); } @@ -278,18 +236,15 @@ public class PlaybackHistoryFragment extends ListFragment { .subscribe(result -> { if (result != null) { playbackHistory = result; - itemsLoaded = true; - if (viewsCreated) { - onFragmentLoaded(); - } + onFragmentLoaded(); } }, error -> Log.e(TAG, Log.getStackTraceString(error))); } + @NonNull private List<FeedItem> loadData() { List<FeedItem> history = DBReader.getPlaybackHistory(); DBReader.loadAdditionalFeedItemListData(history); return history; } - } 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 faeabf75c..0fe413954 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/QueueFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/QueueFragment.java @@ -28,7 +28,6 @@ import java.util.List; import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.MainActivity; -import de.danoeh.antennapod.adapter.DefaultActionButtonCallback; import de.danoeh.antennapod.adapter.QueueRecyclerAdapter; import de.danoeh.antennapod.core.dialog.ConfirmationDialog; import de.danoeh.antennapod.core.event.DownloadEvent; @@ -50,13 +49,22 @@ import de.danoeh.antennapod.core.util.Converter; import de.danoeh.antennapod.core.util.FeedItemUtil; import de.danoeh.antennapod.core.util.LongList; import de.danoeh.antennapod.core.util.QueueSorter; +import de.danoeh.antennapod.core.util.SortOrder; +import de.danoeh.antennapod.dialog.EpisodesApplyActionFragment; import de.danoeh.antennapod.menuhandler.FeedItemMenuHandler; import de.danoeh.antennapod.menuhandler.MenuItemUtils; -import de.greenrobot.event.EventBus; + +import de.danoeh.antennapod.view.EmptyViewHandler; import io.reactivex.Observable; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.Disposable; import io.reactivex.schedulers.Schedulers; +import org.greenrobot.eventbus.EventBus; +import org.greenrobot.eventbus.Subscribe; +import org.greenrobot.eventbus.ThreadMode; + +import static de.danoeh.antennapod.dialog.EpisodesApplyActionFragment.ACTION_DELETE; +import static de.danoeh.antennapod.dialog.EpisodesApplyActionFragment.ACTION_REMOVE_FROM_QUEUE; /** * Shows all items in the queue @@ -72,7 +80,7 @@ public class QueueFragment extends Fragment { private TextView infoBar; private RecyclerView recyclerView; private QueueRecyclerAdapter recyclerAdapter; - private TextView txtvEmpty; + private EmptyViewHandler emptyView; private ProgressBar progLoading; private List<FeedItem> queue; @@ -102,20 +110,20 @@ public class QueueFragment extends Fragment { if (queue != null) { onFragmentLoaded(true); } - } - - @Override - public void onResume() { - super.onResume(); loadItems(true); EventDistributor.getInstance().register(contentUpdate); - EventBus.getDefault().registerSticky(this); + EventBus.getDefault().register(this); } @Override public void onPause() { super.onPause(); saveScrollPosition(); + } + + @Override + public void onStop() { + super.onStop(); EventDistributor.getInstance().unregister(contentUpdate); EventBus.getDefault().unregister(this); if(disposable != null) { @@ -123,9 +131,13 @@ public class QueueFragment extends Fragment { } } + @Subscribe(threadMode = ThreadMode.MAIN) public void onEventMainThread(QueueEvent event) { Log.d(TAG, "onEventMainThread() called with: " + "event = [" + event + "]"); - if(queue == null || recyclerAdapter == null) { + if (queue == null) { + return; + } else if (recyclerAdapter == null) { + loadItems(true); return; } switch(event.action) { @@ -158,9 +170,13 @@ public class QueueFragment extends Fragment { onFragmentLoaded(false); } + @Subscribe(threadMode = ThreadMode.MAIN) public void onEventMainThread(FeedItemEvent event) { Log.d(TAG, "onEventMainThread() called with: " + "event = [" + event + "]"); - if(queue == null || recyclerAdapter == null) { + if (queue == null) { + return; + } else if (recyclerAdapter == null) { + loadItems(true); return; } for(int i=0, size = event.items.size(); i < size; i++) { @@ -170,10 +186,12 @@ public class QueueFragment extends Fragment { queue.remove(pos); queue.add(pos, item); recyclerAdapter.notifyItemChanged(pos); + refreshInfoBar(); } } } + @Subscribe(sticky = true, threadMode = ThreadMode.MAIN) public void onEventMainThread(DownloadEvent event) { Log.d(TAG, "onEventMainThread() called with: " + "event = [" + event + "]"); DownloaderUpdate update = event.update; @@ -259,6 +277,19 @@ public class QueueFragment extends Fragment { MenuItemUtils.refreshLockItem(getActivity(), menu); + // Show Lock Item only if queue is sorted manually + boolean keepSorted = UserPreferences.isQueueKeepSorted(); + MenuItem lockItem = menu.findItem(R.id.queue_lock); + lockItem.setVisible(!keepSorted); + + // Random sort is not supported in keep sorted mode + MenuItem sortRandomItem = menu.findItem(R.id.queue_sort_random); + sortRandomItem.setVisible(!keepSorted); + + // Set keep sorted checkbox + MenuItem keepSortedItem = menu.findItem(R.id.queue_keep_sorted); + keepSortedItem.setChecked(keepSorted); + isUpdatingFeeds = MenuItemUtils.updateRefreshMenuItem(menu, R.id.refresh_item, updateRefreshMenuItemChecker); } } @@ -271,7 +302,9 @@ public class QueueFragment extends Fragment { boolean newLockState = !UserPreferences.isQueueLocked(); UserPreferences.setQueueLocked(newLockState); getActivity().supportInvalidateOptionsMenu(); - recyclerAdapter.setLocked(newLockState); + if (recyclerAdapter != null) { + recyclerAdapter.setLocked(newLockState); + } if (newLockState) { Snackbar.make(getActivity().findViewById(R.id.content), R.string .queue_locked, Snackbar.LENGTH_SHORT).show(); @@ -301,38 +334,55 @@ public class QueueFragment extends Fragment { }; conDialog.createNewDialog().show(); return true; + case R.id.episode_actions: + ((MainActivity) requireActivity()) .loadChildFragment( + EpisodesApplyActionFragment.newInstance(queue, ACTION_DELETE | ACTION_REMOVE_FROM_QUEUE)); + return true; case R.id.queue_sort_episode_title_asc: - QueueSorter.sort(getActivity(), QueueSorter.Rule.EPISODE_TITLE_ASC, true); + setSortOrder(SortOrder.EPISODE_TITLE_A_Z); return true; case R.id.queue_sort_episode_title_desc: - QueueSorter.sort(getActivity(), QueueSorter.Rule.EPISODE_TITLE_DESC, true); + setSortOrder(SortOrder.EPISODE_TITLE_Z_A); return true; case R.id.queue_sort_date_asc: - QueueSorter.sort(getActivity(), QueueSorter.Rule.DATE_ASC, true); + setSortOrder(SortOrder.DATE_OLD_NEW); return true; case R.id.queue_sort_date_desc: - QueueSorter.sort(getActivity(), QueueSorter.Rule.DATE_DESC, true); + setSortOrder(SortOrder.DATE_NEW_OLD); return true; case R.id.queue_sort_duration_asc: - QueueSorter.sort(getActivity(), QueueSorter.Rule.DURATION_ASC, true); + setSortOrder(SortOrder.DURATION_SHORT_LONG); return true; case R.id.queue_sort_duration_desc: - QueueSorter.sort(getActivity(), QueueSorter.Rule.DURATION_DESC, true); + setSortOrder(SortOrder.DURATION_LONG_SHORT); return true; case R.id.queue_sort_feed_title_asc: - QueueSorter.sort(getActivity(), QueueSorter.Rule.FEED_TITLE_ASC, true); + setSortOrder(SortOrder.FEED_TITLE_A_Z); return true; case R.id.queue_sort_feed_title_desc: - QueueSorter.sort(getActivity(), QueueSorter.Rule.FEED_TITLE_DESC, true); + setSortOrder(SortOrder.FEED_TITLE_Z_A); return true; case R.id.queue_sort_random: - QueueSorter.sort(getActivity(), QueueSorter.Rule.RANDOM, true); + setSortOrder(SortOrder.RANDOM); return true; case R.id.queue_sort_smart_shuffle_asc: - QueueSorter.sort(getActivity(), QueueSorter.Rule.SMART_SHUFFLE_ASC, true); + setSortOrder(SortOrder.SMART_SHUFFLE_OLD_NEW); return true; case R.id.queue_sort_smart_shuffle_desc: - QueueSorter.sort(getActivity(), QueueSorter.Rule.SMART_SHUFFLE_DESC, true); + setSortOrder(SortOrder.SMART_SHUFFLE_NEW_OLD); + return true; + case R.id.queue_keep_sorted: + boolean keepSortedOld = UserPreferences.isQueueKeepSorted(); + boolean keepSortedNew = !keepSortedOld; + UserPreferences.setQueueKeepSorted(keepSortedNew); + if (keepSortedNew) { + SortOrder sortOrder = UserPreferences.getQueueKeepSortedOrder(); + QueueSorter.sort(sortOrder, true); + recyclerAdapter.setLocked(true); + } else { + recyclerAdapter.setLocked(UserPreferences.isQueueLocked()); + } + getActivity().invalidateOptionsMenu(); return true; default: return false; @@ -342,6 +392,15 @@ public class QueueFragment extends Fragment { } } + /** + * This method is called if the user clicks on a sort order menu item. + * + * @param sortOrder New sort order. + */ + private void setSortOrder(SortOrder sortOrder) { + UserPreferences.setQueueKeepSortedOrder(sortOrder); + QueueSorter.sort(sortOrder, true); + } @Override public boolean onContextItemSelected(MenuItem item) { @@ -431,7 +490,7 @@ public class QueueFragment extends Fragment { final FeedItem item = queue.get(position); final boolean isRead = item.isPlayed(); DBWriter.markItemPlayed(FeedItem.PLAYED, false, item.getId()); - DBWriter.removeQueueItem(getActivity(), item, true); + DBWriter.removeQueueItem(getActivity(), true, item); Snackbar snackbar = Snackbar.make(root, getString(R.string.marked_as_read_label), Snackbar.LENGTH_LONG); snackbar.setAction(getString(R.string.undo), v -> { DBWriter.addQueueItemAt(getActivity(), item.getId(), position, false); @@ -494,8 +553,12 @@ public class QueueFragment extends Fragment { ); itemTouchHelper.attachToRecyclerView(recyclerView); - txtvEmpty = root.findViewById(android.R.id.empty); - txtvEmpty.setVisibility(View.GONE); + emptyView = new EmptyViewHandler(getContext()); + emptyView.attachToRecyclerView(recyclerView); + emptyView.setIcon(R.attr.stat_playlist); + emptyView.setTitle(R.string.no_items_header_label); + emptyView.setMessage(R.string.no_items_label); + progLoading = root.findViewById(R.id.progLoading); progLoading.setVisibility(View.VISIBLE); @@ -503,19 +566,19 @@ public class QueueFragment extends Fragment { } private void onFragmentLoaded(final boolean restoreScrollPosition) { - if (recyclerAdapter == null) { - MainActivity activity = (MainActivity) getActivity(); - recyclerAdapter = new QueueRecyclerAdapter(activity, itemAccess, - new DefaultActionButtonCallback(activity), itemTouchHelper); - recyclerAdapter.setHasStableIds(true); - recyclerView.setAdapter(recyclerAdapter); - } - if(queue == null || queue.size() == 0) { - recyclerView.setVisibility(View.GONE); - txtvEmpty.setVisibility(View.VISIBLE); - } else { - txtvEmpty.setVisibility(View.GONE); + if (queue != null && queue.size() > 0) { + if (recyclerAdapter == null) { + MainActivity activity = (MainActivity) getActivity(); + recyclerAdapter = new QueueRecyclerAdapter(activity, itemAccess, itemTouchHelper); + recyclerAdapter.setHasStableIds(true); + recyclerView.setAdapter(recyclerAdapter); + emptyView.updateAdapter(recyclerAdapter); + } recyclerView.setVisibility(View.VISIBLE); + } else { + recyclerAdapter = null; + recyclerView.setVisibility(View.GONE); + emptyView.updateAdapter(recyclerAdapter); } if (restoreScrollPosition) { @@ -628,22 +691,19 @@ public class QueueFragment extends Fragment { } if (queue == null) { recyclerView.setVisibility(View.GONE); - txtvEmpty.setVisibility(View.GONE); + emptyView.hide(); progLoading.setVisibility(View.VISIBLE); } disposable = Observable.fromCallable(DBReader::getQueue) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(items -> { - if(items != null) { - progLoading.setVisibility(View.GONE); - queue = items; - onFragmentLoaded(restoreScrollPosition); - if(recyclerAdapter != null) { - recyclerAdapter.notifyDataSetChanged(); - } + progLoading.setVisibility(View.GONE); + queue = items; + onFragmentLoaded(restoreScrollPosition); + if(recyclerAdapter != null) { + recyclerAdapter.notifyDataSetChanged(); } }, error -> Log.e(TAG, Log.getStackTraceString(error))); } - } diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/QuickFeedDiscoveryFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/QuickFeedDiscoveryFragment.java new file mode 100644 index 000000000..e4213cc6b --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/fragment/QuickFeedDiscoveryFragment.java @@ -0,0 +1,119 @@ +package de.danoeh.antennapod.fragment; + +import android.content.Intent; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.GridView; +import android.widget.ProgressBar; +import android.widget.TextView; +import com.afollestad.materialdialogs.MaterialDialog; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.activity.MainActivity; +import de.danoeh.antennapod.activity.OnlineFeedViewActivity; +import de.danoeh.antennapod.adapter.FeedDiscoverAdapter; +import de.danoeh.antennapod.discovery.ItunesTopListLoader; +import de.danoeh.antennapod.discovery.PodcastSearchResult; +import io.reactivex.disposables.Disposable; + +import java.util.ArrayList; +import java.util.List; + + +public class QuickFeedDiscoveryFragment extends Fragment implements AdapterView.OnItemClickListener { + private static final String TAG = "FeedDiscoveryFragment"; + + private ProgressBar progressBar; + private Disposable disposable; + private FeedDiscoverAdapter adapter; + private GridView subscriptionGridLayout; + private TextView errorTextView; + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + super.onCreateView(inflater, container, savedInstanceState); + View root = inflater.inflate(R.layout.quick_feed_discovery, container, false); + View discoverMore = root.findViewById(R.id.discover_more); + discoverMore.setOnClickListener(v -> + ((MainActivity) getActivity()).loadChildFragment(new ItunesSearchFragment())); + + subscriptionGridLayout = root.findViewById(R.id.discover_grid); + progressBar = root.findViewById(R.id.discover_progress_bar); + errorTextView = root.findViewById(R.id.discover_error); + + adapter = new FeedDiscoverAdapter((MainActivity) getActivity()); + subscriptionGridLayout.setAdapter(adapter); + subscriptionGridLayout.setOnItemClickListener(this); + + // Fill with dummy elements to have a fixed height and + // prevent the UI elements below from jumping on slow connections + List<PodcastSearchResult> dummies = new ArrayList<>(); + for (int i = 0; i < 8; i++) { + dummies.add(PodcastSearchResult.dummy()); + } + adapter.updateData(dummies); + + loadToplist(); + + return root; + } + + @Override + public void onDestroy() { + super.onDestroy(); + if (disposable != null) { + disposable.dispose(); + } + } + + private void loadToplist() { + progressBar.setVisibility(View.VISIBLE); + subscriptionGridLayout.setVisibility(View.INVISIBLE); + errorTextView.setVisibility(View.GONE); + + ItunesTopListLoader loader = new ItunesTopListLoader(getContext()); + disposable = loader.loadToplist(8) + .subscribe(podcasts -> { + errorTextView.setVisibility(View.GONE); + progressBar.setVisibility(View.GONE); + subscriptionGridLayout.setVisibility(View.VISIBLE); + adapter.updateData(podcasts); + }, error -> { + Log.e(TAG, Log.getStackTraceString(error)); + errorTextView.setText(error.getLocalizedMessage()); + errorTextView.setVisibility(View.VISIBLE); + progressBar.setVisibility(View.GONE); + subscriptionGridLayout.setVisibility(View.INVISIBLE); + }); + } + + @Override + public void onItemClick(AdapterView<?> parent, final View view, int position, long id) { + PodcastSearchResult podcast = adapter.getItem(position); + if (podcast.feedUrl == null) { + return; + } + view.setAlpha(0.5f); + ItunesTopListLoader loader = new ItunesTopListLoader(getContext()); + disposable = loader.getFeedUrl(podcast) + .subscribe(feedUrl -> { + view.setAlpha(1f); + Intent intent = new Intent(getActivity(), OnlineFeedViewActivity.class); + intent.putExtra(OnlineFeedViewActivity.ARG_FEEDURL, feedUrl); + intent.putExtra(OnlineFeedViewActivity.ARG_TITLE, getString(R.string.add_feed_label)); + startActivity(intent); + }, error -> { + Log.e(TAG, Log.getStackTraceString(error)); + view.setAlpha(1f); + String prefix = getString(R.string.error_msg_prefix); + new MaterialDialog.Builder(getActivity()) + .content(prefix + " " + error.getMessage()) + .neutralText(android.R.string.ok) + .show(); + }); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/RunningDownloadsFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/RunningDownloadsFragment.java index 66c59b7f7..2a7f7d12b 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/RunningDownloadsFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/RunningDownloadsFragment.java @@ -7,6 +7,10 @@ import android.view.View; import android.widget.ListView; import android.widget.Toast; +import org.greenrobot.eventbus.EventBus; +import org.greenrobot.eventbus.Subscribe; + +import java.util.ArrayList; import java.util.List; import de.danoeh.antennapod.R; @@ -20,7 +24,7 @@ import de.danoeh.antennapod.core.service.download.Downloader; import de.danoeh.antennapod.core.storage.DBReader; import de.danoeh.antennapod.core.storage.DBWriter; import de.danoeh.antennapod.core.storage.DownloadRequester; -import de.greenrobot.event.EventBus; +import de.danoeh.antennapod.view.EmptyViewHandler; /** * Displays all running downloads and provides actions to cancel them @@ -30,7 +34,7 @@ public class RunningDownloadsFragment extends ListFragment { private static final String TAG = "RunningDownloadsFrag"; private DownloadlistAdapter adapter; - private List<Downloader> downloaderList; + private List<Downloader> downloaderList = new ArrayList<>(); @Override public void onViewCreated(View view, Bundle savedInstanceState) { @@ -44,17 +48,24 @@ public class RunningDownloadsFragment extends ListFragment { adapter = new DownloadlistAdapter(getActivity(), itemAccess); setListAdapter(adapter); + + EmptyViewHandler emptyView = new EmptyViewHandler(getActivity()); + emptyView.setIcon(R.attr.av_download); + emptyView.setTitle(R.string.no_run_downloads_head_label); + emptyView.setMessage(R.string.no_run_downloads_label); + emptyView.attachToListView(getListView()); + } @Override - public void onResume() { - super.onResume(); - EventBus.getDefault().registerSticky(this); + public void onStart() { + super.onStart(); + EventBus.getDefault().register(this); } @Override - public void onPause() { - super.onPause(); + public void onStop() { + super.onStop(); EventBus.getDefault().unregister(this); } @@ -62,28 +73,25 @@ public class RunningDownloadsFragment extends ListFragment { public void onDestroy() { super.onDestroy(); setListAdapter(null); - adapter = null; } + @Subscribe(sticky = true) public void onEvent(DownloadEvent event) { Log.d(TAG, "onEvent() called with: " + "event = [" + event + "]"); DownloaderUpdate update = event.update; downloaderList = update.downloaders; - if (adapter != null) { - adapter.notifyDataSetChanged(); - } + adapter.notifyDataSetChanged(); } - private final DownloadlistAdapter.ItemAccess itemAccess = new DownloadlistAdapter.ItemAccess() { @Override public int getCount() { - return (downloaderList != null) ? downloaderList.size() : 0; + return downloaderList.size(); } @Override public Downloader getItem(int position) { - if (downloaderList != null && 0 <= position && position < downloaderList.size()) { + if (0 <= position && position < downloaderList.size()) { return downloaderList.get(position); } else { return null; diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/SearchFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/SearchFragment.java index 8322a5573..0892bce0a 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/SearchFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/SearchFragment.java @@ -2,6 +2,7 @@ package de.danoeh.antennapod.fragment; import android.content.Context; import android.os.Bundle; +import android.support.annotation.NonNull; import android.support.v4.app.ListFragment; import android.support.v4.view.MenuItemCompat; import android.support.v7.app.AppCompatActivity; @@ -13,6 +14,7 @@ import android.view.MenuItem; import android.view.View; import android.widget.ListView; +import java.util.ArrayList; import java.util.List; import de.danoeh.antennapod.R; @@ -39,11 +41,7 @@ public class SearchFragment extends ListFragment { private static final String ARG_FEED = "feed"; private SearchlistAdapter searchAdapter; - private List<SearchResult> searchResults; - - private boolean viewCreated = false; - private boolean itemsLoaded = false; - + private List<SearchResult> searchResults = new ArrayList<>(); private Disposable disposable; /** @@ -73,13 +71,13 @@ public class SearchFragment extends ListFragment { super.onCreate(savedInstanceState); setRetainInstance(true); setHasOptionsMenu(true); - search(); } @Override public void onStart() { super.onStart(); EventDistributor.getInstance().register(contentUpdate); + search(); } @Override @@ -92,21 +90,6 @@ public class SearchFragment extends ListFragment { } @Override - public void onDetach() { - super.onDetach(); - if(disposable != null) { - disposable.dispose(); - } - } - - @Override - public void onDestroyView() { - super.onDestroyView(); - searchAdapter = null; - viewCreated = false; - } - - @Override public void onViewCreated(View view, Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); @@ -117,10 +100,9 @@ public class SearchFragment extends ListFragment { lv.setPadding(0, vertPadding, 0, vertPadding); ((AppCompatActivity) getActivity()).getSupportActionBar().setTitle(R.string.search_label); - viewCreated = true; - if (itemsLoaded) { - onFragmentLoaded(); - } + + searchAdapter = new SearchlistAdapter(getActivity(), itemAccess); + setListAdapter(searchAdapter); } @Override @@ -141,28 +123,26 @@ public class SearchFragment extends ListFragment { @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { super.onCreateOptionsMenu(menu, inflater); - if (itemsLoaded) { - 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); - final SearchView sv = new SearchView(getActivity()); - sv.setQueryHint(getString(R.string.search_hint)); - sv.setQuery(getArguments().getString(ARG_QUERY), false); - sv.setOnQueryTextListener(new SearchView.OnQueryTextListener() { - @Override - public boolean onQueryTextSubmit(String s) { - getArguments().putString(ARG_QUERY, s); - itemsLoaded = false; - search(); - return true; - } - - @Override - public boolean onQueryTextChange(String s) { - return false; - } - }); - MenuItemCompat.setActionView(item, sv); - } + 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); + final SearchView sv = new SearchView(getActivity()); + sv.setQueryHint(getString(R.string.search_hint)); + sv.setQuery(getArguments().getString(ARG_QUERY), false); + sv.setOnQueryTextListener(new SearchView.OnQueryTextListener() { + @Override + public boolean onQueryTextSubmit(String s) { + sv.clearFocus(); + getArguments().putString(ARG_QUERY, s); + search(); + return true; + } + + @Override + public boolean onQueryTextChange(String s) { + return false; + } + }); + MenuItemCompat.setActionView(item, sv); } private final EventDistributor.EventListener contentUpdate = new EventDistributor.EventListener() { @@ -175,14 +155,9 @@ public class SearchFragment extends ListFragment { } }; - private void onFragmentLoaded() { - if (searchAdapter == null) { - searchAdapter = new SearchlistAdapter(getActivity(), itemAccess); - setListAdapter(searchAdapter); - } + private void onSearchResults(List<SearchResult> results) { + searchResults = results; searchAdapter.notifyDataSetChanged(); - setListShown(true); - String query = getArguments().getString(ARG_QUERY); setEmptyText(getString(R.string.no_results_for_query, query)); } @@ -190,12 +165,12 @@ public class SearchFragment extends ListFragment { private final SearchlistAdapter.ItemAccess itemAccess = new SearchlistAdapter.ItemAccess() { @Override public int getCount() { - return (searchResults != null) ? searchResults.size() : 0; + return searchResults.size(); } @Override public SearchResult getItem(int position) { - if (searchResults != null && 0 <= position && position < searchResults.size()) { + if (0 <= position && position < searchResults.size()) { return searchResults.get(position); } else { return null; @@ -203,28 +178,17 @@ public class SearchFragment extends ListFragment { } }; - private void search() { if(disposable != null) { disposable.dispose(); } - if (viewCreated && !itemsLoaded) { - setListShown(false); - } disposable = Observable.fromCallable(this::performSearch) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) - .subscribe(result -> { - if (result != null) { - itemsLoaded = true; - searchResults = result; - if (viewCreated) { - onFragmentLoaded(); - } - } - }, error -> Log.e(TAG, Log.getStackTraceString(error))); + .subscribe(this::onSearchResults, error -> Log.e(TAG, Log.getStackTraceString(error))); } + @NonNull private List<SearchResult> performSearch() { Bundle args = getArguments(); String query = args.getString(ARG_QUERY); @@ -232,5 +196,4 @@ public class SearchFragment extends ListFragment { Context context = getActivity(); return FeedSearcher.performSearch(context, query, feed); } - } diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/SubscriptionFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/SubscriptionFragment.java index 5f09be8ce..15c6052a9 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/SubscriptionFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/SubscriptionFragment.java @@ -1,11 +1,16 @@ package de.danoeh.antennapod.fragment; +import android.annotation.SuppressLint; +import android.content.Context; import android.content.DialogInterface; +import android.content.SharedPreferences; import android.os.Bundle; +import android.support.annotation.StringRes; import android.support.v4.app.Fragment; import android.util.Log; import android.view.ContextMenu; import android.view.LayoutInflater; +import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; @@ -13,6 +18,8 @@ import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.GridView; +import java.util.concurrent.Callable; + import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.MainActivity; import de.danoeh.antennapod.adapter.SubscriptionsAdapter; @@ -41,6 +48,8 @@ public class SubscriptionFragment extends Fragment { private static final int EVENTS = EventDistributor.FEED_LIST_UPDATE | EventDistributor.UNREAD_ITEMS_UPDATE; + private static final String PREFS = "SubscriptionFragment"; + private static final String PREF_NUM_COLUMNS = "columns"; private GridView subscriptionGridLayout; private DBReader.NavDrawerData navDrawerData; @@ -49,19 +58,15 @@ public class SubscriptionFragment extends Fragment { private int mPosition = -1; private Disposable disposable; - - public SubscriptionFragment() { - } + private SharedPreferences prefs; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setRetainInstance(true); - - // So, we certainly *don't* have an options menu, - // but unless we say we do, old options menus sometimes - // persist. mfietz thinks this causes the ActionBar to be invalidated setHasOptionsMenu(true); + + prefs = requireActivity().getSharedPreferences(PREFS, Context.MODE_PRIVATE); } @Override @@ -69,31 +74,75 @@ public class SubscriptionFragment extends Fragment { Bundle savedInstanceState) { View root = inflater.inflate(R.layout.fragment_subscriptions, container, false); subscriptionGridLayout = root.findViewById(R.id.subscriptions_grid); + subscriptionGridLayout.setNumColumns(prefs.getInt(PREF_NUM_COLUMNS, 3)); registerForContextMenu(subscriptionGridLayout); return root; } @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + super.onCreateOptionsMenu(menu, inflater); + inflater.inflate(R.menu.subscriptions, menu); + + int columns = prefs.getInt(PREF_NUM_COLUMNS, 3); + menu.findItem(R.id.subscription_num_columns_2).setChecked(columns == 2); + menu.findItem(R.id.subscription_num_columns_3).setChecked(columns == 3); + menu.findItem(R.id.subscription_num_columns_4).setChecked(columns == 4); + menu.findItem(R.id.subscription_num_columns_5).setChecked(columns == 5); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (super.onOptionsItemSelected(item)) { + return true; + } + switch (item.getItemId()) { + case R.id.subscription_num_columns_2: + setColumnNumber(2); + return true; + case R.id.subscription_num_columns_3: + setColumnNumber(3); + return true; + case R.id.subscription_num_columns_4: + setColumnNumber(4); + return true; + case R.id.subscription_num_columns_5: + setColumnNumber(5); + return true; + default: + return false; + } + } + + private void setColumnNumber(int columns) { + subscriptionGridLayout.setNumColumns(columns); + prefs.edit().putInt(PREF_NUM_COLUMNS, columns).apply(); + getActivity().invalidateOptionsMenu(); + } + + @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); subscriptionAdapter = new SubscriptionsAdapter((MainActivity)getActivity(), itemAccess); - subscriptionGridLayout.setAdapter(subscriptionAdapter); - - loadSubscriptions(); - subscriptionGridLayout.setOnItemClickListener(subscriptionAdapter); if (getActivity() instanceof MainActivity) { ((MainActivity) getActivity()).getSupportActionBar().setTitle(R.string.subscriptions_label); } + } + @Override + public void onStart() { + super.onStart(); EventDistributor.getInstance().register(contentUpdate); + loadSubscriptions(); } @Override - public void onDestroy() { - super.onDestroy(); + public void onStop() { + super.onStop(); + EventDistributor.getInstance().unregister(contentUpdate); if(disposable != null) { disposable.dispose(); } @@ -126,7 +175,7 @@ public class SubscriptionFragment extends Fragment { Feed feed = (Feed)selectedObject; - MenuInflater inflater = getActivity().getMenuInflater(); + MenuInflater inflater = requireActivity().getMenuInflater(); inflater.inflate(R.menu.nav_feed_context, menu); menu.setHeaderTitle(feed.getTitle()); @@ -136,7 +185,6 @@ public class SubscriptionFragment extends Fragment { @Override public boolean onContextItemSelected(MenuItem item) { - final int position = mPosition; mPosition = -1; // reset if(position < 0) { @@ -151,84 +199,73 @@ public class SubscriptionFragment extends Fragment { Feed feed = (Feed)selectedObject; switch(item.getItemId()) { - case R.id.mark_all_seen_item: - ConfirmationDialog markAllSeenConfirmationDialog = new ConfirmationDialog(getActivity(), - R.string.mark_all_seen_label, - R.string.mark_all_seen_confirmation_msg) { - - @Override - public void onConfirmButtonPressed(DialogInterface dialog) { - dialog.dismiss(); - - Observable.fromCallable(() -> DBWriter.markFeedSeen(feed.getId())) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(result -> loadSubscriptions(), - error -> Log.e(TAG, Log.getStackTraceString(error))); - } - }; - markAllSeenConfirmationDialog.createNewDialog().show(); + case R.id.remove_all_new_flags_item: + displayConfirmationDialog( + R.string.remove_all_new_flags_label, + R.string.remove_all_new_flags_confirmation_msg, + () -> DBWriter.removeFeedNewFlag(feed.getId())); return true; case R.id.mark_all_read_item: - ConfirmationDialog markAllReadConfirmationDialog = new ConfirmationDialog(getActivity(), + displayConfirmationDialog( R.string.mark_all_read_label, - R.string.mark_all_read_confirmation_msg) { - - @Override - public void onConfirmButtonPressed(DialogInterface dialog) { - dialog.dismiss(); - Observable.fromCallable(() -> DBWriter.markFeedRead(feed.getId())) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(result -> loadSubscriptions(), - error -> Log.e(TAG, Log.getStackTraceString(error))); - } - }; - markAllReadConfirmationDialog.createNewDialog().show(); + R.string.mark_all_read_confirmation_msg, + () -> DBWriter.markFeedRead(feed.getId())); return true; case R.id.rename_item: new RenameFeedDialog(getActivity(), feed).show(); return true; case R.id.remove_item: - final FeedRemover remover = new FeedRemover(getContext(), feed) { - @Override - protected void onPostExecute(Void result) { - super.onPostExecute(result); - loadSubscriptions(); - } - }; - ConfirmationDialog conDialog = new ConfirmationDialog(getContext(), - R.string.remove_feed_label, - getString(R.string.feed_delete_confirmation_msg, feed.getTitle())) { - @Override - public void onConfirmButtonPressed( - DialogInterface dialog) { - dialog.dismiss(); - long mediaId = PlaybackPreferences.getCurrentlyPlayingFeedMediaId(); - if (mediaId > 0 && - FeedItemUtil.indexOfItemWithMediaId(feed.getItems(), mediaId) >= 0) { - Log.d(TAG, "Currently playing episode is about to be deleted, skipping"); - remover.skipOnCompletion = true; - int playerStatus = PlaybackPreferences.getCurrentPlayerStatus(); - if(playerStatus == PlaybackPreferences.PLAYER_STATUS_PLAYING) { - IntentUtils.sendLocalBroadcast(getContext(), PlaybackService.ACTION_PAUSE_PLAY_CURRENT_EPISODE); - - } - } - remover.executeAsync(); - } - }; - conDialog.createNewDialog().show(); + displayRemoveFeedDialog(feed); return true; default: return super.onContextItemSelected(item); } } - @Override - public void onResume() { - super.onResume(); - loadSubscriptions(); + private void displayRemoveFeedDialog(Feed feed) { + final FeedRemover remover = new FeedRemover(getContext(), feed) { + @Override + protected void onPostExecute(Void result) { + super.onPostExecute(result); + loadSubscriptions(); + } + }; + + String message = getString(R.string.feed_delete_confirmation_msg, feed.getTitle()); + ConfirmationDialog dialog = new ConfirmationDialog(getContext(), R.string.remove_feed_label, message) { + @Override + public void onConfirmButtonPressed(DialogInterface clickedDialog) { + clickedDialog.dismiss(); + long mediaId = PlaybackPreferences.getCurrentlyPlayingFeedMediaId(); + if (mediaId > 0 && FeedItemUtil.indexOfItemWithMediaId(feed.getItems(), mediaId) >= 0) { + Log.d(TAG, "Currently playing episode is about to be deleted, skipping"); + remover.skipOnCompletion = true; + int playerStatus = PlaybackPreferences.getCurrentPlayerStatus(); + if(playerStatus == PlaybackPreferences.PLAYER_STATUS_PLAYING) { + IntentUtils.sendLocalBroadcast(getContext(), PlaybackService.ACTION_PAUSE_PLAY_CURRENT_EPISODE); + + } + } + remover.executeAsync(); + } + }; + dialog.createNewDialog().show(); + } + + private <T> void displayConfirmationDialog(@StringRes int title, @StringRes int message, Callable<? extends T> task) { + ConfirmationDialog dialog = new ConfirmationDialog(getActivity(), title, message) { + @Override + @SuppressLint("CheckResult") + public void onConfirmButtonPressed(DialogInterface clickedDialog) { + clickedDialog.dismiss(); + Observable.fromCallable(task) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(result -> loadSubscriptions(), + error -> Log.e(TAG, Log.getStackTraceString(error))); + } + }; + dialog.createNewDialog().show(); } private final EventDistributor.EventListener contentUpdate = new EventDistributor.EventListener() { diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/AutoDownloadPreferencesFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/AutoDownloadPreferencesFragment.java new file mode 100644 index 000000000..6cba798ba --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/AutoDownloadPreferencesFragment.java @@ -0,0 +1,184 @@ +package de.danoeh.antennapod.fragment.preferences; + +import android.app.Activity; +import android.content.Context; +import android.content.res.Resources; +import android.net.wifi.WifiConfiguration; +import android.net.wifi.WifiManager; +import android.os.Bundle; +import android.support.v7.preference.CheckBoxPreference; +import android.support.v7.preference.ListPreference; +import android.support.v7.preference.Preference; +import android.support.v7.preference.PreferenceFragmentCompat; +import android.support.v7.preference.PreferenceScreen; +import android.util.Log; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.core.preferences.UserPreferences; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +public class AutoDownloadPreferencesFragment extends PreferenceFragmentCompat { + private static final String TAG = "AutoDnldPrefFragment"; + private CheckBoxPreference[] selectedNetworks; + + @Override + public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + addPreferencesFromResource(R.xml.preferences_autodownload); + + setupAutoDownloadScreen(); + buildAutodownloadSelectedNetworksPreference(); + setSelectedNetworksEnabled(UserPreferences.isEnableAutodownloadWifiFilter()); + buildEpisodeCleanupPreference(); + } + + @Override + public void onResume() { + super.onResume(); + checkAutodownloadItemVisibility(UserPreferences.isEnableAutodownload()); + } + + private void setupAutoDownloadScreen() { + findPreference(UserPreferences.PREF_ENABLE_AUTODL).setOnPreferenceChangeListener( + (preference, newValue) -> { + if (newValue instanceof Boolean) { + checkAutodownloadItemVisibility((Boolean) newValue); + } + return true; + }); + findPreference(UserPreferences.PREF_ENABLE_AUTODL_WIFI_FILTER) + .setOnPreferenceChangeListener( + (preference, newValue) -> { + if (newValue instanceof Boolean) { + setSelectedNetworksEnabled((Boolean) newValue); + return true; + } else { + return false; + } + } + ); + } + + private void checkAutodownloadItemVisibility(boolean autoDownload) { + findPreference(UserPreferences.PREF_EPISODE_CACHE_SIZE).setEnabled(autoDownload); + findPreference(UserPreferences.PREF_ENABLE_AUTODL_ON_BATTERY).setEnabled(autoDownload); + findPreference(UserPreferences.PREF_ENABLE_AUTODL_WIFI_FILTER).setEnabled(autoDownload); + findPreference(UserPreferences.PREF_EPISODE_CLEANUP).setEnabled(autoDownload); + setSelectedNetworksEnabled(autoDownload && UserPreferences.isEnableAutodownloadWifiFilter()); + } + + private static String blankIfNull(String val) { + return val == null ? "" : val; + } + + private void buildAutodownloadSelectedNetworksPreference() { + final Activity activity = getActivity(); + + if (selectedNetworks != null) { + clearAutodownloadSelectedNetworsPreference(); + } + // get configured networks + WifiManager wifiservice = (WifiManager) activity.getApplicationContext().getSystemService(Context.WIFI_SERVICE); + List<WifiConfiguration> networks = wifiservice.getConfiguredNetworks(); + + if (networks == null) { + Log.e(TAG, "Couldn't get list of configure Wi-Fi networks"); + return; + } + Collections.sort(networks, (x, y) -> + blankIfNull(x.SSID).compareTo(blankIfNull(y.SSID))); + selectedNetworks = new CheckBoxPreference[networks.size()]; + List<String> prefValues = Arrays.asList(UserPreferences + .getAutodownloadSelectedNetworks()); + PreferenceScreen prefScreen = getPreferenceScreen(); + Preference.OnPreferenceClickListener clickListener = preference -> { + if (preference instanceof CheckBoxPreference) { + String key = preference.getKey(); + List<String> prefValuesList = new ArrayList<>( + Arrays.asList(UserPreferences + .getAutodownloadSelectedNetworks()) + ); + boolean newValue = ((CheckBoxPreference) preference) + .isChecked(); + Log.d(TAG, "Selected network " + key + ". New state: " + newValue); + + int index = prefValuesList.indexOf(key); + if (index >= 0 && !newValue) { + // remove network + prefValuesList.remove(index); + } else if (index < 0 && newValue) { + prefValuesList.add(key); + } + + UserPreferences.setAutodownloadSelectedNetworks( + prefValuesList.toArray(new String[prefValuesList.size()]) + ); + return true; + } else { + return false; + } + }; + // create preference for each known network. attach listener and set + // value + for (int i = 0; i < networks.size(); i++) { + WifiConfiguration config = networks.get(i); + + CheckBoxPreference pref = new CheckBoxPreference(activity); + String key = Integer.toString(config.networkId); + pref.setTitle(config.SSID); + pref.setKey(key); + pref.setOnPreferenceClickListener(clickListener); + pref.setPersistent(false); + pref.setChecked(prefValues.contains(key)); + selectedNetworks[i] = pref; + prefScreen.addPreference(pref); + } + } + + private void clearAutodownloadSelectedNetworsPreference() { + if (selectedNetworks != null) { + PreferenceScreen prefScreen = getPreferenceScreen(); + + for (CheckBoxPreference network : selectedNetworks) { + if (network != null) { + prefScreen.removePreference(network); + } + } + } + } + + private void buildEpisodeCleanupPreference() { + final Resources res = getActivity().getResources(); + + ListPreference pref = (ListPreference) findPreference(UserPreferences.PREF_EPISODE_CLEANUP); + String[] values = res.getStringArray( + R.array.episode_cleanup_values); + String[] entries = new String[values.length]; + for (int x = 0; x < values.length; x++) { + int v = Integer.parseInt(values[x]); + if (v == UserPreferences.EPISODE_CLEANUP_QUEUE) { + entries[x] = res.getString(R.string.episode_cleanup_queue_removal); + } else if (v == UserPreferences.EPISODE_CLEANUP_NULL){ + entries[x] = res.getString(R.string.episode_cleanup_never); + } else if (v == 0) { + entries[x] = res.getString(R.string.episode_cleanup_after_listening); + } else if (v > 0 && v < 24) { + entries[x] = res.getQuantityString(R.plurals.episode_cleanup_hours_after_listening, v, v); + } else { + int numDays = v / 24; // assume underlying value will be NOT fraction of days, e.g., 36 (hours) + entries[x] = res.getQuantityString(R.plurals.episode_cleanup_days_after_listening, numDays, numDays); + } + } + pref.setEntries(entries); + } + + private void setSelectedNetworksEnabled(boolean b) { + if (selectedNetworks != null) { + for (Preference p : selectedNetworks) { + p.setEnabled(b); + } + } + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/GpodderPreferencesFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/GpodderPreferencesFragment.java new file mode 100644 index 000000000..491922056 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/GpodderPreferencesFragment.java @@ -0,0 +1,144 @@ +package de.danoeh.antennapod.fragment.preferences; + +import android.app.Activity; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.support.v7.preference.Preference; +import android.support.v7.preference.PreferenceFragmentCompat; +import android.text.Html; +import android.text.format.DateUtils; +import android.widget.Toast; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.core.preferences.GpodnetPreferences; +import de.danoeh.antennapod.core.service.GpodnetSyncService; +import de.danoeh.antennapod.dialog.AuthenticationDialog; +import de.danoeh.antennapod.dialog.GpodnetSetHostnameDialog; + +public class GpodderPreferencesFragment extends PreferenceFragmentCompat { + private static final String PREF_GPODNET_LOGIN = "pref_gpodnet_authenticate"; + private static final String PREF_GPODNET_SETLOGIN_INFORMATION = "pref_gpodnet_setlogin_information"; + private static final String PREF_GPODNET_SYNC = "pref_gpodnet_sync"; + private static final String PREF_GPODNET_FORCE_FULL_SYNC = "pref_gpodnet_force_full_sync"; + private static final String PREF_GPODNET_LOGOUT = "pref_gpodnet_logout"; + private static final String PREF_GPODNET_HOSTNAME = "pref_gpodnet_hostname"; + private static final String PREF_GPODNET_NOTIFICATIONS = "pref_gpodnet_notifications"; + + @Override + public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + addPreferencesFromResource(R.xml.preferences_gpodder); + setupGpodderScreen(); + } + + @Override + public void onResume() { + super.onResume(); + GpodnetPreferences.registerOnSharedPreferenceChangeListener(gpoddernetListener); + updateGpodnetPreferenceScreen(); + } + + @Override + public void onPause() { + super.onPause(); + GpodnetPreferences.unregisterOnSharedPreferenceChangeListener(gpoddernetListener); + } + + private final SharedPreferences.OnSharedPreferenceChangeListener gpoddernetListener = + (sharedPreferences, key) -> { + if (GpodnetPreferences.PREF_LAST_SYNC_ATTEMPT_TIMESTAMP.equals(key)) { + updateLastGpodnetSyncReport(GpodnetPreferences.getLastSyncAttemptResult(), + GpodnetPreferences.getLastSyncAttemptTimestamp()); + } + }; + + private void setupGpodderScreen() { + final Activity activity = getActivity(); + + findPreference(PREF_GPODNET_SETLOGIN_INFORMATION) + .setOnPreferenceClickListener(preference -> { + AuthenticationDialog dialog = new AuthenticationDialog(activity, + R.string.pref_gpodnet_setlogin_information_title, false, false, GpodnetPreferences.getUsername(), + null) { + + @Override + protected void onConfirmed(String username, String password, boolean saveUsernamePassword) { + GpodnetPreferences.setPassword(password); + } + }; + dialog.show(); + return true; + }); + findPreference(PREF_GPODNET_SYNC). + setOnPreferenceClickListener(preference -> { + GpodnetSyncService.sendSyncIntent(getActivity().getApplicationContext()); + Toast toast = Toast.makeText(getActivity(), R.string.pref_gpodnet_sync_started, + Toast.LENGTH_SHORT); + toast.show(); + return true; + }); + findPreference(PREF_GPODNET_FORCE_FULL_SYNC). + setOnPreferenceClickListener(preference -> { + GpodnetPreferences.setLastSubscriptionSyncTimestamp(0L); + GpodnetPreferences.setLastEpisodeActionsSyncTimestamp(0L); + GpodnetPreferences.setLastSyncAttempt(false, 0); + updateLastGpodnetSyncReport(false, 0); + GpodnetSyncService.sendSyncIntent(getActivity().getApplicationContext()); + Toast toast = Toast.makeText(getActivity(), R.string.pref_gpodnet_sync_started, + Toast.LENGTH_SHORT); + toast.show(); + return true; + }); + findPreference(PREF_GPODNET_LOGOUT).setOnPreferenceClickListener( + preference -> { + GpodnetPreferences.logout(); + Toast toast = Toast.makeText(activity, R.string.pref_gpodnet_logout_toast, Toast.LENGTH_SHORT); + toast.show(); + updateGpodnetPreferenceScreen(); + return true; + }); + findPreference(PREF_GPODNET_HOSTNAME).setOnPreferenceClickListener( + preference -> { + GpodnetSetHostnameDialog.createDialog(activity).setOnDismissListener(dialog -> updateGpodnetPreferenceScreen()); + return true; + }); + } + + private void updateGpodnetPreferenceScreen() { + final boolean loggedIn = GpodnetPreferences.loggedIn(); + findPreference(PREF_GPODNET_LOGIN).setEnabled(!loggedIn); + findPreference(PREF_GPODNET_SETLOGIN_INFORMATION).setEnabled(loggedIn); + findPreference(PREF_GPODNET_SYNC).setEnabled(loggedIn); + findPreference(PREF_GPODNET_FORCE_FULL_SYNC).setEnabled(loggedIn); + findPreference(PREF_GPODNET_LOGOUT).setEnabled(loggedIn); + findPreference(PREF_GPODNET_NOTIFICATIONS).setEnabled(loggedIn); + if(loggedIn) { + String format = getActivity().getString(R.string.pref_gpodnet_login_status); + String summary = String.format(format, GpodnetPreferences.getUsername(), + GpodnetPreferences.getDeviceID()); + findPreference(PREF_GPODNET_LOGOUT).setSummary(Html.fromHtml(summary)); + updateLastGpodnetSyncReport(GpodnetPreferences.getLastSyncAttemptResult(), + GpodnetPreferences.getLastSyncAttemptTimestamp()); + } else { + findPreference(PREF_GPODNET_LOGOUT).setSummary(null); + updateLastGpodnetSyncReport(false, 0); + } + findPreference(PREF_GPODNET_HOSTNAME).setSummary(GpodnetPreferences.getHostname()); + } + + private void updateLastGpodnetSyncReport(boolean successful, long lastTime) { + Preference sync = findPreference(PREF_GPODNET_SYNC); + if (lastTime != 0) { + sync.setSummary(getActivity().getString(R.string.pref_gpodnet_sync_changes_sum) + "\n" + + getActivity().getString(R.string.pref_gpodnet_sync_sum_last_sync_line, + getActivity().getString(successful ? + R.string.gpodnetsync_pref_report_successful : + R.string.gpodnetsync_pref_report_failed), + DateUtils.getRelativeDateTimeString(getActivity(), + lastTime, + DateUtils.MINUTE_IN_MILLIS, + DateUtils.WEEK_IN_MILLIS, + DateUtils.FORMAT_SHOW_TIME))); + } else { + sync.setSummary(getActivity().getString(R.string.pref_gpodnet_sync_changes_sum)); + } + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/IntegrationsPreferencesFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/IntegrationsPreferencesFragment.java new file mode 100644 index 000000000..229274b76 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/IntegrationsPreferencesFragment.java @@ -0,0 +1,23 @@ +package de.danoeh.antennapod.fragment.preferences; + +import android.os.Bundle; +import android.support.v7.preference.PreferenceFragmentCompat; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.activity.PreferenceActivity; + +public class IntegrationsPreferencesFragment extends PreferenceFragmentCompat { + private static final String PREF_SCREEN_GPODDER = "prefGpodderSettings"; + + @Override + public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + addPreferencesFromResource(R.xml.preferences_integrations); + setupIntegrationsScreen(); + } + + private void setupIntegrationsScreen() { + findPreference(PREF_SCREEN_GPODDER).setOnPreferenceClickListener(preference -> { + ((PreferenceActivity) getActivity()).openScreen(R.xml.preferences_gpodder); + return true; + }); + } +} 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 new file mode 100644 index 000000000..701d21ce0 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/MainPreferencesFragment.java @@ -0,0 +1,147 @@ +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.PreferenceActivity; +import de.danoeh.antennapod.activity.StatisticsActivity; + +import java.util.List; + +public class MainPreferencesFragment extends PreferenceFragmentCompat { + private static final String TAG = "MainPreferencesFragment"; + + private static final String PREF_SCREEN_USER_INTERFACE = "prefScreenInterface"; + private static final String PREF_SCREEN_PLAYBACK = "prefScreenPlayback"; + private static final String PREF_SCREEN_NETWORK = "prefScreenNetwork"; + private static final String PREF_SCREEN_INTEGRATIONS = "prefScreenIntegrations"; + private static final String PREF_SCREEN_STORAGE = "prefScreenStorage"; + private static final String PREF_KNOWN_ISSUES = "prefKnownIssues"; + private static final String PREF_FAQ = "prefFaq"; + private static final String PREF_SEND_CRASH_REPORT = "prefSendCrashReport"; + private static final String STATISTICS = "statistics"; + private static final String PREF_ABOUT = "prefAbout"; + + @Override + public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + addPreferencesFromResource(R.xml.preferences); + setupMainScreen(); + setupSearch(); + } + + private void setupMainScreen() { + findPreference(PREF_SCREEN_USER_INTERFACE).setOnPreferenceClickListener(preference -> { + ((PreferenceActivity) getActivity()).openScreen(R.xml.preferences_user_interface); + return true; + }); + findPreference(PREF_SCREEN_PLAYBACK).setOnPreferenceClickListener(preference -> { + ((PreferenceActivity) getActivity()).openScreen(R.xml.preferences_playback); + return true; + }); + findPreference(PREF_SCREEN_NETWORK).setOnPreferenceClickListener(preference -> { + ((PreferenceActivity) getActivity()).openScreen(R.xml.preferences_network); + return true; + }); + findPreference(PREF_SCREEN_INTEGRATIONS).setOnPreferenceClickListener(preference -> { + ((PreferenceActivity) getActivity()).openScreen(R.xml.preferences_integrations); + return true; + }); + findPreference(PREF_SCREEN_STORAGE).setOnPreferenceClickListener(preference -> { + ((PreferenceActivity) getActivity()).openScreen(R.xml.preferences_storage); + return true; + }); + + findPreference(PREF_ABOUT).setOnPreferenceClickListener( + preference -> { + startActivity(new Intent(getActivity(), AboutActivity.class)); + return true; + } + ); + findPreference(STATISTICS).setOnPreferenceClickListener( + preference -> { + startActivity(new Intent(getActivity(), StatisticsActivity.class)); + return true; + } + ); + findPreference(PREF_KNOWN_ISSUES).setOnPreferenceClickListener(preference -> { + openInBrowser("https://github.com/AntennaPod/AntennaPod/labels/bug"); + return true; + }); + findPreference(PREF_FAQ).setOnPreferenceClickListener(preference -> { + openInBrowser("http://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)); + return true; + }); + } + + private void openInBrowser(String url) { + try { + Intent myIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); + startActivity(myIntent); + } catch (ActivityNotFoundException e) { + Toast.makeText(getActivity(), R.string.pref_no_browser_found, Toast.LENGTH_LONG).show(); + Log.e(TAG, Log.getStackTraceString(e)); + } + } + + private void setupSearch() { + SearchPreference searchPreference = (SearchPreference) findPreference("searchPreference"); + SearchConfiguration config = searchPreference.getSearchConfiguration(); + config.setActivity((AppCompatActivity) getActivity()); + config.setFragmentContainerViewId(R.id.content); + config.setBreadcrumbsEnabled(true); + + config.index(R.xml.preferences_user_interface) + .addBreadcrumb(PreferenceActivity.getTitleOfPage(R.xml.preferences_user_interface)); + config.index(R.xml.preferences_playback) + .addBreadcrumb(PreferenceActivity.getTitleOfPage(R.xml.preferences_playback)); + config.index(R.xml.preferences_network) + .addBreadcrumb(PreferenceActivity.getTitleOfPage(R.xml.preferences_network)); + config.index(R.xml.preferences_storage) + .addBreadcrumb(PreferenceActivity.getTitleOfPage(R.xml.preferences_storage)); + config.index(R.xml.preferences_autodownload) + .addBreadcrumb(PreferenceActivity.getTitleOfPage(R.xml.preferences_network)) + .addBreadcrumb(R.string.automation) + .addBreadcrumb(PreferenceActivity.getTitleOfPage(R.xml.preferences_autodownload)); + config.index(R.xml.preferences_gpodder) + .addBreadcrumb(PreferenceActivity.getTitleOfPage(R.xml.preferences_integrations)) + .addBreadcrumb(PreferenceActivity.getTitleOfPage(R.xml.preferences_gpodder)); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/NetworkPreferencesFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/NetworkPreferencesFragment.java new file mode 100644 index 000000000..ac2436e25 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/NetworkPreferencesFragment.java @@ -0,0 +1,175 @@ +package de.danoeh.antennapod.fragment.preferences; + +import android.app.TimePickerDialog; +import android.content.Context; +import android.content.res.Resources; +import android.os.Bundle; +import android.support.v7.app.AlertDialog; +import android.support.v7.app.AppCompatActivity; +import android.support.v7.preference.Preference; +import android.support.v7.preference.PreferenceFragmentCompat; +import android.text.format.DateFormat; +import com.afollestad.materialdialogs.MaterialDialog; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.activity.PreferenceActivity; +import de.danoeh.antennapod.core.preferences.UserPreferences; +import de.danoeh.antennapod.dialog.ProxyDialog; +import org.apache.commons.lang3.ArrayUtils; + +import java.util.Calendar; +import java.util.GregorianCalendar; +import java.util.concurrent.TimeUnit; + +public class NetworkPreferencesFragment extends PreferenceFragmentCompat { + private static final String PREF_SCREEN_AUTODL = "prefAutoDownloadSettings"; + private static final String PREF_PROXY = "prefProxy"; + + @Override + public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + addPreferencesFromResource(R.xml.preferences_network); + setupNetworkScreen(); + } + + @Override + public void onResume() { + super.onResume(); + setUpdateIntervalText(); + setParallelDownloadsText(UserPreferences.getParallelDownloads()); + } + + private void setupNetworkScreen() { + findPreference(PREF_SCREEN_AUTODL).setOnPreferenceClickListener(preference -> { + ((PreferenceActivity) getActivity()).openScreen(R.xml.preferences_autodownload); + return true; + }); + findPreference(UserPreferences.PREF_UPDATE_INTERVAL) + .setOnPreferenceClickListener(preference -> { + showUpdateIntervalTimePreferencesDialog(); + return true; + }); + findPreference(UserPreferences.PREF_PARALLEL_DOWNLOADS) + .setOnPreferenceChangeListener( + (preference, o) -> { + if (o instanceof Integer) { + setParallelDownloadsText((Integer) o); + } + return true; + } + ); + // validate and set correct value: number of downloads between 1 and 50 (inclusive) + findPreference(PREF_PROXY).setOnPreferenceClickListener(preference -> { + ProxyDialog dialog = new ProxyDialog(getActivity()); + dialog.createDialog().show(); + return true; + }); + } + + private void setUpdateIntervalText() { + Context context = getActivity().getApplicationContext(); + String val; + long interval = UserPreferences.getUpdateInterval(); + if(interval > 0) { + int hours = (int) TimeUnit.MILLISECONDS.toHours(interval); + String hoursStr = context.getResources().getQuantityString(R.plurals.time_hours_quantified, hours, hours); + val = String.format(context.getString(R.string.pref_autoUpdateIntervallOrTime_every), hoursStr); + } else { + int[] timeOfDay = UserPreferences.getUpdateTimeOfDay(); + if(timeOfDay.length == 2) { + Calendar cal = new GregorianCalendar(); + cal.set(Calendar.HOUR_OF_DAY, timeOfDay[0]); + cal.set(Calendar.MINUTE, timeOfDay[1]); + String timeOfDayStr = DateFormat.getTimeFormat(context).format(cal.getTime()); + val = String.format(context.getString(R.string.pref_autoUpdateIntervallOrTime_at), + timeOfDayStr); + } else { + val = context.getString(R.string.pref_smart_mark_as_played_disabled); // TODO: Is this a bug? Otherwise document why is this related to smart mark??? + } + } + String summary = context.getString(R.string.pref_autoUpdateIntervallOrTime_sum) + "\n" + + String.format(context.getString(R.string.pref_current_value), val); + findPreference(UserPreferences.PREF_UPDATE_INTERVAL).setSummary(summary); + } + + private void setParallelDownloadsText(int downloads) { + final Resources res = getActivity().getResources(); + + String s = Integer.toString(downloads) + + res.getString(R.string.parallel_downloads_suffix); + findPreference(UserPreferences.PREF_PARALLEL_DOWNLOADS).setSummary(s); + } + + private void showUpdateIntervalTimePreferencesDialog() { + final Context context = getActivity(); + + MaterialDialog.Builder builder = new MaterialDialog.Builder(context); + builder.title(R.string.pref_autoUpdateIntervallOrTime_title); + builder.content(R.string.pref_autoUpdateIntervallOrTime_message); + builder.positiveText(R.string.pref_autoUpdateIntervallOrTime_Interval); + builder.negativeText(R.string.pref_autoUpdateIntervallOrTime_TimeOfDay); + builder.neutralText(R.string.pref_autoUpdateIntervallOrTime_Disable); + builder.onPositive((dialog, which) -> { + AlertDialog.Builder builder1 = new AlertDialog.Builder(context); + builder1.setTitle(context.getString(R.string.pref_autoUpdateIntervallOrTime_Interval)); + final String[] values = context.getResources().getStringArray(R.array.update_intervall_values); + final String[] entries = getUpdateIntervalEntries(values); + long currInterval = UserPreferences.getUpdateInterval(); + int checkedItem = -1; + if(currInterval > 0) { + String currIntervalStr = String.valueOf(TimeUnit.MILLISECONDS.toHours(currInterval)); + checkedItem = ArrayUtils.indexOf(values, currIntervalStr); + } + builder1.setSingleChoiceItems(entries, checkedItem, (dialog1, which1) -> { + int hours = Integer.parseInt(values[which1]); + UserPreferences.setUpdateInterval(hours); + dialog1.dismiss(); + setUpdateIntervalText(); + }); + builder1.setNegativeButton(context.getString(R.string.cancel_label), null); + builder1.show(); + }); + builder.onNegative((dialog, which) -> { + int hourOfDay = 7, minute = 0; + int[] updateTime = UserPreferences.getUpdateTimeOfDay(); + if (updateTime.length == 2) { + hourOfDay = updateTime[0]; + minute = updateTime[1]; + } + TimePickerDialog timePickerDialog = new TimePickerDialog(context, + (view, selectedHourOfDay, selectedMinute) -> { + if (view.getTag() == null) { // onTimeSet() may get called twice! + view.setTag("TAGGED"); + UserPreferences.setUpdateTimeOfDay(selectedHourOfDay, selectedMinute); + setUpdateIntervalText(); + } + }, hourOfDay, minute, DateFormat.is24HourFormat(context)); + timePickerDialog.setTitle(context.getString(R.string.pref_autoUpdateIntervallOrTime_TimeOfDay)); + timePickerDialog.show(); + }); + builder.onNeutral((dialog, which) -> { + UserPreferences.disableAutoUpdate(); + setUpdateIntervalText(); + }); + builder.show(); + } + + private String[] getUpdateIntervalEntries(final String[] values) { + final Resources res = getActivity().getResources(); + String[] entries = new String[values.length]; + for (int x = 0; x < values.length; x++) { + Integer v = Integer.parseInt(values[x]); + switch (v) { + case 0: + entries[x] = res.getString(R.string.pref_update_interval_hours_manual); + break; + case 1: + entries[x] = v + " " + res.getString(R.string.pref_update_interval_hours_singular); + break; + default: + entries[x] = v + " " + res.getString(R.string.pref_update_interval_hours_plural); + break; + + } + } + return entries; + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/PlaybackPreferencesFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/PlaybackPreferencesFragment.java new file mode 100644 index 000000000..e1714d4bd --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/PlaybackPreferencesFragment.java @@ -0,0 +1,92 @@ +package de.danoeh.antennapod.fragment.preferences; + +import android.app.Activity; +import android.content.res.Resources; +import android.os.Build; +import android.os.Bundle; +import android.support.v7.preference.ListPreference; +import android.support.v7.preference.PreferenceFragmentCompat; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.activity.MediaplayerActivity; +import de.danoeh.antennapod.core.preferences.UserPreferences; +import de.danoeh.antennapod.core.util.gui.PictureInPictureUtil; +import de.danoeh.antennapod.dialog.VariableSpeedDialog; +import de.danoeh.antennapod.preferences.PreferenceControllerFlavorHelper; + +public class PlaybackPreferencesFragment extends PreferenceFragmentCompat { + private static final String PREF_PLAYBACK_SPEED_LAUNCHER = "prefPlaybackSpeedLauncher"; + private static final String PREF_PLAYBACK_REWIND_DELTA_LAUNCHER = "prefPlaybackRewindDeltaLauncher"; + private static final String PREF_PLAYBACK_FAST_FORWARD_DELTA_LAUNCHER = "prefPlaybackFastForwardDeltaLauncher"; + + @Override + public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + addPreferencesFromResource(R.xml.preferences_playback); + + setupPlaybackScreen(); + PreferenceControllerFlavorHelper.setupFlavoredUI(this); + buildSmartMarkAsPlayedPreference(); + } + + @Override + public void onResume() { + super.onResume(); + checkSonicItemVisibility(); + } + + private void setupPlaybackScreen() { + final Activity activity = getActivity(); + + findPreference(PREF_PLAYBACK_SPEED_LAUNCHER) + .setOnPreferenceClickListener(preference -> { + VariableSpeedDialog.showDialog(activity); + return true; + }); + findPreference(PREF_PLAYBACK_REWIND_DELTA_LAUNCHER) + .setOnPreferenceClickListener(preference -> { + MediaplayerActivity.showSkipPreference(activity, MediaplayerActivity.SkipDirection.SKIP_REWIND); + return true; + }); + findPreference(PREF_PLAYBACK_FAST_FORWARD_DELTA_LAUNCHER) + .setOnPreferenceClickListener(preference -> { + MediaplayerActivity.showSkipPreference(activity, MediaplayerActivity.SkipDirection.SKIP_FORWARD); + return true; + }); + if (!PictureInPictureUtil.supportsPictureInPicture(activity)) { + ListPreference behaviour = (ListPreference) findPreference(UserPreferences.PREF_VIDEO_BEHAVIOR); + behaviour.setEntries(R.array.video_background_behavior_options_without_pip); + behaviour.setEntryValues(R.array.video_background_behavior_values_without_pip); + } + } + + private void buildSmartMarkAsPlayedPreference() { + final Resources res = getActivity().getResources(); + + ListPreference pref = (ListPreference) findPreference(UserPreferences.PREF_SMART_MARK_AS_PLAYED_SECS); + String[] values = res.getStringArray(R.array.smart_mark_as_played_values); + String[] entries = new String[values.length]; + for (int x = 0; x < values.length; x++) { + if(x == 0) { + entries[x] = res.getString(R.string.pref_smart_mark_as_played_disabled); + } else { + Integer v = Integer.parseInt(values[x]); + if(v < 60) { + entries[x] = res.getQuantityString(R.plurals.time_seconds_quantified, v, v); + } else { + v /= 60; + entries[x] = res.getQuantityString(R.plurals.time_minutes_quantified, v, v); + } + } + } + pref.setEntries(entries); + } + + + + private void checkSonicItemVisibility() { + if (Build.VERSION.SDK_INT < 16) { + ListPreference p = (ListPreference) findPreference(UserPreferences.PREF_MEDIA_PLAYER); + p.setEntries(R.array.media_player_options_no_sonic); + p.setEntryValues(R.array.media_player_values_no_sonic); + } + } +} 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 new file mode 100644 index 000000000..b4226b546 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/StoragePreferencesFragment.java @@ -0,0 +1,244 @@ +package de.danoeh.antennapod.fragment.preferences; + +import android.Manifest; +import android.annotation.SuppressLint; +import android.app.Activity; +import android.app.ProgressDialog; +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.app.ActivityCompat; +import android.support.v4.content.FileProvider; +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.ExportWorker; +import de.danoeh.antennapod.core.export.ExportWriter; +import de.danoeh.antennapod.core.export.html.HtmlWriter; +import de.danoeh.antennapod.core.export.opml.OpmlWriter; +import de.danoeh.antennapod.core.preferences.UserPreferences; +import de.danoeh.antennapod.dialog.ChooseDataFolderDialog; +import io.reactivex.Observable; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.Disposable; +import io.reactivex.schedulers.Schedulers; + +import java.io.File; +import java.util.List; + +public class StoragePreferencesFragment extends PreferenceFragmentCompat { + private static final String TAG = "StoragePrefFragment"; + private static final String PREF_OPML_EXPORT = "prefOpmlExport"; + private static final String PREF_OPML_IMPORT = "prefOpmlImport"; + private static final String PREF_HTML_EXPORT = "prefHtmlExport"; + private static final String IMPORT_EXPORT = "importExport"; + private static final String PREF_CHOOSE_DATA_DIR = "prefChooseDataDir"; + private static final String[] EXTERNAL_STORAGE_PERMISSIONS = { + Manifest.permission.READ_EXTERNAL_STORAGE, + Manifest.permission.WRITE_EXTERNAL_STORAGE }; + private static final int PERMISSION_REQUEST_EXTERNAL_STORAGE = 41; + private Disposable disposable; + + @Override + public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + addPreferencesFromResource(R.xml.preferences_storage); + setupStorageScreen(); + } + + @Override + public void onResume() { + super.onResume(); + setDataFolderText(); + } + + private void setupStorageScreen() { + final Activity activity = getActivity(); + + findPreference(IMPORT_EXPORT).setOnPreferenceClickListener( + preference -> { + activity.startActivity(new Intent(activity, ImportExportActivity.class)); + return true; + } + ); + findPreference(PREF_OPML_EXPORT).setOnPreferenceClickListener( + preference -> export(new OpmlWriter())); + findPreference(PREF_HTML_EXPORT).setOnPreferenceClickListener( + preference -> export(new HtmlWriter())); + findPreference(PREF_OPML_IMPORT).setOnPreferenceClickListener( + preference -> { + activity.startActivity(new Intent(activity, OpmlImportFromPathActivity.class)); + return true; + }); + findPreference(PREF_CHOOSE_DATA_DIR).setOnPreferenceClickListener( + preference -> { + if (Build.VERSION_CODES.KITKAT <= Build.VERSION.SDK_INT && + Build.VERSION.SDK_INT <= Build.VERSION_CODES.LOLLIPOP_MR1) { + showChooseDataFolderDialog(); + } else { + int readPermission = ActivityCompat.checkSelfPermission( + activity, Manifest.permission.READ_EXTERNAL_STORAGE); + int writePermission = ActivityCompat.checkSelfPermission( + activity, Manifest.permission.WRITE_EXTERNAL_STORAGE); + if (readPermission == PackageManager.PERMISSION_GRANTED && + writePermission == PackageManager.PERMISSION_GRANTED) { + openDirectoryChooser(); + } else { + requestPermission(); + } + } + return true; + } + ); + findPreference(PREF_CHOOSE_DATA_DIR) + .setOnPreferenceClickListener( + preference -> { + if (Build.VERSION.SDK_INT >= 19) { + showChooseDataFolderDialog(); + } else { + Intent intent = new Intent(activity, DirectoryChooserActivity.class); + activity.startActivityForResult(intent, + DirectoryChooserActivity.RESULT_CODE_DIR_SELECTED); + } + return true; + } + ); + findPreference(UserPreferences.PREF_IMAGE_CACHE_SIZE).setOnPreferenceChangeListener( + (preference, o) -> { + if (o instanceof String) { + int newValue = Integer.parseInt((String) o) * 1024 * 1024; + if (newValue != UserPreferences.getImageCacheSize()) { + AlertDialog.Builder dialog = new AlertDialog.Builder(getActivity()); + dialog.setTitle(android.R.string.dialog_alert_title); + dialog.setMessage(R.string.pref_restart_required); + dialog.setPositiveButton(android.R.string.ok, null); + dialog.show(); + } + return true; + } + return false; + } + ); + } + + private boolean export(ExportWriter exportWriter) { + 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) -> { + 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); + return true; + } + + public void unsubscribeExportSubscription() { + if (disposable != null) { + disposable.dispose(); + } + } + + @SuppressLint("NewApi") + public void onActivityResult(int requestCode, int resultCode, Intent data) { + if (resultCode == Activity.RESULT_OK && + requestCode == DirectoryChooserActivity.RESULT_CODE_DIR_SELECTED) { + String dir = data.getStringExtra(DirectoryChooserActivity.RESULT_SELECTED_DIR); + + File path; + if(dir != null) { + path = new File(dir); + } else { + path = getActivity().getExternalFilesDir(null); + } + String message = null; + 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()) { + message = String.format(context.getString(R.string.folder_not_readable_error), dir); + } else if(!path.canWrite()) { + message = String.format(context.getString(R.string.folder_not_writable_error), dir); + } + + if(message == null) { + Log.d(TAG, "Setting data folder: " + dir); + UserPreferences.setDataFolder(dir); + setDataFolderText(); + } else { + AlertDialog.Builder ab = new AlertDialog.Builder(getActivity()); + ab.setMessage(message); + ab.setPositiveButton(android.R.string.ok, null); + ab.show(); + } + } + } + + private void setDataFolderText() { + File f = UserPreferences.getDataFolder(null); + if (f != null) { + findPreference(PREF_CHOOSE_DATA_DIR) + .setSummary(f.getAbsolutePath()); + } + } + + private void requestPermission() { + ActivityCompat.requestPermissions(getActivity(), EXTERNAL_STORAGE_PERMISSIONS, + PERMISSION_REQUEST_EXTERNAL_STORAGE); + } + + private void openDirectoryChooser() { + Activity activity = getActivity(); + Intent intent = new Intent(activity, DirectoryChooserActivity.class); + activity.startActivityForResult(intent, DirectoryChooserActivity.RESULT_CODE_DIR_SELECTED); + } + + private void showChooseDataFolderDialog() { + ChooseDataFolderDialog.showDialog( + getActivity(), new ChooseDataFolderDialog.RunnableWithString() { + @Override + public void run(final String folder) { + UserPreferences.setDataFolder(folder); + setDataFolderText(); + } + }); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/UserInterfacePreferencesFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/UserInterfacePreferencesFragment.java new file mode 100644 index 000000000..e1d44f7d3 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/UserInterfacePreferencesFragment.java @@ -0,0 +1,165 @@ +package de.danoeh.antennapod.fragment.preferences; + +import android.content.Context; +import android.content.Intent; +import android.os.Build; +import android.os.Bundle; +import android.support.design.widget.Snackbar; +import android.support.v7.app.AlertDialog; +import android.support.v7.preference.PreferenceFragmentCompat; +import android.widget.ListView; +import android.widget.Toast; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.activity.MainActivity; +import de.danoeh.antennapod.core.preferences.UserPreferences; +import org.apache.commons.lang3.ArrayUtils; + +import java.util.List; + +public class UserInterfacePreferencesFragment extends PreferenceFragmentCompat { + private static final String PREF_EXPANDED_NOTIFICATION = "prefExpandNotify"; + + @Override + public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + addPreferencesFromResource(R.xml.preferences_user_interface); + setupInterfaceScreen(); + } + + private void setupInterfaceScreen() { + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) { + // disable expanded notification option on unsupported android versions + findPreference(PREF_EXPANDED_NOTIFICATION).setEnabled(false); + findPreference(PREF_EXPANDED_NOTIFICATION).setOnPreferenceClickListener( + preference -> { + Toast toast = Toast.makeText(getActivity(), + R.string.pref_expand_notify_unsupport_toast, Toast.LENGTH_SHORT); + toast.show(); + return true; + } + ); + } + findPreference(UserPreferences.PREF_THEME) + .setOnPreferenceChangeListener( + (preference, newValue) -> { + Intent i = new Intent(getActivity(), MainActivity.class); + i.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK + | Intent.FLAG_ACTIVITY_NEW_TASK); + getActivity().finish(); + startActivity(i); + return true; + } + ); + findPreference(UserPreferences.PREF_HIDDEN_DRAWER_ITEMS) + .setOnPreferenceClickListener(preference -> { + showDrawerPreferencesDialog(); + return true; + }); + + findPreference(UserPreferences.PREF_COMPACT_NOTIFICATION_BUTTONS) + .setOnPreferenceClickListener(preference -> { + showNotificationButtonsDialog(); + return true; + }); + + findPreference(UserPreferences.PREF_BACK_BUTTON_BEHAVIOR) + .setOnPreferenceChangeListener((preference, newValue) -> { + if (newValue.equals("page")) { + final Context context = getActivity(); + final String[] navTitles = context.getResources().getStringArray(R.array.back_button_go_to_pages); + final String[] navTags = context.getResources().getStringArray(R.array.back_button_go_to_pages_tags); + final String choice[] = { UserPreferences.getBackButtonGoToPage() }; + + AlertDialog.Builder builder = new AlertDialog.Builder(context); + builder.setTitle(R.string.back_button_go_to_page_title); + builder.setSingleChoiceItems(navTitles, ArrayUtils.indexOf(navTags, UserPreferences.getBackButtonGoToPage()), (dialogInterface, i) -> { + if (i >= 0) { + choice[0] = navTags[i]; + } + }); + builder.setPositiveButton(R.string.confirm_label, (dialogInterface, i) -> UserPreferences.setBackButtonGoToPage(choice[0])); + builder.setNegativeButton(R.string.cancel_label, null); + builder.create().show(); + return true; + } else { + return true; + } + }); + + if (Build.VERSION.SDK_INT >= 26) { + findPreference(UserPreferences.PREF_EXPANDED_NOTIFICATION).setVisible(false); + } + } + + private void showDrawerPreferencesDialog() { + final Context context = getActivity(); + final List<String> hiddenDrawerItems = UserPreferences.getHiddenDrawerItems(); + final String[] navTitles = context.getResources().getStringArray(R.array.nav_drawer_titles); + final String[] NAV_DRAWER_TAGS = MainActivity.NAV_DRAWER_TAGS; + boolean[] checked = new boolean[MainActivity.NAV_DRAWER_TAGS.length]; + for(int i=0; i < NAV_DRAWER_TAGS.length; i++) { + String tag = NAV_DRAWER_TAGS[i]; + if(!hiddenDrawerItems.contains(tag)) { + checked[i] = true; + } + } + AlertDialog.Builder builder = new AlertDialog.Builder(context); + builder.setTitle(R.string.drawer_preferences); + builder.setMultiChoiceItems(navTitles, checked, (dialog, which, isChecked) -> { + if (isChecked) { + hiddenDrawerItems.remove(NAV_DRAWER_TAGS[which]); + } else { + hiddenDrawerItems.add(NAV_DRAWER_TAGS[which]); + } + }); + builder.setPositiveButton(R.string.confirm_label, (dialog, which) -> + UserPreferences.setHiddenDrawerItems(hiddenDrawerItems)); + builder.setNegativeButton(R.string.cancel_label, null); + builder.create().show(); + } + + private void showNotificationButtonsDialog() { + final Context context = getActivity(); + final List<Integer> preferredButtons = UserPreferences.getCompactNotificationButtons(); + final String[] allButtonNames = context.getResources().getStringArray( + R.array.compact_notification_buttons_options); + boolean[] checked = new boolean[allButtonNames.length]; // booleans default to false in java + + for(int i=0; i < checked.length; i++) { + if(preferredButtons.contains(i)) { + checked[i] = true; + } + } + + AlertDialog.Builder builder = new AlertDialog.Builder(context); + builder.setTitle(String.format(context.getResources().getString( + R.string.pref_compact_notification_buttons_dialog_title), 2)); + builder.setMultiChoiceItems(allButtonNames, checked, (dialog, which, isChecked) -> { + checked[which] = isChecked; + + if (isChecked) { + if (preferredButtons.size() < 2) { + preferredButtons.add(which); + } else { + // Only allow a maximum of two selections. This is because the notification + // on the lock screen can only display 3 buttons, and the play/pause button + // is always included. + checked[which] = false; + ListView selectionView = ((AlertDialog) dialog).getListView(); + selectionView.setItemChecked(which, false); + Snackbar.make( + selectionView, + String.format(context.getResources().getString( + R.string.pref_compact_notification_buttons_dialog_error), 2), + Snackbar.LENGTH_SHORT).show(); + } + } else { + preferredButtons.remove((Integer) which); + } + }); + builder.setPositiveButton(R.string.confirm_label, (dialog, which) -> + UserPreferences.setCompactNotificationButtons(preferredButtons)); + builder.setNegativeButton(R.string.cancel_label, null); + builder.create().show(); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedItemMenuHandler.java b/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedItemMenuHandler.java index ffdfa9516..156657a00 100644 --- a/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedItemMenuHandler.java +++ b/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedItemMenuHandler.java @@ -15,7 +15,6 @@ import de.danoeh.antennapod.core.gpoddernet.model.GpodnetEpisodeAction.Action; import de.danoeh.antennapod.core.preferences.GpodnetPreferences; import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.service.playback.PlaybackService; -import de.danoeh.antennapod.core.storage.DBTasks; import de.danoeh.antennapod.core.storage.DBWriter; import de.danoeh.antennapod.core.util.FeedItemUtil; import de.danoeh.antennapod.core.util.IntentUtils; @@ -68,16 +67,17 @@ public class FeedItemMenuHandler { } boolean hasMedia = selectedItem.getMedia() != null; boolean isPlaying = hasMedia && selectedItem.getState() == FeedItem.State.PLAYING; + boolean keepSorted = UserPreferences.isQueueKeepSorted(); if (!isPlaying) { mi.setItemVisibility(R.id.skip_episode_item, false); } boolean isInQueue = selectedItem.isTagged(FeedItem.TAG_QUEUE); - if(queueAccess == null || queueAccess.size() == 0 || queueAccess.get(0) == selectedItem.getId()) { + if (queueAccess == null || queueAccess.size() == 0 || queueAccess.get(0) == selectedItem.getId() || keepSorted) { mi.setItemVisibility(R.id.move_to_top_item, false); } - if(queueAccess == null || queueAccess.size() == 0 || queueAccess.get(queueAccess.size()-1) == selectedItem.getId()) { + if (queueAccess == null || queueAccess.size() == 0 || queueAccess.get(queueAccess.size()-1) == selectedItem.getId() || keepSorted) { mi.setItemVisibility(R.id.move_to_bottom_item, false); } if (!isInQueue) { @@ -101,7 +101,8 @@ public class FeedItemMenuHandler { mi.setItemVisibility(R.id.share_download_url_with_position_item, false); } - mi.setItemVisibility(R.id.share_file, hasMedia && selectedItem.getMedia().fileExists()); + boolean fileDownloaded = hasMedia && selectedItem.getMedia().fileExists(); + mi.setItemVisibility(R.id.share_file, fileDownloaded); if (selectedItem.isPlayed()) { mi.setItemVisibility(R.id.mark_read_item, false); @@ -122,14 +123,12 @@ public class FeedItemMenuHandler { mi.setItemVisibility(R.id.deactivate_auto_download, false); } - if (selectedItem.getPaymentLink() == null || !selectedItem.getFlattrStatus().flattrable()) { - mi.setItemVisibility(R.id.support_item, false); - } - boolean isFavorite = selectedItem.isTagged(FeedItem.TAG_FAVORITE); mi.setItemVisibility(R.id.add_to_favorites_item, !isFavorite); mi.setItemVisibility(R.id.remove_from_favorites_item, isFavorite); + mi.setItemVisibility(R.id.remove_item, fileDownloaded); + return true; } @@ -196,7 +195,7 @@ public class FeedItemMenuHandler { DBWriter.addQueueItem(context, selectedItem); break; case R.id.remove_from_queue_item: - DBWriter.removeQueueItem(context, selectedItem, true); + DBWriter.removeQueueItem(context, true, selectedItem); break; case R.id.add_to_favorites_item: DBWriter.addFavoriteItem(selectedItem); @@ -226,9 +225,6 @@ public class FeedItemMenuHandler { Toast.LENGTH_SHORT).show(); } break; - case R.id.support_item: - DBTasks.flattrItemIfLoggedIn(context, selectedItem); - break; case R.id.share_link_item: ShareUtils.shareFeedItemLink(context, selectedItem); break; 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 bd4fe9bcf..0928cfd62 100644 --- a/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedMenuHandler.java +++ b/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedMenuHandler.java @@ -46,11 +46,6 @@ public class FeedMenuHandler { } Log.d(TAG, "Preparing options menu"); - if (selectedFeed.getPaymentLink() != null && selectedFeed.getFlattrStatus().flattrable()) { - menu.findItem(R.id.support_item).setVisible(true); - } else { - menu.findItem(R.id.support_item).setVisible(false); - } menu.findItem(R.id.refresh_complete_item).setVisible(selectedFeed.isPaged()); @@ -98,9 +93,6 @@ public class FeedMenuHandler { Toast.LENGTH_SHORT).show(); } break; - case R.id.support_item: - DBTasks.flattrFeedIfLoggedIn(context, selectedFeed); - break; case R.id.share_link_item: ShareUtils.shareFeedlink(context, selectedFeed); break; diff --git a/app/src/main/java/de/danoeh/antennapod/preferences/PreferenceController.java b/app/src/main/java/de/danoeh/antennapod/preferences/PreferenceController.java deleted file mode 100644 index 31b2cbcb2..000000000 --- a/app/src/main/java/de/danoeh/antennapod/preferences/PreferenceController.java +++ /dev/null @@ -1,1218 +0,0 @@ -package de.danoeh.antennapod.preferences; - -import android.Manifest; -import android.annotation.SuppressLint; -import android.app.Activity; -import android.app.ProgressDialog; -import android.app.TimePickerDialog; -import android.content.ActivityNotFoundException; -import android.content.Context; -import android.content.Intent; -import android.content.SharedPreferences; -import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; -import android.content.res.Resources; -import android.net.Uri; -import android.net.wifi.WifiConfiguration; -import android.net.wifi.WifiManager; -import android.os.Build; -import android.os.Bundle; -import android.support.design.widget.Snackbar; -import android.support.v4.app.ActivityCompat; -import android.support.v4.content.FileProvider; -import android.support.v7.app.AlertDialog; -import android.support.v7.app.AppCompatActivity; -import android.support.v7.preference.CheckBoxPreference; -import android.support.v7.preference.ListPreference; -import android.support.v7.preference.Preference; -import android.support.v7.preference.PreferenceFragmentCompat; -import android.support.v7.preference.PreferenceManager; -import android.support.v7.preference.PreferenceScreen; -import android.text.Html; -import android.text.format.DateFormat; -import android.text.format.DateUtils; -import android.util.Log; -import android.widget.ListView; -import android.widget.Toast; - -import com.afollestad.materialdialogs.MaterialDialog; -import com.bytehamster.lib.preferencesearch.SearchConfiguration; -import com.bytehamster.lib.preferencesearch.SearchPreference; - -import org.apache.commons.lang3.ArrayUtils; - -import java.io.File; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Calendar; -import java.util.Collections; -import java.util.GregorianCalendar; -import java.util.List; -import java.util.concurrent.TimeUnit; - -import de.danoeh.antennapod.CrashReportWriter; -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.activity.AboutActivity; -import de.danoeh.antennapod.activity.DirectoryChooserActivity; -import de.danoeh.antennapod.activity.ImportExportActivity; -import de.danoeh.antennapod.activity.MainActivity; -import de.danoeh.antennapod.activity.MediaplayerActivity; -import de.danoeh.antennapod.activity.OpmlImportFromPathActivity; -import de.danoeh.antennapod.activity.PreferenceActivity; -import de.danoeh.antennapod.activity.StatisticsActivity; -import de.danoeh.antennapod.asynctask.ExportWorker; -import de.danoeh.antennapod.core.export.ExportWriter; -import de.danoeh.antennapod.core.export.html.HtmlWriter; -import de.danoeh.antennapod.core.export.opml.OpmlWriter; -import de.danoeh.antennapod.core.preferences.GpodnetPreferences; -import de.danoeh.antennapod.core.preferences.UserPreferences; -import de.danoeh.antennapod.core.service.GpodnetSyncService; -import de.danoeh.antennapod.core.util.flattr.FlattrUtils; -import de.danoeh.antennapod.core.util.gui.PictureInPictureUtil; -import de.danoeh.antennapod.dialog.AuthenticationDialog; -import de.danoeh.antennapod.dialog.AutoFlattrPreferenceDialog; -import de.danoeh.antennapod.dialog.ChooseDataFolderDialog; -import de.danoeh.antennapod.dialog.GpodnetSetHostnameDialog; -import de.danoeh.antennapod.dialog.ProxyDialog; -import de.danoeh.antennapod.dialog.VariableSpeedDialog; -import io.reactivex.Observable; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.disposables.Disposable; -import io.reactivex.schedulers.Schedulers; - -import static de.danoeh.antennapod.activity.PreferenceActivity.PARAM_RESOURCE; - -/** - * Sets up a preference UI that lets the user change user preferences. - */ - -public class PreferenceController implements SharedPreferences.OnSharedPreferenceChangeListener { - - private static final String TAG = "PreferenceController"; - - private static final String PREF_SCREEN_USER_INTERFACE = "prefScreenInterface"; - private static final String PREF_SCREEN_PLAYBACK = "prefScreenPlayback"; - private static final String PREF_SCREEN_NETWORK = "prefScreenNetwork"; - private static final String PREF_SCREEN_INTEGRATIONS = "prefScreenIntegrations"; - private static final String PREF_SCREEN_STORAGE = "prefScreenStorage"; - private static final String PREF_SCREEN_AUTODL = "prefAutoDownloadSettings"; - private static final String PREF_SCREEN_FLATTR = "prefFlattrSettings"; - private static final String PREF_SCREEN_GPODDER = "prefGpodderSettings"; - - private static final String PREF_FLATTR_AUTH = "pref_flattr_authenticate"; - private static final String PREF_FLATTR_REVOKE = "prefRevokeAccess"; - private static final String PREF_AUTO_FLATTR_PREFS = "prefAutoFlattrPrefs"; - private static final String PREF_OPML_EXPORT = "prefOpmlExport"; - private static final String PREF_OPML_IMPORT = "prefOpmlImport"; - private static final String PREF_HTML_EXPORT = "prefHtmlExport"; - private static final String STATISTICS = "statistics"; - private static final String IMPORT_EXPORT = "importExport"; - private static final String PREF_ABOUT = "prefAbout"; - private static final String PREF_CHOOSE_DATA_DIR = "prefChooseDataDir"; - private static final String PREF_PLAYBACK_SPEED_LAUNCHER = "prefPlaybackSpeedLauncher"; - private static final String PREF_PLAYBACK_REWIND_DELTA_LAUNCHER = "prefPlaybackRewindDeltaLauncher"; - private static final String PREF_PLAYBACK_FAST_FORWARD_DELTA_LAUNCHER = "prefPlaybackFastForwardDeltaLauncher"; - private static final String PREF_GPODNET_LOGIN = "pref_gpodnet_authenticate"; - private static final String PREF_GPODNET_SETLOGIN_INFORMATION = "pref_gpodnet_setlogin_information"; - private static final String PREF_GPODNET_SYNC = "pref_gpodnet_sync"; - private static final String PREF_GPODNET_FORCE_FULL_SYNC = "pref_gpodnet_force_full_sync"; - private static final String PREF_GPODNET_LOGOUT = "pref_gpodnet_logout"; - private static final String PREF_GPODNET_HOSTNAME = "pref_gpodnet_hostname"; - private static final String PREF_GPODNET_NOTIFICATIONS = "pref_gpodnet_notifications"; - private static final String PREF_EXPANDED_NOTIFICATION = "prefExpandNotify"; - private static final String PREF_PROXY = "prefProxy"; - private static final String PREF_KNOWN_ISSUES = "prefKnownIssues"; - private static final String PREF_FAQ = "prefFaq"; - private static final String PREF_SEND_CRASH_REPORT = "prefSendCrashReport"; - private static final String[] EXTERNAL_STORAGE_PERMISSIONS = { - Manifest.permission.READ_EXTERNAL_STORAGE, - Manifest.permission.WRITE_EXTERNAL_STORAGE }; - private static final int PERMISSION_REQUEST_EXTERNAL_STORAGE = 41; - private final PreferenceUI ui; - private final SharedPreferences.OnSharedPreferenceChangeListener gpoddernetListener = - (sharedPreferences, key) -> { - if (GpodnetPreferences.PREF_LAST_SYNC_ATTEMPT_TIMESTAMP.equals(key)) { - updateLastGpodnetSyncReport(GpodnetPreferences.getLastSyncAttemptResult(), - GpodnetPreferences.getLastSyncAttemptTimestamp()); - } - }; - private CheckBoxPreference[] selectedNetworks; - private Disposable disposable; - - public PreferenceController(PreferenceUI ui) { - this.ui = ui; - PreferenceManager.getDefaultSharedPreferences(ui.getActivity().getApplicationContext()) - .registerOnSharedPreferenceChangeListener(this); - } - - @Override - public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { - - } - - - - public void onCreate(int screen) { - switch (screen) { - case R.xml.preferences: - setupMainScreen(); - break; - case R.xml.preferences_network: - setupNetworkScreen(); - break; - case R.xml.preferences_autodownload: - setupAutoDownloadScreen(); - buildAutodownloadSelectedNetworksPreference(); - setSelectedNetworksEnabled(UserPreferences.isEnableAutodownloadWifiFilter()); - buildEpisodeCleanupPreference(); - break; - case R.xml.preferences_playback: - setupPlaybackScreen(); - PreferenceControllerFlavorHelper.setupFlavoredUI(ui); - buildSmartMarkAsPlayedPreference(); - break; - case R.xml.preferences_integrations: - setupIntegrationsScreen(); - break; - case R.xml.preferences_flattr: - setupFlattrScreen(); - break; - case R.xml.preferences_gpodder: - setupGpodderScreen(); - break; - case R.xml.preferences_storage: - setupStorageScreen(); - break; - case R.xml.preferences_user_interface: - setupInterfaceScreen(); - break; - } - } - - private void setupInterfaceScreen() { - final Activity activity = ui.getActivity(); - - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) { - // disable expanded notification option on unsupported android versions - ui.findPreference(PreferenceController.PREF_EXPANDED_NOTIFICATION).setEnabled(false); - ui.findPreference(PreferenceController.PREF_EXPANDED_NOTIFICATION).setOnPreferenceClickListener( - preference -> { - Toast toast = Toast.makeText(activity, - R.string.pref_expand_notify_unsupport_toast, Toast.LENGTH_SHORT); - toast.show(); - return true; - } - ); - } - ui.findPreference(UserPreferences.PREF_THEME) - .setOnPreferenceChangeListener( - (preference, newValue) -> { - Intent i = new Intent(activity, MainActivity.class); - i.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK - | Intent.FLAG_ACTIVITY_NEW_TASK); - activity.finish(); - activity.startActivity(i); - return true; - } - ); - ui.findPreference(UserPreferences.PREF_HIDDEN_DRAWER_ITEMS) - .setOnPreferenceClickListener(preference -> { - showDrawerPreferencesDialog(); - return true; - }); - - ui.findPreference(UserPreferences.PREF_COMPACT_NOTIFICATION_BUTTONS) - .setOnPreferenceClickListener(preference -> { - showNotificationButtonsDialog(); - return true; - }); - - ui.findPreference(UserPreferences.PREF_BACK_BUTTON_BEHAVIOR) - .setOnPreferenceChangeListener((preference, newValue) -> { - if (newValue.equals("page")) { - final Context context = ui.getActivity(); - final String[] navTitles = context.getResources().getStringArray(R.array.back_button_go_to_pages); - final String[] navTags = context.getResources().getStringArray(R.array.back_button_go_to_pages_tags); - final String choice[] = { UserPreferences.getBackButtonGoToPage() }; - - AlertDialog.Builder builder = new AlertDialog.Builder(context); - builder.setTitle(R.string.back_button_go_to_page_title); - builder.setSingleChoiceItems(navTitles, ArrayUtils.indexOf(navTags, UserPreferences.getBackButtonGoToPage()), (dialogInterface, i) -> { - if (i >= 0) { - choice[0] = navTags[i]; - } - }); - builder.setPositiveButton(R.string.confirm_label, (dialogInterface, i) -> UserPreferences.setBackButtonGoToPage(choice[0])); - builder.setNegativeButton(R.string.cancel_label, null); - builder.create().show(); - return true; - } else { - return true; - } - }); - - if (Build.VERSION.SDK_INT >= 26) { - ui.findPreference(UserPreferences.PREF_EXPANDED_NOTIFICATION).setVisible(false); - } - } - - private void setupStorageScreen() { - final Activity activity = ui.getActivity(); - - ui.findPreference(PreferenceController.IMPORT_EXPORT).setOnPreferenceClickListener( - preference -> { - activity.startActivity(new Intent(activity, ImportExportActivity.class)); - return true; - } - ); - ui.findPreference(PreferenceController.PREF_OPML_EXPORT).setOnPreferenceClickListener( - preference -> export(new OpmlWriter())); - ui.findPreference(PreferenceController.PREF_HTML_EXPORT).setOnPreferenceClickListener( - preference -> export(new HtmlWriter())); - ui.findPreference(PreferenceController.PREF_OPML_IMPORT).setOnPreferenceClickListener( - preference -> { - activity.startActivity(new Intent(activity, OpmlImportFromPathActivity.class)); - return true; - }); - ui.findPreference(PreferenceController.PREF_CHOOSE_DATA_DIR).setOnPreferenceClickListener( - preference -> { - if (Build.VERSION_CODES.KITKAT <= Build.VERSION.SDK_INT && - Build.VERSION.SDK_INT <= Build.VERSION_CODES.LOLLIPOP_MR1) { - showChooseDataFolderDialog(); - } else { - int readPermission = ActivityCompat.checkSelfPermission( - activity, Manifest.permission.READ_EXTERNAL_STORAGE); - int writePermission = ActivityCompat.checkSelfPermission( - activity, Manifest.permission.WRITE_EXTERNAL_STORAGE); - if (readPermission == PackageManager.PERMISSION_GRANTED && - writePermission == PackageManager.PERMISSION_GRANTED) { - openDirectoryChooser(); - } else { - requestPermission(); - } - } - return true; - } - ); - ui.findPreference(PreferenceController.PREF_CHOOSE_DATA_DIR) - .setOnPreferenceClickListener( - preference -> { - if (Build.VERSION.SDK_INT >= 19) { - showChooseDataFolderDialog(); - } else { - Intent intent = new Intent(activity, DirectoryChooserActivity.class); - activity.startActivityForResult(intent, - DirectoryChooserActivity.RESULT_CODE_DIR_SELECTED); - } - return true; - } - ); - ui.findPreference(UserPreferences.PREF_IMAGE_CACHE_SIZE).setOnPreferenceChangeListener( - (preference, o) -> { - if (o instanceof String) { - int newValue = Integer.parseInt((String) o) * 1024 * 1024; - if (newValue != UserPreferences.getImageCacheSize()) { - AlertDialog.Builder dialog = new AlertDialog.Builder(ui.getActivity()); - dialog.setTitle(android.R.string.dialog_alert_title); - dialog.setMessage(R.string.pref_restart_required); - dialog.setPositiveButton(android.R.string.ok, null); - dialog.show(); - } - return true; - } - return false; - } - ); - } - - private void setupIntegrationsScreen() { - final AppCompatActivity activity = ui.getActivity(); - - ui.findPreference(PREF_SCREEN_FLATTR).setOnPreferenceClickListener(preference -> { - openScreen(R.xml.preferences_flattr, activity); - return true; - }); - ui.findPreference(PREF_SCREEN_GPODDER).setOnPreferenceClickListener(preference -> { - openScreen(R.xml.preferences_gpodder, activity); - return true; - }); - } - - private void setupFlattrScreen() { - final AppCompatActivity activity = ui.getActivity(); - - ui.findPreference(PreferenceController.PREF_FLATTR_REVOKE).setOnPreferenceClickListener( - preference -> { - FlattrUtils.revokeAccessToken(activity); - checkFlattrItemVisibility(); - return true; - } - ); - - ui.findPreference(PreferenceController.PREF_AUTO_FLATTR_PREFS) - .setOnPreferenceClickListener(preference -> { - AutoFlattrPreferenceDialog.newAutoFlattrPreferenceDialog(activity, - new AutoFlattrPreferenceDialog.AutoFlattrPreferenceDialogInterface() { - @Override - public void onCancelled() { - - } - - @Override - public void onConfirmed(boolean autoFlattrEnabled, float autoFlattrValue) { - UserPreferences.setAutoFlattrSettings(autoFlattrEnabled, autoFlattrValue); - checkFlattrItemVisibility(); - } - }); - return true; - }); - } - - private void setupGpodderScreen() { - final AppCompatActivity activity = ui.getActivity(); - - ui.findPreference(PreferenceController.PREF_GPODNET_SETLOGIN_INFORMATION) - .setOnPreferenceClickListener(preference -> { - AuthenticationDialog dialog = new AuthenticationDialog(activity, - R.string.pref_gpodnet_setlogin_information_title, false, false, GpodnetPreferences.getUsername(), - null) { - - @Override - protected void onConfirmed(String username, String password, boolean saveUsernamePassword) { - GpodnetPreferences.setPassword(password); - } - }; - dialog.show(); - return true; - }); - ui.findPreference(PreferenceController.PREF_GPODNET_SYNC). - setOnPreferenceClickListener(preference -> { - GpodnetSyncService.sendSyncIntent(ui.getActivity().getApplicationContext()); - Toast toast = Toast.makeText(ui.getActivity(), R.string.pref_gpodnet_sync_started, - Toast.LENGTH_SHORT); - toast.show(); - return true; - }); - ui.findPreference(PreferenceController.PREF_GPODNET_FORCE_FULL_SYNC). - setOnPreferenceClickListener(preference -> { - GpodnetPreferences.setLastSubscriptionSyncTimestamp(0L); - GpodnetPreferences.setLastEpisodeActionsSyncTimestamp(0L); - GpodnetPreferences.setLastSyncAttempt(false, 0); - updateLastGpodnetSyncReport(false, 0); - GpodnetSyncService.sendSyncIntent(ui.getActivity().getApplicationContext()); - Toast toast = Toast.makeText(ui.getActivity(), R.string.pref_gpodnet_sync_started, - Toast.LENGTH_SHORT); - toast.show(); - return true; - }); - ui.findPreference(PreferenceController.PREF_GPODNET_LOGOUT).setOnPreferenceClickListener( - preference -> { - GpodnetPreferences.logout(); - Toast toast = Toast.makeText(activity, R.string.pref_gpodnet_logout_toast, Toast.LENGTH_SHORT); - toast.show(); - updateGpodnetPreferenceScreen(); - return true; - }); - ui.findPreference(PreferenceController.PREF_GPODNET_HOSTNAME).setOnPreferenceClickListener( - preference -> { - GpodnetSetHostnameDialog.createDialog(activity).setOnDismissListener(dialog -> updateGpodnetPreferenceScreen()); - return true; - }); - } - - private void setupPlaybackScreen() { - final Activity activity = ui.getActivity(); - - ui.findPreference(PreferenceController.PREF_PLAYBACK_SPEED_LAUNCHER) - .setOnPreferenceClickListener(preference -> { - VariableSpeedDialog.showDialog(activity); - return true; - }); - ui.findPreference(PreferenceController.PREF_PLAYBACK_REWIND_DELTA_LAUNCHER) - .setOnPreferenceClickListener(preference -> { - MediaplayerActivity.showSkipPreference(activity, MediaplayerActivity.SkipDirection.SKIP_REWIND); - return true; - }); - ui.findPreference(PreferenceController.PREF_PLAYBACK_FAST_FORWARD_DELTA_LAUNCHER) - .setOnPreferenceClickListener(preference -> { - MediaplayerActivity.showSkipPreference(activity, MediaplayerActivity.SkipDirection.SKIP_FORWARD); - return true; - }); - if (!PictureInPictureUtil.supportsPictureInPicture(activity)) { - ListPreference behaviour = (ListPreference) ui.findPreference(UserPreferences.PREF_VIDEO_BEHAVIOR); - behaviour.setEntries(R.array.video_background_behavior_options_without_pip); - behaviour.setEntryValues(R.array.video_background_behavior_values_without_pip); - } - } - - private void setupAutoDownloadScreen() { - ui.findPreference(UserPreferences.PREF_ENABLE_AUTODL).setOnPreferenceChangeListener( - (preference, newValue) -> { - if (newValue instanceof Boolean) { - checkAutodownloadItemVisibility((Boolean) newValue); - } - return true; - }); - ui.findPreference(UserPreferences.PREF_ENABLE_AUTODL_WIFI_FILTER) - .setOnPreferenceChangeListener( - (preference, newValue) -> { - if (newValue instanceof Boolean) { - setSelectedNetworksEnabled((Boolean) newValue); - return true; - } else { - return false; - } - } - ); - ui.findPreference(UserPreferences.PREF_EPISODE_CACHE_SIZE) - .setOnPreferenceChangeListener( - (preference, o) -> { - if (o instanceof String) { - setEpisodeCacheSizeText(UserPreferences.readEpisodeCacheSize((String) o)); - } - return true; - } - ); - } - - private void setupNetworkScreen() { - final AppCompatActivity activity = ui.getActivity(); - ui.findPreference(PREF_SCREEN_AUTODL).setOnPreferenceClickListener(preference -> { - openScreen(R.xml.preferences_autodownload, activity); - return true; - }); - ui.findPreference(UserPreferences.PREF_UPDATE_INTERVAL) - .setOnPreferenceClickListener(preference -> { - showUpdateIntervalTimePreferencesDialog(); - return true; - }); - ui.findPreference(UserPreferences.PREF_PARALLEL_DOWNLOADS) - .setOnPreferenceChangeListener( - (preference, o) -> { - if (o instanceof Integer) { - setParallelDownloadsText((Integer) o); - } - return true; - } - ); - // validate and set correct value: number of downloads between 1 and 50 (inclusive) - ui.findPreference(PREF_PROXY).setOnPreferenceClickListener(preference -> { - ProxyDialog dialog = new ProxyDialog(ui.getActivity()); - dialog.createDialog().show(); - return true; - }); - } - - private void setupMainScreen() { - final AppCompatActivity activity = ui.getActivity(); - setupSearch(); - ui.findPreference(PREF_SCREEN_USER_INTERFACE).setOnPreferenceClickListener(preference -> { - openScreen(R.xml.preferences_user_interface, activity); - return true; - }); - ui.findPreference(PREF_SCREEN_PLAYBACK).setOnPreferenceClickListener(preference -> { - openScreen(R.xml.preferences_playback, activity); - return true; - }); - ui.findPreference(PREF_SCREEN_NETWORK).setOnPreferenceClickListener(preference -> { - openScreen(R.xml.preferences_network, activity); - return true; - }); - ui.findPreference(PREF_SCREEN_INTEGRATIONS).setOnPreferenceClickListener(preference -> { - openScreen(R.xml.preferences_integrations, activity); - return true; - }); - ui.findPreference(PREF_SCREEN_STORAGE).setOnPreferenceClickListener(preference -> { - openScreen(R.xml.preferences_storage, activity); - return true; - }); - - ui.findPreference(PreferenceController.PREF_ABOUT).setOnPreferenceClickListener( - preference -> { - activity.startActivity(new Intent(activity, AboutActivity.class)); - return true; - } - ); - ui.findPreference(PreferenceController.STATISTICS).setOnPreferenceClickListener( - preference -> { - activity.startActivity(new Intent(activity, StatisticsActivity.class)); - return true; - } - ); - ui.findPreference(PREF_KNOWN_ISSUES).setOnPreferenceClickListener(preference -> { - openInBrowser("https://github.com/AntennaPod/AntennaPod/labels/bug"); - return true; - }); - ui.findPreference(PREF_FAQ).setOnPreferenceClickListener(preference -> { - openInBrowser("http://antennapod.org/faq.html"); - return true; - }); - ui.findPreference(PREF_SEND_CRASH_REPORT).setOnPreferenceClickListener(preference -> { - Context context = ui.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 = ui.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); - } - } - ui.getActivity().startActivity(Intent.createChooser(emailIntent, intentTitle)); - return true; - }); - } - - private void setupSearch() { - final AppCompatActivity activity = ui.getActivity(); - - SearchPreference searchPreference = (SearchPreference) ui.findPreference("searchPreference"); - SearchConfiguration config = searchPreference.getSearchConfiguration(); - config.setActivity(activity); - config.setFragmentContainerViewId(R.id.content); - config.setBreadcrumbsEnabled(true); - - config.index() - .addBreadcrumb(getTitleOfPage(R.xml.preferences_user_interface)) - .addFile(R.xml.preferences_user_interface); - config.index() - .addBreadcrumb(getTitleOfPage(R.xml.preferences_playback)) - .addFile(R.xml.preferences_playback); - config.index() - .addBreadcrumb(getTitleOfPage(R.xml.preferences_network)) - .addFile(R.xml.preferences_network); - config.index() - .addBreadcrumb(getTitleOfPage(R.xml.preferences_storage)) - .addFile(R.xml.preferences_storage); - config.index() - .addBreadcrumb(getTitleOfPage(R.xml.preferences_network)) - .addBreadcrumb(R.string.automation) - .addBreadcrumb(getTitleOfPage(R.xml.preferences_autodownload)) - .addFile(R.xml.preferences_autodownload); - config.index() - .addBreadcrumb(getTitleOfPage(R.xml.preferences_integrations)) - .addBreadcrumb(getTitleOfPage(R.xml.preferences_gpodder)) - .addFile(R.xml.preferences_gpodder); - config.index() - .addBreadcrumb(getTitleOfPage(R.xml.preferences_integrations)) - .addBreadcrumb(getTitleOfPage(R.xml.preferences_flattr)) - .addFile(R.xml.preferences_flattr); - } - - public PreferenceFragmentCompat openScreen(int preferences, AppCompatActivity activity) { - PreferenceFragmentCompat prefFragment = new PreferenceActivity.MainFragment(); - Bundle args = new Bundle(); - args.putInt(PARAM_RESOURCE, preferences); - prefFragment.setArguments(args); - activity.getSupportFragmentManager().beginTransaction() - .replace(R.id.content, prefFragment) - .addToBackStack(TAG).commit(); - return prefFragment; - } - - public static int getTitleOfPage(int preferences) { - switch (preferences) { - case R.xml.preferences_network: - return R.string.network_pref; - case R.xml.preferences_autodownload: - return R.string.pref_automatic_download_title; - case R.xml.preferences_playback: - return R.string.playback_pref; - case R.xml.preferences_storage: - return R.string.storage_pref; - case R.xml.preferences_user_interface: - return R.string.user_interface_label; - case R.xml.preferences_integrations: - return R.string.integrations_label; - case R.xml.preferences_flattr: - return R.string.flattr_label; - case R.xml.preferences_gpodder: - return R.string.gpodnet_main_label; - default: - return R.string.settings_label; - } - } - - private boolean export(ExportWriter exportWriter) { - Context context = ui.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) -> { - Uri fileUri = FileProvider.getUriForFile(context.getApplicationContext(), - "de.danoeh.antennapod.provider", 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); - return true; - } - - private void openInBrowser(String url) { - try { - Intent myIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); - ui.getActivity().startActivity(myIntent); - } catch (ActivityNotFoundException e) { - Toast.makeText(ui.getActivity(), R.string.pref_no_browser_found, Toast.LENGTH_LONG).show(); - Log.e(TAG, Log.getStackTraceString(e)); - } - } - - public void onResume(int screen) { - switch (screen) { - case R.xml.preferences_network: - setUpdateIntervalText(); - setParallelDownloadsText(UserPreferences.getParallelDownloads()); - break; - case R.xml.preferences_autodownload: - setEpisodeCacheSizeText(UserPreferences.getEpisodeCacheSize()); - checkAutodownloadItemVisibility(UserPreferences.isEnableAutodownload()); - break; - case R.xml.preferences_storage: - setDataFolderText(); - break; - case R.xml.preferences_integrations: - setIntegrationsItemVisibility(); - return; - case R.xml.preferences_flattr: - checkFlattrItemVisibility(); - break; - case R.xml.preferences_gpodder: - GpodnetPreferences.registerOnSharedPreferenceChangeListener(gpoddernetListener); - updateGpodnetPreferenceScreen(); - break; - case R.xml.preferences_playback: - checkSonicItemVisibility(); - break; - } - } - - public void unregisterGpodnet() { - GpodnetPreferences.unregisterOnSharedPreferenceChangeListener(gpoddernetListener); - } - - public void unsubscribeExportSubscription() { - if (disposable != null) { - disposable.dispose(); - } - } - - @SuppressLint("NewApi") - public void onActivityResult(int requestCode, int resultCode, Intent data) { - if (resultCode == Activity.RESULT_OK && - requestCode == DirectoryChooserActivity.RESULT_CODE_DIR_SELECTED) { - String dir = data.getStringExtra(DirectoryChooserActivity.RESULT_SELECTED_DIR); - - File path; - if(dir != null) { - path = new File(dir); - } else { - path = ui.getActivity().getExternalFilesDir(null); - } - String message = null; - final Context context= ui.getActivity().getApplicationContext(); - if(!path.exists()) { - message = String.format(context.getString(R.string.folder_does_not_exist_error), dir); - } else if(!path.canRead()) { - message = String.format(context.getString(R.string.folder_not_readable_error), dir); - } else if(!path.canWrite()) { - message = String.format(context.getString(R.string.folder_not_writable_error), dir); - } - - if(message == null) { - Log.d(TAG, "Setting data folder: " + dir); - UserPreferences.setDataFolder(dir); - setDataFolderText(); - } else { - AlertDialog.Builder ab = new AlertDialog.Builder(ui.getActivity()); - ab.setMessage(message); - ab.setPositiveButton(android.R.string.ok, null); - ab.show(); - } - } - } - - - private void updateGpodnetPreferenceScreen() { - final boolean loggedIn = GpodnetPreferences.loggedIn(); - ui.findPreference(PreferenceController.PREF_GPODNET_LOGIN).setEnabled(!loggedIn); - ui.findPreference(PreferenceController.PREF_GPODNET_SETLOGIN_INFORMATION).setEnabled(loggedIn); - ui.findPreference(PreferenceController.PREF_GPODNET_SYNC).setEnabled(loggedIn); - ui.findPreference(PreferenceController.PREF_GPODNET_FORCE_FULL_SYNC).setEnabled(loggedIn); - ui.findPreference(PreferenceController.PREF_GPODNET_LOGOUT).setEnabled(loggedIn); - ui.findPreference(PREF_GPODNET_NOTIFICATIONS).setEnabled(loggedIn); - if(loggedIn) { - String format = ui.getActivity().getString(R.string.pref_gpodnet_login_status); - String summary = String.format(format, GpodnetPreferences.getUsername(), - GpodnetPreferences.getDeviceID()); - ui.findPreference(PreferenceController.PREF_GPODNET_LOGOUT).setSummary(Html.fromHtml(summary)); - updateLastGpodnetSyncReport(GpodnetPreferences.getLastSyncAttemptResult(), - GpodnetPreferences.getLastSyncAttemptTimestamp()); - } else { - ui.findPreference(PreferenceController.PREF_GPODNET_LOGOUT).setSummary(null); - updateLastGpodnetSyncReport(false, 0); - } - ui.findPreference(PreferenceController.PREF_GPODNET_HOSTNAME).setSummary(GpodnetPreferences.getHostname()); - } - - private void updateLastGpodnetSyncReport(boolean successful, long lastTime) { - Preference sync = ui.findPreference(PREF_GPODNET_SYNC); - if (lastTime != 0) { - sync.setSummary(ui.getActivity().getString(R.string.pref_gpodnet_sync_changes_sum) + "\n" + - ui.getActivity().getString(R.string.pref_gpodnet_sync_sum_last_sync_line, - ui.getActivity().getString(successful ? - R.string.gpodnetsync_pref_report_successful : - R.string.gpodnetsync_pref_report_failed), - DateUtils.getRelativeDateTimeString(ui.getActivity(), - lastTime, - DateUtils.MINUTE_IN_MILLIS, - DateUtils.WEEK_IN_MILLIS, - DateUtils.FORMAT_SHOW_TIME))); - } else { - sync.setSummary(ui.getActivity().getString(R.string.pref_gpodnet_sync_changes_sum)); - } - } - - private String[] getUpdateIntervalEntries(final String[] values) { - final Resources res = ui.getActivity().getResources(); - String[] entries = new String[values.length]; - for (int x = 0; x < values.length; x++) { - Integer v = Integer.parseInt(values[x]); - switch (v) { - case 0: - entries[x] = res.getString(R.string.pref_update_interval_hours_manual); - break; - case 1: - entries[x] = v + " " + res.getString(R.string.pref_update_interval_hours_singular); - break; - default: - entries[x] = v + " " + res.getString(R.string.pref_update_interval_hours_plural); - break; - - } - } - return entries; - } - - private void buildEpisodeCleanupPreference() { - final Resources res = ui.getActivity().getResources(); - - ListPreference pref = (ListPreference) ui.findPreference(UserPreferences.PREF_EPISODE_CLEANUP); - String[] values = res.getStringArray( - R.array.episode_cleanup_values); - String[] entries = new String[values.length]; - for (int x = 0; x < values.length; x++) { - int v = Integer.parseInt(values[x]); - if (v == UserPreferences.EPISODE_CLEANUP_QUEUE) { - entries[x] = res.getString(R.string.episode_cleanup_queue_removal); - } else if (v == UserPreferences.EPISODE_CLEANUP_NULL){ - entries[x] = res.getString(R.string.episode_cleanup_never); - } else if (v == 0) { - entries[x] = res.getString(R.string.episode_cleanup_after_listening); - } else { - entries[x] = res.getQuantityString(R.plurals.episode_cleanup_days_after_listening, v, v); - } - } - pref.setEntries(entries); - } - - private void buildSmartMarkAsPlayedPreference() { - final Resources res = ui.getActivity().getResources(); - - ListPreference pref = (ListPreference) ui.findPreference(UserPreferences.PREF_SMART_MARK_AS_PLAYED_SECS); - String[] values = res.getStringArray(R.array.smart_mark_as_played_values); - String[] entries = new String[values.length]; - for (int x = 0; x < values.length; x++) { - if(x == 0) { - entries[x] = res.getString(R.string.pref_smart_mark_as_played_disabled); - } else { - Integer v = Integer.parseInt(values[x]); - if(v < 60) { - entries[x] = res.getQuantityString(R.plurals.time_seconds_quantified, v, v); - } else { - v /= 60; - entries[x] = res.getQuantityString(R.plurals.time_minutes_quantified, v, v); - } - } - } - pref.setEntries(entries); - } - - private void setSelectedNetworksEnabled(boolean b) { - if (selectedNetworks != null) { - for (Preference p : selectedNetworks) { - p.setEnabled(b); - } - } - } - - private void setIntegrationsItemVisibility() { - ui.findPreference(PreferenceController.PREF_SCREEN_FLATTR).setEnabled(FlattrUtils.hasAPICredentials()); - } - - @SuppressWarnings("deprecation") - private void checkFlattrItemVisibility() { - boolean hasFlattrToken = FlattrUtils.hasToken(); - ui.findPreference(PreferenceController.PREF_FLATTR_AUTH).setEnabled(!hasFlattrToken); - ui.findPreference(PreferenceController.PREF_FLATTR_REVOKE).setEnabled(hasFlattrToken); - ui.findPreference(PreferenceController.PREF_AUTO_FLATTR_PREFS).setEnabled(hasFlattrToken); - } - - private void checkAutodownloadItemVisibility(boolean autoDownload) { - ui.findPreference(UserPreferences.PREF_EPISODE_CACHE_SIZE).setEnabled(autoDownload); - ui.findPreference(UserPreferences.PREF_ENABLE_AUTODL_ON_BATTERY).setEnabled(autoDownload); - ui.findPreference(UserPreferences.PREF_ENABLE_AUTODL_WIFI_FILTER).setEnabled(autoDownload); - ui.findPreference(UserPreferences.PREF_EPISODE_CLEANUP).setEnabled(autoDownload); - ui.findPreference(UserPreferences.PREF_ENABLE_AUTODL_ON_MOBILE).setEnabled(autoDownload); - setSelectedNetworksEnabled(autoDownload && UserPreferences.isEnableAutodownloadWifiFilter()); - } - - private void checkSonicItemVisibility() { - if (Build.VERSION.SDK_INT < 16) { - ListPreference p = (ListPreference) ui.findPreference(UserPreferences.PREF_MEDIA_PLAYER); - p.setEntries(R.array.media_player_options_no_sonic); - p.setEntryValues(R.array.media_player_values_no_sonic); - } - } - - private void setUpdateIntervalText() { - Context context = ui.getActivity().getApplicationContext(); - String val; - long interval = UserPreferences.getUpdateInterval(); - if(interval > 0) { - int hours = (int) TimeUnit.MILLISECONDS.toHours(interval); - String hoursStr = context.getResources().getQuantityString(R.plurals.time_hours_quantified, hours, hours); - val = String.format(context.getString(R.string.pref_autoUpdateIntervallOrTime_every), hoursStr); - } else { - int[] timeOfDay = UserPreferences.getUpdateTimeOfDay(); - if(timeOfDay.length == 2) { - Calendar cal = new GregorianCalendar(); - cal.set(Calendar.HOUR_OF_DAY, timeOfDay[0]); - cal.set(Calendar.MINUTE, timeOfDay[1]); - String timeOfDayStr = DateFormat.getTimeFormat(context).format(cal.getTime()); - val = String.format(context.getString(R.string.pref_autoUpdateIntervallOrTime_at), - timeOfDayStr); - } else { - val = context.getString(R.string.pref_smart_mark_as_played_disabled); // TODO: Is this a bug? Otherwise document why is this related to smart mark??? - } - } - String summary = context.getString(R.string.pref_autoUpdateIntervallOrTime_sum) + "\n" - + String.format(context.getString(R.string.pref_current_value), val); - ui.findPreference(UserPreferences.PREF_UPDATE_INTERVAL).setSummary(summary); - } - - 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(); - - String s; - if (cacheSize == res.getInteger( - R.integer.episode_cache_size_unlimited)) { - s = res.getString(R.string.pref_episode_cache_unlimited); - } else { - s = Integer.toString(cacheSize) - + res.getString(R.string.episodes_suffix); - } - ui.findPreference(UserPreferences.PREF_EPISODE_CACHE_SIZE).setSummary(s); - } - - private void setDataFolderText() { - File f = UserPreferences.getDataFolder(null); - if (f != null) { - ui.findPreference(PreferenceController.PREF_CHOOSE_DATA_DIR) - .setSummary(f.getAbsolutePath()); - } - } - - private static String blankIfNull(String val) { - return val == null ? "" : val; - } - - private void buildAutodownloadSelectedNetworksPreference() { - final Activity activity = ui.getActivity(); - - if (selectedNetworks != null) { - clearAutodownloadSelectedNetworsPreference(); - } - // get configured networks - WifiManager wifiservice = (WifiManager) activity.getApplicationContext().getSystemService(Context.WIFI_SERVICE); - List<WifiConfiguration> networks = wifiservice.getConfiguredNetworks(); - - if (networks == null) { - Log.e(TAG, "Couldn't get list of configure Wi-Fi networks"); - return; - } - Collections.sort(networks, (x, y) -> - blankIfNull(x.SSID).compareTo(blankIfNull(y.SSID))); - selectedNetworks = new CheckBoxPreference[networks.size()]; - List<String> prefValues = Arrays.asList(UserPreferences - .getAutodownloadSelectedNetworks()); - PreferenceScreen prefScreen = ui.getPreferenceScreen(); - Preference.OnPreferenceClickListener clickListener = preference -> { - if (preference instanceof CheckBoxPreference) { - String key = preference.getKey(); - List<String> prefValuesList = new ArrayList<>( - Arrays.asList(UserPreferences - .getAutodownloadSelectedNetworks()) - ); - boolean newValue = ((CheckBoxPreference) preference) - .isChecked(); - Log.d(TAG, "Selected network " + key + ". New state: " + newValue); - - int index = prefValuesList.indexOf(key); - if (index >= 0 && !newValue) { - // remove network - prefValuesList.remove(index); - } else if (index < 0 && newValue) { - prefValuesList.add(key); - } - - UserPreferences.setAutodownloadSelectedNetworks( - prefValuesList.toArray(new String[prefValuesList.size()]) - ); - return true; - } else { - return false; - } - }; - // create preference for each known network. attach listener and set - // value - for (int i = 0; i < networks.size(); i++) { - WifiConfiguration config = networks.get(i); - - CheckBoxPreference pref = new CheckBoxPreference(activity); - String key = Integer.toString(config.networkId); - pref.setTitle(config.SSID); - pref.setKey(key); - pref.setOnPreferenceClickListener(clickListener); - pref.setPersistent(false); - pref.setChecked(prefValues.contains(key)); - selectedNetworks[i] = pref; - prefScreen.addPreference(pref); - } - } - - private void clearAutodownloadSelectedNetworsPreference() { - if (selectedNetworks != null) { - PreferenceScreen prefScreen = ui.getPreferenceScreen(); - - for (CheckBoxPreference network : selectedNetworks) { - if (network != null) { - prefScreen.removePreference(network); - } - } - } - } - - private void showDrawerPreferencesDialog() { - final Context context = ui.getActivity(); - final List<String> hiddenDrawerItems = UserPreferences.getHiddenDrawerItems(); - final String[] navTitles = context.getResources().getStringArray(R.array.nav_drawer_titles); - final String[] NAV_DRAWER_TAGS = MainActivity.NAV_DRAWER_TAGS; - boolean[] checked = new boolean[MainActivity.NAV_DRAWER_TAGS.length]; - for(int i=0; i < NAV_DRAWER_TAGS.length; i++) { - String tag = NAV_DRAWER_TAGS[i]; - if(!hiddenDrawerItems.contains(tag)) { - checked[i] = true; - } - } - AlertDialog.Builder builder = new AlertDialog.Builder(context); - builder.setTitle(R.string.drawer_preferences); - builder.setMultiChoiceItems(navTitles, checked, (dialog, which, isChecked) -> { - if (isChecked) { - hiddenDrawerItems.remove(NAV_DRAWER_TAGS[which]); - } else { - hiddenDrawerItems.add(NAV_DRAWER_TAGS[which]); - } - }); - builder.setPositiveButton(R.string.confirm_label, (dialog, which) -> - UserPreferences.setHiddenDrawerItems(hiddenDrawerItems)); - builder.setNegativeButton(R.string.cancel_label, null); - builder.create().show(); - } - - private void showNotificationButtonsDialog() { - final Context context = ui.getActivity(); - final List<Integer> preferredButtons = UserPreferences.getCompactNotificationButtons(); - final String[] allButtonNames = context.getResources().getStringArray( - R.array.compact_notification_buttons_options); - boolean[] checked = new boolean[allButtonNames.length]; // booleans default to false in java - - for(int i=0; i < checked.length; i++) { - if(preferredButtons.contains(i)) { - checked[i] = true; - } - } - - AlertDialog.Builder builder = new AlertDialog.Builder(context); - builder.setTitle(String.format(context.getResources().getString( - R.string.pref_compact_notification_buttons_dialog_title), 2)); - builder.setMultiChoiceItems(allButtonNames, checked, (dialog, which, isChecked) -> { - checked[which] = isChecked; - - if (isChecked) { - if (preferredButtons.size() < 2) { - preferredButtons.add(which); - } else { - // Only allow a maximum of two selections. This is because the notification - // on the lock screen can only display 3 buttons, and the play/pause button - // is always included. - checked[which] = false; - ListView selectionView = ((AlertDialog) dialog).getListView(); - selectionView.setItemChecked(which, false); - Snackbar.make( - selectionView, - String.format(context.getResources().getString( - R.string.pref_compact_notification_buttons_dialog_error), 2), - Snackbar.LENGTH_SHORT).show(); - } - } else { - preferredButtons.remove((Integer) which); - } - }); - builder.setPositiveButton(R.string.confirm_label, (dialog, which) -> - UserPreferences.setCompactNotificationButtons(preferredButtons)); - builder.setNegativeButton(R.string.cancel_label, null); - builder.create().show(); - } - - // CHOOSE DATA FOLDER - - private void requestPermission() { - ActivityCompat.requestPermissions(ui.getActivity(), EXTERNAL_STORAGE_PERMISSIONS, - PERMISSION_REQUEST_EXTERNAL_STORAGE); - } - - private void openDirectoryChooser() { - Activity activity = ui.getActivity(); - Intent intent = new Intent(activity, DirectoryChooserActivity.class); - activity.startActivityForResult(intent, DirectoryChooserActivity.RESULT_CODE_DIR_SELECTED); - } - - private void showChooseDataFolderDialog() { - ChooseDataFolderDialog.showDialog( - ui.getActivity(), new ChooseDataFolderDialog.RunnableWithString() { - @Override - public void run(final String folder) { - UserPreferences.setDataFolder(folder); - setDataFolderText(); - } - }); - } - - // UPDATE TIME/INTERVAL DIALOG - - private void showUpdateIntervalTimePreferencesDialog() { - final Context context = ui.getActivity(); - - MaterialDialog.Builder builder = new MaterialDialog.Builder(context); - builder.title(R.string.pref_autoUpdateIntervallOrTime_title); - builder.content(R.string.pref_autoUpdateIntervallOrTime_message); - builder.positiveText(R.string.pref_autoUpdateIntervallOrTime_Interval); - builder.negativeText(R.string.pref_autoUpdateIntervallOrTime_TimeOfDay); - builder.neutralText(R.string.pref_autoUpdateIntervallOrTime_Disable); - builder.onPositive((dialog, which) -> { - AlertDialog.Builder builder1 = new AlertDialog.Builder(context); - builder1.setTitle(context.getString(R.string.pref_autoUpdateIntervallOrTime_Interval)); - final String[] values = context.getResources().getStringArray(R.array.update_intervall_values); - final String[] entries = getUpdateIntervalEntries(values); - long currInterval = UserPreferences.getUpdateInterval(); - int checkedItem = -1; - if(currInterval > 0) { - String currIntervalStr = String.valueOf(TimeUnit.MILLISECONDS.toHours(currInterval)); - checkedItem = ArrayUtils.indexOf(values, currIntervalStr); - } - builder1.setSingleChoiceItems(entries, checkedItem, (dialog1, which1) -> { - int hours = Integer.parseInt(values[which1]); - UserPreferences.setUpdateInterval(hours); - dialog1.dismiss(); - setUpdateIntervalText(); - }); - builder1.setNegativeButton(context.getString(R.string.cancel_label), null); - builder1.show(); - }); - builder.onNegative((dialog, which) -> { - int hourOfDay = 7, minute = 0; - int[] updateTime = UserPreferences.getUpdateTimeOfDay(); - if (updateTime.length == 2) { - hourOfDay = updateTime[0]; - minute = updateTime[1]; - } - TimePickerDialog timePickerDialog = new TimePickerDialog(context, - (view, selectedHourOfDay, selectedMinute) -> { - if (view.getTag() == null) { // onTimeSet() may get called twice! - view.setTag("TAGGED"); - UserPreferences.setUpdateTimeOfDay(selectedHourOfDay, selectedMinute); - setUpdateIntervalText(); - } - }, hourOfDay, minute, DateFormat.is24HourFormat(context)); - timePickerDialog.setTitle(context.getString(R.string.pref_autoUpdateIntervallOrTime_TimeOfDay)); - timePickerDialog.show(); - }); - builder.onNeutral((dialog, which) -> { - UserPreferences.setUpdateInterval(0); - setUpdateIntervalText(); - }); - builder.show(); - } - - - public interface PreferenceUI { - - void setFragment(PreferenceFragmentCompat fragment); - PreferenceFragmentCompat getFragment(); - - /** - * Finds a preference based on its key. - */ - Preference findPreference(CharSequence key); - - PreferenceScreen getPreferenceScreen(); - - AppCompatActivity getActivity(); - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/preferences/PreferenceUpgrader.java b/app/src/main/java/de/danoeh/antennapod/preferences/PreferenceUpgrader.java new file mode 100644 index 000000000..e56703598 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/preferences/PreferenceUpgrader.java @@ -0,0 +1,69 @@ +package de.danoeh.antennapod.preferences; + +import android.content.Context; +import android.content.SharedPreferences; +import android.preference.PreferenceManager; +import de.danoeh.antennapod.BuildConfig; +import de.danoeh.antennapod.core.preferences.UserPreferences; +import de.danoeh.antennapod.core.util.gui.NotificationUtils; + +public class PreferenceUpgrader { + private static final String PREF_CONFIGURED_VERSION = "configuredVersion"; + private static final String PREF_NAME = "PreferenceUpgrader"; + + private static SharedPreferences prefs; + + public static void checkUpgrades(Context context) { + prefs = PreferenceManager.getDefaultSharedPreferences(context); + SharedPreferences upgraderPrefs = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE); + int oldVersion = upgraderPrefs.getInt(PREF_CONFIGURED_VERSION, 1070200); + int newVersion = BuildConfig.VERSION_CODE; + + if (oldVersion != newVersion) { + NotificationUtils.createChannels(context); + + upgraderPrefs.edit().putInt(PREF_CONFIGURED_VERSION, newVersion).apply(); + upgrade(oldVersion); + } + } + + private static void upgrade(int oldVersion) { + if (oldVersion < 1070196) { + // migrate episode cleanup value (unit changed from days to hours) + int oldValueInDays = UserPreferences.getEpisodeCleanupValue(); + if (oldValueInDays > 0) { + UserPreferences.setEpisodeCleanupValue(oldValueInDays * 24); + } // else 0 or special negative values, no change needed + } + if (oldVersion < 1070197) { + if (prefs.getBoolean("prefMobileUpdate", false)) { + prefs.edit().putString("prefMobileUpdateAllowed", "everything").apply(); + } + } + if (oldVersion < 1070300) { + UserPreferences.restartUpdateAlarm(); + + if (UserPreferences.getMediaPlayer().equals("builtin")) { + prefs.edit().putString(UserPreferences.PREF_MEDIA_PLAYER, + UserPreferences.PREF_MEDIA_PLAYER_EXOPLAYER).apply(); + } + + if (prefs.getBoolean("prefEnableAutoDownloadOnMobile", false)) { + UserPreferences.setAllowMobileAutoDownload(true); + } + switch (prefs.getString("prefMobileUpdateAllowed", "images")) { + case "everything": + UserPreferences.setAllowMobileFeedRefresh(true); + UserPreferences.setAllowMobileEpisodeDownload(true); + UserPreferences.setAllowMobileImages(true); + break; + case "images": + UserPreferences.setAllowMobileImages(true); + break; + case "nothing": + UserPreferences.setAllowMobileImages(false); + break; + } + } + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/view/EmptyViewHandler.java b/app/src/main/java/de/danoeh/antennapod/view/EmptyViewHandler.java new file mode 100644 index 000000000..8b886e699 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/view/EmptyViewHandler.java @@ -0,0 +1,112 @@ +package de.danoeh.antennapod.view;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.support.annotation.AttrRes;
+import android.support.v4.content.ContextCompat;
+import android.support.v7.widget.RecyclerView;
+import android.util.TypedValue;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.ListView;
+import android.widget.RelativeLayout;
+import android.widget.TextView;
+
+import de.danoeh.antennapod.R;
+
+public class EmptyViewHandler {
+ private boolean layoutAdded = false;
+ private RecyclerView recyclerView;
+ private RecyclerView.Adapter adapter;
+
+ private final Context context;
+ private final View emptyView;
+ private final TextView tvTitle;
+ private final TextView tvMessage;
+ private final ImageView ivIcon;
+
+ public EmptyViewHandler(Context context) {
+ emptyView = View.inflate(context, R.layout.empty_view_layout, null);
+ this.context = context;
+ tvTitle = emptyView.findViewById(R.id.emptyViewTitle);
+ tvMessage = emptyView.findViewById(R.id.emptyViewMessage);
+ ivIcon = emptyView.findViewById(R.id.emptyViewIcon);
+ }
+
+ public void setTitle(int title) {
+ tvTitle.setText(title);
+ }
+
+ public void setMessage(int message) {
+ tvMessage.setText(message);
+ }
+
+ public void setIcon(@AttrRes int iconAttr) {
+ TypedValue typedValue = new TypedValue();
+ context.getTheme().resolveAttribute(iconAttr, typedValue, true);
+ Drawable d = ContextCompat.getDrawable(context, typedValue.resourceId);
+ ivIcon.setImageDrawable(d);
+ ivIcon.setVisibility(View.VISIBLE);
+ }
+
+ public void hide() {
+ emptyView.setVisibility(View.GONE);
+ }
+
+ public void attachToListView(ListView listView) {
+ if (layoutAdded) {
+ throw new IllegalStateException("Can not attach to ListView multiple times");
+ }
+ layoutAdded = true;
+ ((ViewGroup) listView.getParent()).addView(emptyView);
+ listView.setEmptyView(emptyView);
+ }
+
+ public void attachToRecyclerView(RecyclerView recyclerView) {
+ if (layoutAdded) {
+ throw new IllegalStateException("Can not attach to ListView multiple times");
+ }
+ layoutAdded = true;
+ this.recyclerView = recyclerView;
+ ViewGroup parent = ((ViewGroup) recyclerView.getParent());
+ parent.addView(emptyView);
+ updateAdapter(recyclerView.getAdapter());
+
+ if (parent instanceof RelativeLayout) {
+ RelativeLayout.LayoutParams layoutParams =
+ (RelativeLayout.LayoutParams)emptyView.getLayoutParams();
+ layoutParams.addRule(RelativeLayout.CENTER_IN_PARENT, RelativeLayout.TRUE);
+ emptyView.setLayoutParams(layoutParams);
+ }
+ }
+
+ public void updateAdapter(RecyclerView.Adapter adapter) {
+ if (this.adapter != null) {
+ this.adapter.unregisterAdapterDataObserver(adapterObserver);
+ }
+ this.adapter = adapter;
+ if (adapter != null) {
+ adapter.registerAdapterDataObserver(adapterObserver);
+ }
+ updateVisibility();
+ }
+
+ private final SimpleAdapterDataObserver adapterObserver = new SimpleAdapterDataObserver() {
+ @Override
+ public void anythingChanged() {
+ updateVisibility();
+ }
+ };
+
+ private void updateVisibility() {
+ boolean empty;
+ if (adapter == null) {
+ empty = true;
+ } else {
+ empty = adapter.getItemCount() == 0;
+ }
+ recyclerView.setVisibility(empty ? View.GONE : View.VISIBLE);
+ emptyView.setVisibility(empty ? View.VISIBLE : View.GONE);
+ }
+}
diff --git a/app/src/main/java/de/danoeh/antennapod/view/SimpleAdapterDataObserver.java b/app/src/main/java/de/danoeh/antennapod/view/SimpleAdapterDataObserver.java new file mode 100644 index 000000000..45c3a35bd --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/view/SimpleAdapterDataObserver.java @@ -0,0 +1,41 @@ +package de.danoeh.antennapod.view; + +import android.support.annotation.Nullable; +import android.support.v7.widget.RecyclerView; + +/** + * AdapterDataObserver that relays all events to the method anythingChanged(). + */ +public abstract class SimpleAdapterDataObserver extends RecyclerView.AdapterDataObserver { + public abstract void anythingChanged(); + + @Override + public void onChanged() { + anythingChanged(); + } + + @Override + public void onItemRangeChanged(int positionStart, int itemCount) { + anythingChanged(); + } + + @Override + public void onItemRangeChanged(int positionStart, int itemCount, @Nullable Object payload) { + anythingChanged(); + } + + @Override + public void onItemRangeInserted(int positionStart, int itemCount) { + anythingChanged(); + } + + @Override + public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) { + anythingChanged(); + } + + @Override + public void onItemRangeRemoved(int positionStart, int itemCount) { + anythingChanged(); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/view/WrappingGridView.java b/app/src/main/java/de/danoeh/antennapod/view/WrappingGridView.java new file mode 100644 index 000000000..37792b4d1 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/view/WrappingGridView.java @@ -0,0 +1,35 @@ +package de.danoeh.antennapod.view; + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.GridView; + +/** + * Source: https://stackoverflow.com/a/46350213/ + */ +public class WrappingGridView extends GridView { + + public WrappingGridView(Context context) { + super(context); + } + + public WrappingGridView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public WrappingGridView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int heightSpec = heightMeasureSpec; + if (getLayoutParams().height == LayoutParams.WRAP_CONTENT) { + // The great Android "hackatlon", the love, the magic. + // The two leftmost bits in the height measure spec have + // a special meaning, hence we can't use them to describe height. + heightSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST); + } + super.onMeasure(widthMeasureSpec, heightSpec); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/viewmodel/FeedSettingsViewModel.java b/app/src/main/java/de/danoeh/antennapod/viewmodel/FeedSettingsViewModel.java new file mode 100644 index 000000000..fe11a645c --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/viewmodel/FeedSettingsViewModel.java @@ -0,0 +1,30 @@ +package de.danoeh.antennapod.viewmodel; + +import android.arch.lifecycle.ViewModel; +import de.danoeh.antennapod.core.feed.Feed; +import de.danoeh.antennapod.core.storage.DBReader; +import io.reactivex.Maybe; + +public class FeedSettingsViewModel extends ViewModel { + private Feed feed; + + public Maybe<Feed> getFeed(long feedId) { + if (feed == null) { + return loadFeed(feedId); + } else { + return Maybe.just(feed); + } + } + + private Maybe<Feed> loadFeed(long feedId) { + return Maybe.create(emitter -> { + Feed feed = DBReader.getFeed(feedId); + if (feed != null) { + this.feed = feed; + emitter.onSuccess(feed); + } else { + emitter.onComplete(); + } + }); + } +} |