summaryrefslogtreecommitdiff
path: root/ui/discovery
diff options
context:
space:
mode:
Diffstat (limited to 'ui/discovery')
-rw-r--r--ui/discovery/README.md3
-rw-r--r--ui/discovery/build.gradle26
-rw-r--r--ui/discovery/src/main/java/de/danoeh/antennapod/ui/discovery/DiscoveryFragment.java276
-rw-r--r--ui/discovery/src/main/java/de/danoeh/antennapod/ui/discovery/FeedDiscoverAdapter.java79
-rw-r--r--ui/discovery/src/main/java/de/danoeh/antennapod/ui/discovery/OnlineSearchAdapter.java122
-rw-r--r--ui/discovery/src/main/java/de/danoeh/antennapod/ui/discovery/OnlineSearchFragment.java219
-rw-r--r--ui/discovery/src/main/java/de/danoeh/antennapod/ui/discovery/QuickFeedDiscoveryFragment.java175
-rw-r--r--ui/discovery/src/main/res/layout/fragment_online_search.xml101
-rw-r--r--ui/discovery/src/main/res/layout/online_search_listitem.xml59
-rw-r--r--ui/discovery/src/main/res/layout/quick_feed_discovery.xml92
-rw-r--r--ui/discovery/src/main/res/layout/quick_feed_discovery_item.xml21
-rw-r--r--ui/discovery/src/main/res/layout/select_country_dialog.xml24
-rw-r--r--ui/discovery/src/main/res/menu/countries_menu.xml17
-rw-r--r--ui/discovery/src/main/res/menu/online_search.xml12
14 files changed, 1226 insertions, 0 deletions
diff --git a/ui/discovery/README.md b/ui/discovery/README.md
new file mode 100644
index 000000000..fd8feb580
--- /dev/null
+++ b/ui/discovery/README.md
@@ -0,0 +1,3 @@
+# :ui:discovery
+
+This module provides the screens to discover new podcasts.
diff --git a/ui/discovery/build.gradle b/ui/discovery/build.gradle
new file mode 100644
index 000000000..60b04984b
--- /dev/null
+++ b/ui/discovery/build.gradle
@@ -0,0 +1,26 @@
+plugins {
+ id("com.android.library")
+}
+apply from: "../../common.gradle"
+apply from: "../../playFlavor.gradle"
+
+android {
+ namespace "de.danoeh.antennapod.ui.discovery"
+}
+
+dependencies {
+ implementation project(":event")
+ implementation project(":model")
+ implementation project(':net:discovery')
+ implementation project(':storage:database')
+ implementation project(':ui:app-start-intent')
+ implementation project(':ui:common')
+
+ annotationProcessor "androidx.annotation:annotation:$annotationVersion"
+ implementation "androidx.appcompat:appcompat:$appcompatVersion"
+ implementation "com.google.android.material:material:$googleMaterialVersion"
+ implementation "io.reactivex.rxjava2:rxandroid:$rxAndroidVersion"
+ implementation "io.reactivex.rxjava2:rxjava:$rxJavaVersion"
+ implementation "com.github.bumptech.glide:glide:$glideVersion"
+ implementation "org.greenrobot:eventbus:$eventbusVersion"
+}
diff --git a/ui/discovery/src/main/java/de/danoeh/antennapod/ui/discovery/DiscoveryFragment.java b/ui/discovery/src/main/java/de/danoeh/antennapod/ui/discovery/DiscoveryFragment.java
new file mode 100644
index 000000000..9cdcdbcb9
--- /dev/null
+++ b/ui/discovery/src/main/java/de/danoeh/antennapod/ui/discovery/DiscoveryFragment.java
@@ -0,0 +1,276 @@
+package de.danoeh.antennapod.ui.discovery;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.Button;
+import android.widget.GridView;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+import androidx.annotation.NonNull;
+import androidx.appcompat.widget.Toolbar;
+import androidx.fragment.app.Fragment;
+import com.google.android.material.appbar.MaterialToolbar;
+import com.google.android.material.dialog.MaterialAlertDialogBuilder;
+import com.google.android.material.textfield.MaterialAutoCompleteTextView;
+import com.google.android.material.textfield.TextInputLayout;
+import de.danoeh.antennapod.net.discovery.BuildConfig;
+import de.danoeh.antennapod.storage.database.DBReader;
+import de.danoeh.antennapod.event.DiscoveryDefaultUpdateEvent;
+import de.danoeh.antennapod.net.discovery.ItunesTopListLoader;
+import de.danoeh.antennapod.net.discovery.PodcastSearchResult;
+import de.danoeh.antennapod.ui.appstartintent.OnlineFeedviewActivityStarter;
+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 java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+/**
+ * Searches iTunes store for top podcasts and displays results in a list.
+ */
+public class DiscoveryFragment extends Fragment implements Toolbar.OnMenuItemClickListener {
+ public static final String TAG = "DiscoveryFragment";
+ private static final int NUM_OF_TOP_PODCASTS = 25;
+ private SharedPreferences prefs;
+
+ /**
+ * Adapter responsible with the search results.
+ */
+ private OnlineSearchAdapter 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;
+ private List<PodcastSearchResult> topList;
+ private Disposable disposable;
+ private String countryCode = "US";
+ private boolean hidden;
+ private boolean needsConfirm;
+ private MaterialToolbar toolbar;
+
+ public DiscoveryFragment() {
+ // Required empty public constructor
+ }
+
+ /**
+ * Replace adapter data with provided search results from SearchTask.
+ *
+ * @param result List of Podcast objects containing search results
+ */
+ 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 (PodcastSearchResult p : result) {
+ adapter.add(p);
+ }
+ adapter.notifyDataSetInvalidated();
+ } else {
+ gridView.setVisibility(View.GONE);
+ txtvEmpty.setVisibility(View.VISIBLE);
+ }
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ prefs = getActivity().getSharedPreferences(ItunesTopListLoader.PREFS, Context.MODE_PRIVATE);
+ countryCode = prefs.getString(ItunesTopListLoader.PREF_KEY_COUNTRY_CODE, Locale.getDefault().getCountry());
+ hidden = prefs.getBoolean(ItunesTopListLoader.PREF_KEY_HIDDEN_DISCOVERY_COUNTRY, false);
+ needsConfirm = prefs.getBoolean(ItunesTopListLoader.PREF_KEY_NEEDS_CONFIRM, true);
+ }
+
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ // Inflate the layout for this fragment
+ View root = inflater.inflate(R.layout.fragment_online_search, container, false);
+ gridView = root.findViewById(R.id.gridView);
+ adapter = new OnlineSearchAdapter(getActivity(), new ArrayList<>());
+ gridView.setAdapter(adapter);
+
+ toolbar = root.findViewById(R.id.toolbar);
+ toolbar.setNavigationOnClickListener(v -> getParentFragmentManager().popBackStack());
+ toolbar.inflateMenu(R.menu.countries_menu);
+ MenuItem discoverHideItem = toolbar.getMenu().findItem(R.id.discover_hide_item);
+ discoverHideItem.setChecked(hidden);
+ toolbar.setOnMenuItemClickListener(this);
+
+ //Show information about the podcast when the list item is clicked
+ gridView.setOnItemClickListener((parent, view1, position, id) -> {
+ PodcastSearchResult podcast = searchResults.get(position);
+ if (podcast.feedUrl == null) {
+ return;
+ }
+ startActivity(new OnlineFeedviewActivityStarter(getContext(), podcast.feedUrl).getIntent());
+ });
+
+ 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);
+
+ loadToplist(countryCode);
+ return root;
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ if (disposable != null) {
+ disposable.dispose();
+ }
+ adapter = null;
+ }
+
+ private void loadToplist(String country) {
+ if (disposable != null) {
+ disposable.dispose();
+ }
+
+ gridView.setVisibility(View.GONE);
+ txtvError.setVisibility(View.GONE);
+ butRetry.setVisibility(View.GONE);
+ butRetry.setText(R.string.retry_label);
+ txtvEmpty.setVisibility(View.GONE);
+ progressBar.setVisibility(View.VISIBLE);
+
+ if (hidden) {
+ gridView.setVisibility(View.GONE);
+ txtvError.setVisibility(View.VISIBLE);
+ txtvError.setText(getResources().getString(R.string.discover_is_hidden));
+ butRetry.setVisibility(View.GONE);
+ txtvEmpty.setVisibility(View.GONE);
+ progressBar.setVisibility(View.GONE);
+ return;
+ }
+ //noinspection ConstantConditions
+ if (BuildConfig.FLAVOR.equals("free") && needsConfirm) {
+ txtvError.setVisibility(View.VISIBLE);
+ txtvError.setText("");
+ butRetry.setVisibility(View.VISIBLE);
+ butRetry.setText(R.string.discover_confirm);
+ butRetry.setOnClickListener(v -> {
+ prefs.edit().putBoolean(ItunesTopListLoader.PREF_KEY_NEEDS_CONFIRM, false).apply();
+ needsConfirm = false;
+ loadToplist(country);
+ });
+ txtvEmpty.setVisibility(View.GONE);
+ progressBar.setVisibility(View.GONE);
+ return;
+ }
+
+ ItunesTopListLoader loader = new ItunesTopListLoader(getContext());
+ disposable = Observable.fromCallable(() ->
+ loader.loadToplist(country, NUM_OF_TOP_PODCASTS, DBReader.getFeedList()))
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(
+ podcasts -> {
+ progressBar.setVisibility(View.GONE);
+ topList = podcasts;
+ updateData(topList);
+ }, error -> {
+ Log.e(TAG, Log.getStackTraceString(error));
+ progressBar.setVisibility(View.GONE);
+ txtvError.setText(error.getMessage());
+ txtvError.setVisibility(View.VISIBLE);
+ butRetry.setOnClickListener(v -> loadToplist(country));
+ butRetry.setVisibility(View.VISIBLE);
+ });
+ }
+
+ @Override
+ public boolean onMenuItemClick(MenuItem item) {
+ final int itemId = item.getItemId();
+ if (itemId == R.id.discover_hide_item) {
+ item.setChecked(!item.isChecked());
+ hidden = item.isChecked();
+ prefs.edit().putBoolean(ItunesTopListLoader.PREF_KEY_HIDDEN_DISCOVERY_COUNTRY, hidden).apply();
+
+ EventBus.getDefault().post(new DiscoveryDefaultUpdateEvent());
+ loadToplist(countryCode);
+ return true;
+ } else if (itemId == R.id.discover_countries_item) {
+
+ LayoutInflater inflater = getLayoutInflater();
+ View selectCountryDialogView = inflater.inflate(R.layout.select_country_dialog, null);
+ MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(getContext());
+ builder.setView(selectCountryDialogView);
+
+ List<String> countryCodeArray = new ArrayList<>(Arrays.asList(Locale.getISOCountries()));
+ Map<String, String> countryCodeNames = new HashMap<>();
+ Map<String, String> countryNameCodes = new HashMap<>();
+ for (String code : countryCodeArray) {
+ Locale locale = new Locale("", code);
+ String countryName = locale.getDisplayCountry();
+ countryCodeNames.put(code, countryName);
+ countryNameCodes.put(countryName, code);
+ }
+
+ List<String> countryNamesSort = new ArrayList<>(countryCodeNames.values());
+ Collections.sort(countryNamesSort);
+
+ ArrayAdapter<String> dataAdapter =
+ new ArrayAdapter<>(this.getContext(), android.R.layout.simple_list_item_1, countryNamesSort);
+ TextInputLayout textInput = selectCountryDialogView.findViewById(R.id.country_text_input);
+ MaterialAutoCompleteTextView editText = (MaterialAutoCompleteTextView) textInput.getEditText();
+ editText.setAdapter(dataAdapter);
+ editText.setText(countryCodeNames.get(countryCode));
+ editText.setOnClickListener(view -> {
+ if (editText.getText().length() != 0) {
+ editText.setText("");
+ editText.postDelayed(editText::showDropDown, 100);
+ }
+ });
+ editText.setOnFocusChangeListener((v, hasFocus) -> {
+ if (hasFocus) {
+ editText.setText("");
+ editText.postDelayed(editText::showDropDown, 100);
+ }
+ });
+
+ builder.setPositiveButton(android.R.string.ok, (dialogInterface, i) -> {
+ String countryName = editText.getText().toString();
+ if (countryNameCodes.containsKey(countryName)) {
+ countryCode = countryNameCodes.get(countryName);
+ MenuItem discoverHideItem = toolbar.getMenu().findItem(R.id.discover_hide_item);
+ discoverHideItem.setChecked(false);
+ hidden = false;
+ }
+
+ prefs.edit().putBoolean(ItunesTopListLoader.PREF_KEY_HIDDEN_DISCOVERY_COUNTRY, hidden).apply();
+ prefs.edit().putString(ItunesTopListLoader.PREF_KEY_COUNTRY_CODE, countryCode).apply();
+
+ EventBus.getDefault().post(new DiscoveryDefaultUpdateEvent());
+ loadToplist(countryCode);
+ });
+ builder.setNegativeButton(R.string.cancel_label, null);
+ builder.show();
+ return true;
+ }
+ return false;
+ }
+}
diff --git a/ui/discovery/src/main/java/de/danoeh/antennapod/ui/discovery/FeedDiscoverAdapter.java b/ui/discovery/src/main/java/de/danoeh/antennapod/ui/discovery/FeedDiscoverAdapter.java
new file mode 100644
index 000000000..ac400da9f
--- /dev/null
+++ b/ui/discovery/src/main/java/de/danoeh/antennapod/ui/discovery/FeedDiscoverAdapter.java
@@ -0,0 +1,79 @@
+package de.danoeh.antennapod.ui.discovery;
+
+import android.content.Context;
+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.load.resource.bitmap.FitCenter;
+import com.bumptech.glide.load.resource.bitmap.RoundedCorners;
+import com.bumptech.glide.request.RequestOptions;
+import de.danoeh.antennapod.net.discovery.PodcastSearchResult;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class FeedDiscoverAdapter extends BaseAdapter {
+
+ private final List<PodcastSearchResult> data = new ArrayList<>();
+ private final Context context;
+
+ public FeedDiscoverAdapter(Context context) {
+ this.context = context;
+ }
+
+ 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(context, 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);
+ holder.imageView.setContentDescription(podcast.title);
+
+ Glide.with(context)
+ .load(podcast.imageUrl)
+ .apply(new RequestOptions()
+ .placeholder(R.color.light_gray)
+ .transform(new FitCenter(), new RoundedCorners((int)
+ (8 * context.getResources().getDisplayMetrics().density)))
+ .dontAnimate())
+ .into(holder.imageView);
+
+ return convertView;
+ }
+
+ static class Holder {
+ ImageView imageView;
+ }
+}
diff --git a/ui/discovery/src/main/java/de/danoeh/antennapod/ui/discovery/OnlineSearchAdapter.java b/ui/discovery/src/main/java/de/danoeh/antennapod/ui/discovery/OnlineSearchAdapter.java
new file mode 100644
index 000000000..a536b8ebd
--- /dev/null
+++ b/ui/discovery/src/main/java/de/danoeh/antennapod/ui/discovery/OnlineSearchAdapter.java
@@ -0,0 +1,122 @@
+package de.danoeh.antennapod.ui.discovery;
+
+import android.content.Context;
+import android.widget.ArrayAdapter;
+import androidx.annotation.NonNull;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import com.bumptech.glide.Glide;
+import com.bumptech.glide.load.engine.DiskCacheStrategy;
+
+import com.bumptech.glide.load.resource.bitmap.FitCenter;
+import com.bumptech.glide.load.resource.bitmap.RoundedCorners;
+import com.bumptech.glide.request.RequestOptions;
+
+import java.util.List;
+
+import de.danoeh.antennapod.net.discovery.PodcastSearchResult;
+
+public class OnlineSearchAdapter extends ArrayAdapter<PodcastSearchResult> {
+ /**
+ * Related Context
+ */
+ private final Context context;
+
+ /**
+ * List holding the podcasts found in the search
+ */
+ private final List<PodcastSearchResult> data;
+
+ /**
+ * Constructor.
+ *
+ * @param context Related context
+ * @param objects Search result
+ */
+ public OnlineSearchAdapter(Context context, List<PodcastSearchResult> objects) {
+ super(context, 0, objects);
+ this.data = objects;
+ this.context = context;
+ }
+
+ @NonNull
+ @Override
+ public View getView(int position, View convertView, @NonNull ViewGroup parent) {
+ //Current podcast
+ PodcastSearchResult podcast = data.get(position);
+
+ //ViewHolder
+ PodcastViewHolder viewHolder;
+
+ //Resulting view
+ View view;
+
+ //Handle view holder stuff
+ if (convertView == null) {
+ view = View.inflate(context, R.layout.online_search_listitem, null);
+ viewHolder = new PodcastViewHolder(view);
+ view.setTag(viewHolder);
+ } else {
+ view = convertView;
+ viewHolder = (PodcastViewHolder) view.getTag();
+ }
+
+ // Set the title
+ viewHolder.titleView.setText(podcast.title);
+ if (podcast.author != null && ! podcast.author.trim().isEmpty()) {
+ viewHolder.authorView.setText(podcast.author);
+ viewHolder.authorView.setVisibility(View.VISIBLE);
+ } else if (podcast.feedUrl != null && !podcast.feedUrl.contains("itunes.apple.com")) {
+ viewHolder.authorView.setText(podcast.feedUrl);
+ viewHolder.authorView.setVisibility(View.VISIBLE);
+ } else {
+ viewHolder.authorView.setVisibility(View.GONE);
+ }
+
+ //Update the empty imageView with the image from the feed
+ Glide.with(context)
+ .load(podcast.imageUrl)
+ .apply(new RequestOptions()
+ .placeholder(R.color.light_gray)
+ .diskCacheStrategy(DiskCacheStrategy.NONE)
+ .transform(new FitCenter(),
+ new RoundedCorners((int) (4 * context.getResources().getDisplayMetrics().density)))
+ .dontAnimate())
+ .into(viewHolder.coverView);
+
+ //Feed the grid view
+ return view;
+ }
+
+ /**
+ * View holder object for the GridView
+ */
+ static class PodcastViewHolder {
+
+ /**
+ * ImageView holding the Podcast image
+ */
+ final ImageView coverView;
+
+ /**
+ * TextView holding the Podcast title
+ */
+ final TextView titleView;
+
+ final TextView authorView;
+
+
+ /**
+ * Constructor
+ * @param view GridView cell
+ */
+ PodcastViewHolder(View view) {
+ coverView = view.findViewById(R.id.imgvCover);
+ titleView = view.findViewById(R.id.txtvTitle);
+ authorView = view.findViewById(R.id.txtvAuthor);
+ }
+ }
+}
diff --git a/ui/discovery/src/main/java/de/danoeh/antennapod/ui/discovery/OnlineSearchFragment.java b/ui/discovery/src/main/java/de/danoeh/antennapod/ui/discovery/OnlineSearchFragment.java
new file mode 100644
index 000000000..00459a174
--- /dev/null
+++ b/ui/discovery/src/main/java/de/danoeh/antennapod/ui/discovery/OnlineSearchFragment.java
@@ -0,0 +1,219 @@
+package de.danoeh.antennapod.ui.discovery;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.AbsListView;
+import android.widget.Button;
+import android.widget.GridView;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+
+import androidx.appcompat.widget.SearchView;
+import androidx.fragment.app.Fragment;
+
+import com.google.android.material.appbar.MaterialToolbar;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import de.danoeh.antennapod.net.discovery.PodcastSearchResult;
+import de.danoeh.antennapod.net.discovery.PodcastSearcher;
+import de.danoeh.antennapod.net.discovery.PodcastSearcherRegistry;
+import de.danoeh.antennapod.ui.appstartintent.OnlineFeedviewActivityStarter;
+import io.reactivex.disposables.Disposable;
+
+public class OnlineSearchFragment extends Fragment {
+
+ private static final String TAG = "FyydSearchFragment";
+ private static final String ARG_SEARCHER = "searcher";
+ private static final String ARG_QUERY = "query";
+
+ /**
+ * Adapter responsible with the search results
+ */
+ private OnlineSearchAdapter adapter;
+ private PodcastSearcher searchProvider;
+ 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;
+ private Disposable disposable;
+
+ public static OnlineSearchFragment newInstance(Class<? extends PodcastSearcher> searchProvider) {
+ return newInstance(searchProvider, null);
+ }
+
+ public static OnlineSearchFragment newInstance(Class<? extends PodcastSearcher> searchProvider, String query) {
+ OnlineSearchFragment fragment = new OnlineSearchFragment();
+ Bundle arguments = new Bundle();
+ arguments.putString(ARG_SEARCHER, searchProvider.getName());
+ arguments.putString(ARG_QUERY, query);
+ fragment.setArguments(arguments);
+ return fragment;
+ }
+
+ /**
+ * Constructor
+ */
+ public OnlineSearchFragment() {
+ // Required empty public constructor
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ for (PodcastSearcherRegistry.SearcherInfo info : PodcastSearcherRegistry.getSearchProviders()) {
+ if (info.searcher.getClass().getName().equals(getArguments().getString(ARG_SEARCHER))) {
+ searchProvider = info.searcher;
+ break;
+ }
+ }
+ if (searchProvider == null) {
+ throw new IllegalArgumentException("Podcast searcher not found");
+ }
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ // Inflate the layout for this fragment
+ View root = inflater.inflate(R.layout.fragment_online_search, container, false);
+ gridView = root.findViewById(R.id.gridView);
+ adapter = new OnlineSearchAdapter(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);
+ startActivity(new OnlineFeedviewActivityStarter(getContext(), podcast.feedUrl)
+ .withStartedFromSearch().getIntent());
+ });
+ 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);
+ TextView txtvPoweredBy = root.findViewById(R.id.search_powered_by);
+ txtvPoweredBy.setText(getString(R.string.search_powered_by, searchProvider.getName()));
+ setupToolbar(root.findViewById(R.id.toolbar));
+
+ gridView.setOnScrollListener(new AbsListView.OnScrollListener() {
+ @Override
+ public void onScrollStateChanged(AbsListView view, int scrollState) {
+ if (scrollState == SCROLL_STATE_TOUCH_SCROLL) {
+ InputMethodManager imm = (InputMethodManager)
+ getActivity().getSystemService(Context.INPUT_METHOD_SERVICE);
+ imm.hideSoftInputFromWindow(view.getWindowToken(), 0);
+ }
+ }
+
+ @Override
+ public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
+ }
+ });
+ return root;
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ if (disposable != null) {
+ disposable.dispose();
+ }
+ adapter = null;
+ }
+
+ private void setupToolbar(MaterialToolbar toolbar) {
+ toolbar.inflateMenu(R.menu.online_search);
+ toolbar.setNavigationOnClickListener(v -> getParentFragmentManager().popBackStack());
+
+ MenuItem searchItem = toolbar.getMenu().findItem(R.id.action_search);
+ final SearchView sv = (SearchView) searchItem.getActionView();
+ sv.setQueryHint(getString(R.string.search_podcast_hint));
+ 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;
+ }
+ });
+ sv.setOnQueryTextFocusChangeListener((view, hasFocus) -> {
+ if (hasFocus) {
+ showInputMethod(view.findFocus());
+ }
+ });
+ searchItem.setOnActionExpandListener(new MenuItem.OnActionExpandListener() {
+ @Override
+ public boolean onMenuItemActionExpand(MenuItem item) {
+ return true;
+ }
+
+ @Override
+ public boolean onMenuItemActionCollapse(MenuItem item) {
+ getActivity().getSupportFragmentManager().popBackStack();
+ return true;
+ }
+ });
+ searchItem.expandActionView();
+
+ if (getArguments().getString(ARG_QUERY, null) != null) {
+ sv.setQuery(getArguments().getString(ARG_QUERY, null), true);
+ }
+ }
+
+ private void search(String query) {
+ if (disposable != null) {
+ disposable.dispose();
+ }
+ showOnlyProgressBar();
+ disposable = searchProvider.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);
+ txtvEmpty.setText(getString(R.string.no_results_for_query, query));
+ }, 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);
+ }
+
+ private void showInputMethod(View view) {
+ InputMethodManager imm = (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE);
+ if (imm != null) {
+ imm.showSoftInput(view, 0);
+ }
+ }
+}
diff --git a/ui/discovery/src/main/java/de/danoeh/antennapod/ui/discovery/QuickFeedDiscoveryFragment.java b/ui/discovery/src/main/java/de/danoeh/antennapod/ui/discovery/QuickFeedDiscoveryFragment.java
new file mode 100644
index 000000000..ff5e328f2
--- /dev/null
+++ b/ui/discovery/src/main/java/de/danoeh/antennapod/ui/discovery/QuickFeedDiscoveryFragment.java
@@ -0,0 +1,175 @@
+package de.danoeh.antennapod.ui.discovery;
+
+import android.content.SharedPreferences;
+import android.os.Bundle;
+import android.text.TextUtils;
+import android.util.DisplayMetrics;
+
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.Button;
+import android.widget.GridView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+import androidx.fragment.app.Fragment;
+import de.danoeh.antennapod.net.discovery.BuildConfig;
+import de.danoeh.antennapod.storage.database.DBReader;
+import de.danoeh.antennapod.event.DiscoveryDefaultUpdateEvent;
+import de.danoeh.antennapod.net.discovery.ItunesTopListLoader;
+import de.danoeh.antennapod.net.discovery.PodcastSearchResult;
+import de.danoeh.antennapod.ui.appstartintent.MainActivityStarter;
+import de.danoeh.antennapod.ui.appstartintent.OnlineFeedviewActivityStarter;
+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 java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+
+import static android.content.Context.MODE_PRIVATE;
+
+
+public class QuickFeedDiscoveryFragment extends Fragment implements AdapterView.OnItemClickListener {
+ private static final String TAG = "FeedDiscoveryFragment";
+ private static final int NUM_SUGGESTIONS = 12;
+
+ private Disposable disposable;
+ private FeedDiscoverAdapter adapter;
+ private GridView discoverGridLayout;
+ private TextView errorTextView;
+ private TextView poweredByTextView;
+ private LinearLayout errorView;
+ private Button errorRetry;
+
+ @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 -> startActivity(new MainActivityStarter(getContext())
+ .withFragmentLoaded(DiscoveryFragment.TAG)
+ .withAddToBackStack()
+ .getIntent()));
+
+ discoverGridLayout = root.findViewById(R.id.discover_grid);
+ errorView = root.findViewById(R.id.discover_error);
+ errorTextView = root.findViewById(R.id.discover_error_txtV);
+ errorRetry = root.findViewById(R.id.discover_error_retry_btn);
+ poweredByTextView = root.findViewById(R.id.discover_powered_by_itunes);
+
+ adapter = new FeedDiscoverAdapter(getActivity());
+ discoverGridLayout.setAdapter(adapter);
+ discoverGridLayout.setOnItemClickListener(this);
+
+ DisplayMetrics displayMetrics = getContext().getResources().getDisplayMetrics();
+ float screenWidthDp = displayMetrics.widthPixels / displayMetrics.density;
+ if (screenWidthDp > 600) {
+ discoverGridLayout.setNumColumns(6);
+ } else {
+ discoverGridLayout.setNumColumns(4);
+ }
+
+ // 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 < NUM_SUGGESTIONS; i++) {
+ dummies.add(PodcastSearchResult.dummy());
+ }
+
+ adapter.updateData(dummies);
+ loadToplist();
+
+ EventBus.getDefault().register(this);
+ return root;
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ EventBus.getDefault().unregister(this);
+ if (disposable != null) {
+ disposable.dispose();
+ }
+ }
+
+ @Subscribe(threadMode = ThreadMode.MAIN)
+ @SuppressWarnings("unused")
+ public void onDiscoveryDefaultUpdateEvent(DiscoveryDefaultUpdateEvent event) {
+ loadToplist();
+ }
+
+ private void loadToplist() {
+ errorView.setVisibility(View.GONE);
+ errorRetry.setVisibility(View.INVISIBLE);
+ errorRetry.setText(R.string.retry_label);
+ poweredByTextView.setVisibility(View.VISIBLE);
+
+ ItunesTopListLoader loader = new ItunesTopListLoader(getContext());
+ SharedPreferences prefs = getActivity().getSharedPreferences(ItunesTopListLoader.PREFS, MODE_PRIVATE);
+ String countryCode = prefs.getString(ItunesTopListLoader.PREF_KEY_COUNTRY_CODE,
+ Locale.getDefault().getCountry());
+ if (prefs.getBoolean(ItunesTopListLoader.PREF_KEY_HIDDEN_DISCOVERY_COUNTRY, false)) {
+ errorTextView.setText(R.string.discover_is_hidden);
+ errorView.setVisibility(View.VISIBLE);
+ discoverGridLayout.setVisibility(View.GONE);
+ errorRetry.setVisibility(View.GONE);
+ poweredByTextView.setVisibility(View.GONE);
+ return;
+ }
+ //noinspection ConstantConditions
+ if (BuildConfig.FLAVOR.equals("free") && prefs.getBoolean(ItunesTopListLoader.PREF_KEY_NEEDS_CONFIRM, true)) {
+ errorTextView.setText("");
+ errorView.setVisibility(View.VISIBLE);
+ discoverGridLayout.setVisibility(View.VISIBLE);
+ errorRetry.setVisibility(View.VISIBLE);
+ errorRetry.setText(R.string.discover_confirm);
+ poweredByTextView.setVisibility(View.VISIBLE);
+ errorRetry.setOnClickListener(v -> {
+ prefs.edit().putBoolean(ItunesTopListLoader.PREF_KEY_NEEDS_CONFIRM, false).apply();
+ loadToplist();
+ });
+ return;
+ }
+
+ disposable = Observable.fromCallable(() ->
+ loader.loadToplist(countryCode, NUM_SUGGESTIONS, DBReader.getFeedList()))
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(
+ podcasts -> {
+ errorView.setVisibility(View.GONE);
+ if (podcasts.size() == 0) {
+ errorTextView.setText(getResources().getText(R.string.search_status_no_results));
+ errorView.setVisibility(View.VISIBLE);
+ discoverGridLayout.setVisibility(View.INVISIBLE);
+ } else {
+ discoverGridLayout.setVisibility(View.VISIBLE);
+ adapter.updateData(podcasts);
+ }
+ }, error -> {
+ Log.e(TAG, Log.getStackTraceString(error));
+ errorTextView.setText(error.getLocalizedMessage());
+ errorView.setVisibility(View.VISIBLE);
+ discoverGridLayout.setVisibility(View.INVISIBLE);
+ errorRetry.setVisibility(View.VISIBLE);
+ errorRetry.setOnClickListener(v -> loadToplist());
+ });
+ }
+
+ @Override
+ public void onItemClick(AdapterView<?> parent, final View view, int position, long id) {
+ PodcastSearchResult podcast = adapter.getItem(position);
+ if (TextUtils.isEmpty(podcast.feedUrl)) {
+ return;
+ }
+ startActivity(new OnlineFeedviewActivityStarter(getContext(), podcast.feedUrl).getIntent());
+ }
+}
diff --git a/ui/discovery/src/main/res/layout/fragment_online_search.xml b/ui/discovery/src/main/res/layout/fragment_online_search.xml
new file mode 100644
index 000000000..902dd8b74
--- /dev/null
+++ b/ui/discovery/src/main/res/layout/fragment_online_search.xml
@@ -0,0 +1,101 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <com.google.android.material.appbar.AppBarLayout
+ android:id="@+id/appbar"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentTop="true"
+ android:fitsSystemWindows="true"
+ android:elevation="0dp">
+
+ <com.google.android.material.appbar.MaterialToolbar
+ android:id="@+id/toolbar"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:minHeight="?attr/actionBarSize"
+ android:theme="?attr/actionBarTheme"
+ app:title="@string/discover"
+ app:navigationContentDescription="@string/toolbar_back_button_content_description"
+ app:navigationIcon="?homeAsUpIndicator" />
+
+ </com.google.android.material.appbar.AppBarLayout>
+
+ <GridView
+ android:id="@+id/gridView"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_below="@id/appbar"
+ android:clipToPadding="false"
+ android:columnWidth="400dp"
+ android:gravity="center"
+ android:horizontalSpacing="8dp"
+ android:numColumns="auto_fit"
+ android:paddingBottom="@dimen/list_vertical_padding"
+ android:paddingTop="@dimen/list_vertical_padding"
+ android:stretchMode="columnWidth"
+ android:verticalSpacing="8dp"
+ tools:listitem="@layout/online_search_listitem" />
+
+ <TextView
+ android:id="@android:id/empty"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_centerInParent="true"
+ android:gravity="center"
+ android:visibility="gone"
+ android:text="@string/search_status_no_results" />
+
+ <ProgressBar
+ android:id="@+id/progressBar"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_centerInParent="true"
+ android:indeterminateOnly="true"
+ android:visibility="gone" />
+
+ <TextView
+ android:id="@+id/txtvError"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_centerInParent="true"
+ android:layout_margin="16dp"
+ android:textAlignment="center"
+ android:textSize="@dimen/text_size_small"
+ android:visibility="gone"
+ tools:visibility="visible"
+ tools:text="Error message"
+ tools:background="@android:color/holo_red_light" />
+
+ <Button
+ android:id="@+id/butRetry"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/txtvError"
+ android:layout_centerHorizontal="true"
+ android:layout_margin="16dp"
+ android:text="@string/retry_label"
+ android:visibility="gone"
+ tools:visibility="visible"
+ tools:background="@android:color/holo_red_light" />
+
+ <TextView
+ android:id="@+id/search_powered_by"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textColor="?android:attr/textColorTertiary"
+ android:text="@string/discover_powered_by_itunes"
+ android:textSize="12sp"
+ android:padding="4dp"
+ android:background="?android:attr/colorBackground"
+ android:layout_alignParentBottom="true"
+ android:layout_alignParentRight="true"
+ android:layout_alignParentEnd="true"
+ android:textAlignment="textEnd" />
+
+</RelativeLayout>
diff --git a/ui/discovery/src/main/res/layout/online_search_listitem.xml b/ui/discovery/src/main/res/layout/online_search_listitem.xml
new file mode 100644
index 000000000..da2de457b
--- /dev/null
+++ b/ui/discovery/src/main/res/layout/online_search_listitem.xml
@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingTop="8dp"
+ android:paddingLeft="16dp"
+ android:paddingRight="16dp"
+ android:paddingBottom="8dp"
+ tools:background="@android:color/darker_gray">
+
+ <ImageView
+ android:id="@+id/imgvCover"
+ android:layout_width="@dimen/thumbnail_length_itemlist"
+ android:layout_height="@dimen/thumbnail_length_itemlist"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentStart="true"
+ android:layout_alignParentTop="true"
+ android:adjustViewBounds="true"
+ android:importantForAccessibility="no"
+ android:cropToPadding="true"
+ android:scaleType="fitXY"
+ tools:background="@android:color/holo_green_dark"
+ tools:src="@tools:sample/avatars" />
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_toRightOf="@id/imgvCover"
+ android:layout_toEndOf="@id/imgvCover"
+ android:layout_centerVertical="true"
+ android:orientation="vertical"
+ android:layout_marginLeft="16dp"
+ android:layout_marginStart="16dp">
+
+ <TextView
+ android:id="@+id/txtvTitle"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ style="@style/AntennaPod.TextView.ListItemPrimaryTitle"
+ tools:background="@android:color/holo_green_dark"
+ tools:text="Podcast title" />
+
+ <TextView
+ android:id="@+id/txtvAuthor"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textSize="14sp"
+ android:textColor="?android:attr/textColorSecondary"
+ android:ellipsize="middle"
+ android:maxLines="2"
+ style="android:style/TextAppearance.Small"
+ tools:text="author"
+ tools:background="@android:color/holo_green_dark" />
+
+ </LinearLayout>
+
+</RelativeLayout>
diff --git a/ui/discovery/src/main/res/layout/quick_feed_discovery.xml b/ui/discovery/src/main/res/layout/quick_feed_discovery.xml
new file mode 100644
index 000000000..f4e406d20
--- /dev/null
+++ b/ui/discovery/src/main/res/layout/quick_feed_discovery.xml
@@ -0,0 +1,92 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+
+ <TextView
+ android:layout_width="0dip"
+ android:layout_height="wrap_content"
+ android:text="@string/discover"
+ android:textSize="18sp"
+ android:layout_marginBottom="8dp"
+ android:layout_weight="1"
+ android:accessibilityHeading="true"
+ android:textColor="?android:attr/textColorPrimary" />
+
+ <Button
+ android:id="@+id/discover_more"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:minHeight="48dp"
+ android:minWidth="0dp"
+ android:text="@string/discover_more"
+ style="@style/Widget.MaterialComponents.Button.TextButton" />
+
+ </LinearLayout>
+
+ <RelativeLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <de.danoeh.antennapod.ui.common.WrappingGridView
+ android:id="@+id/discover_grid"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:numColumns="4"
+ android:scrollbars="none"
+ android:layout_marginTop="8dp"
+ android:layout_centerInParent="true"
+ android:layout_gravity="center_horizontal"
+ android:layout_columnWeight="1"
+ android:layout_rowWeight="1" />
+
+ <LinearLayout
+ android:id="@+id/discover_error"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_centerInParent="true"
+ android:gravity="center"
+ android:orientation="vertical">
+
+ <TextView
+ android:id="@+id/discover_error_txtV"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center"
+ android:layout_margin="16dp"
+ android:textSize="@dimen/text_size_small"
+ tools:text="Error message"
+ tools:background="@android:color/holo_red_light" />
+
+ <Button
+ android:id="@+id/discover_error_retry_btn"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_margin="16dp"
+ android:text="@string/retry_label"
+ tools:background="@android:color/holo_red_light" />
+
+ </LinearLayout>
+
+ </RelativeLayout>
+
+ <TextView
+ android:id="@+id/discover_powered_by_itunes"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textColor="?android:attr/textColorTertiary"
+ android:text="@string/discover_powered_by_itunes"
+ android:textSize="12sp"
+ android:layout_gravity="right|end"
+ android:paddingHorizontal="4dp"
+ android:textAlignment="textEnd" />
+
+</LinearLayout>
diff --git a/ui/discovery/src/main/res/layout/quick_feed_discovery_item.xml b/ui/discovery/src/main/res/layout/quick_feed_discovery_item.xml
new file mode 100644
index 000000000..4407eb2f5
--- /dev/null
+++ b/ui/discovery/src/main/res/layout/quick_feed_discovery_item.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:squareImageView="http://schemas.android.com/apk/de.danoeh.antennapod"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:padding="4dp"
+ android:clipToPadding="false">
+
+ <de.danoeh.antennapod.ui.common.SquareImageView
+ android:id="@+id/discovery_cover"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:elevation="4dp"
+ android:outlineProvider="background"
+ android:foreground="?android:attr/selectableItemBackground"
+ squareImageView:direction="width"
+ tools:src="@tools:sample/avatars" />
+
+</LinearLayout>
diff --git a/ui/discovery/src/main/res/layout/select_country_dialog.xml b/ui/discovery/src/main/res/layout/select_country_dialog.xml
new file mode 100644
index 000000000..70f93bb77
--- /dev/null
+++ b/ui/discovery/src/main/res/layout/select_country_dialog.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <com.google.android.material.textfield.TextInputLayout
+ android:id="@+id/country_text_input"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_margin="20dp"
+ android:hint="@string/select_country"
+ app:endIconMode="none"
+ style="@style/Widget.MaterialComponents.TextInputLayout.FilledBox.ExposedDropdownMenu">
+
+ <AutoCompleteTextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content" />
+
+ </com.google.android.material.textfield.TextInputLayout>
+
+</LinearLayout>
diff --git a/ui/discovery/src/main/res/menu/countries_menu.xml b/ui/discovery/src/main/res/menu/countries_menu.xml
new file mode 100644
index 000000000..e0bd7dafe
--- /dev/null
+++ b/ui/discovery/src/main/res/menu/countries_menu.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto">
+
+ <item
+ android:id="@+id/discover_hide_item"
+ android:checkable="true"
+ android:enabled="true"
+ android:title="@string/discover_hide"
+ android:visible="true"
+ app:showAsAction="never" />
+ <item
+ android:id="@+id/discover_countries_item"
+ android:enabled="true"
+ android:title="@string/select_country"
+ android:visible="true" />
+</menu> \ No newline at end of file
diff --git a/ui/discovery/src/main/res/menu/online_search.xml b/ui/discovery/src/main/res/menu/online_search.xml
new file mode 100644
index 000000000..374a054fa
--- /dev/null
+++ b/ui/discovery/src/main/res/menu/online_search.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:custom="http://schemas.android.com/apk/res-auto">
+
+ <item
+ android:id="@+id/action_search"
+ android:icon="@drawable/ic_search"
+ custom:showAsAction="collapseActionView|ifRoom"
+ custom:actionViewClass="androidx.appcompat.widget.SearchView"
+ android:title="@string/search_label"/>
+
+</menu>