Implemented the new search engine

This commit is contained in:
CappielloAntonio 2021-07-31 14:47:29 +02:00
parent 05d2e0b9ec
commit 320e3b8678
9 changed files with 212 additions and 238 deletions

View file

@ -2,21 +2,154 @@ package com.cappielloantonio.play.repository;
import android.app.Application; import android.app.Application;
import androidx.lifecycle.MutableLiveData;
import com.cappielloantonio.play.App;
import com.cappielloantonio.play.database.AppDatabase; import com.cappielloantonio.play.database.AppDatabase;
import com.cappielloantonio.play.database.dao.RecentSearchDao; import com.cappielloantonio.play.database.dao.RecentSearchDao;
import com.cappielloantonio.play.model.Album;
import com.cappielloantonio.play.model.Artist;
import com.cappielloantonio.play.model.RecentSearch; import com.cappielloantonio.play.model.RecentSearch;
import com.cappielloantonio.play.model.Song;
import com.cappielloantonio.play.subsonic.models.AlbumID3;
import com.cappielloantonio.play.subsonic.models.ArtistID3;
import com.cappielloantonio.play.subsonic.models.Child;
import com.cappielloantonio.play.subsonic.models.ResponseStatus;
import com.cappielloantonio.play.subsonic.models.SubsonicResponse;
import com.cappielloantonio.play.util.MappingUtil;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
public class SearchingRepository { public class SearchingRepository {
private RecentSearchDao recentSearchDao; private RecentSearchDao recentSearchDao;
private Application application;
public SearchingRepository(Application application) { public SearchingRepository(Application application) {
this.application = application;
AppDatabase database = AppDatabase.getInstance(application); AppDatabase database = AppDatabase.getInstance(application);
recentSearchDao = database.recentSearchDao(); recentSearchDao = database.recentSearchDao();
} }
public MutableLiveData<List<Song>> getSearchedSongs(String query) {
MutableLiveData<List<Song>> searchedSongs = new MutableLiveData<>();
App.getSubsonicClientInstance(application, false)
.getSearchingClient()
.search3(query, 20, 0, 0)
.enqueue(new Callback<SubsonicResponse>() {
@Override
public void onResponse(Call<SubsonicResponse> call, Response<SubsonicResponse> response) {
if (response.body().getStatus().getValue().equals(ResponseStatus.OK)) {
List<Song> songs = new ArrayList<>(MappingUtil.mapSong(response.body().getSearchResult3().getSongs()));
searchedSongs.setValue(songs);
}
}
@Override
public void onFailure(Call<SubsonicResponse> call, Throwable t) {
}
});
return searchedSongs;
}
public MutableLiveData<List<Album>> getSearchedAlbums(String query) {
MutableLiveData<List<Album>> searchedAlbums = new MutableLiveData<>();
App.getSubsonicClientInstance(application, false)
.getSearchingClient()
.search3(query, 0, 20, 0)
.enqueue(new Callback<SubsonicResponse>() {
@Override
public void onResponse(Call<SubsonicResponse> call, Response<SubsonicResponse> response) {
if (response.body().getStatus().getValue().equals(ResponseStatus.OK)) {
List<Album> albums = new ArrayList<>(MappingUtil.mapAlbum(response.body().getSearchResult3().getAlbums()));
searchedAlbums.setValue(albums);
}
}
@Override
public void onFailure(Call<SubsonicResponse> call, Throwable t) {
}
});
return searchedAlbums;
}
public MutableLiveData<List<Artist>> getSearchedArtists(String query) {
MutableLiveData<List<Artist>> searchedArtists = new MutableLiveData<>();
App.getSubsonicClientInstance(application, false)
.getSearchingClient()
.search3(query, 0, 0, 20)
.enqueue(new Callback<SubsonicResponse>() {
@Override
public void onResponse(Call<SubsonicResponse> call, Response<SubsonicResponse> response) {
if (response.body().getStatus().getValue().equals(ResponseStatus.OK)) {
List<Artist> artists = new ArrayList<>(MappingUtil.mapArtist(response.body().getSearchResult3().getArtists()));
searchedArtists.setValue(artists);
}
}
@Override
public void onFailure(Call<SubsonicResponse> call, Throwable t) {
}
});
return searchedArtists;
}
public MutableLiveData<List<String>> getSuggestions(String query) {
MutableLiveData<List<String>> suggestions = new MutableLiveData<>(new ArrayList());
App.getSubsonicClientInstance(application, false)
.getSearchingClient()
.search3(query, 5, 5, 5)
.enqueue(new Callback<SubsonicResponse>() {
@Override
public void onResponse(Call<SubsonicResponse> call, Response<SubsonicResponse> response) {
List<String> newSuggestions = new ArrayList();
if (response.body().getStatus().getValue().equals(ResponseStatus.OK)) {
for(ArtistID3 artistID3 : response.body().getSearchResult3().getArtists()) {
newSuggestions.add(artistID3.getName());
}
for(AlbumID3 albumID3 : response.body().getSearchResult3().getAlbums()) {
newSuggestions.add(albumID3.getName());
}
for(Child song : response.body().getSearchResult3().getSongs()) {
newSuggestions.add(song.getTitle());
}
LinkedHashSet<String> hashSet = new LinkedHashSet<>(newSuggestions);
ArrayList<String> suggestionsWithoutDuplicates = new ArrayList<>(hashSet);
suggestions.setValue(suggestionsWithoutDuplicates);
}
}
@Override
public void onFailure(Call<SubsonicResponse> call, Throwable t) {
}
});
return suggestions;
}
public void insert(RecentSearch recentSearch) { public void insert(RecentSearch recentSearch) {
InsertThreadSafe insert = new InsertThreadSafe(recentSearchDao, recentSearch); InsertThreadSafe insert = new InsertThreadSafe(recentSearchDao, recentSearch);
Thread thread = new Thread(insert); Thread thread = new Thread(insert);
@ -29,12 +162,6 @@ public class SearchingRepository {
thread.start(); thread.start();
} }
public void deleteAll() {
DeleteAllThreadSafe delete = new DeleteAllThreadSafe(recentSearchDao);
Thread thread = new Thread(delete);
thread.start();
}
public List<String> getRecentSearchSuggestion(int limit) { public List<String> getRecentSearchSuggestion(int limit) {
List<String> recent = new ArrayList<>(); List<String> recent = new ArrayList<>();
@ -82,19 +209,6 @@ public class SearchingRepository {
} }
} }
private static class DeleteAllThreadSafe implements Runnable {
private RecentSearchDao recentSearchDao;
public DeleteAllThreadSafe(RecentSearchDao recentSearchDao) {
this.recentSearchDao = recentSearchDao;
}
@Override
public void run() {
recentSearchDao.deleteAll();
}
}
private static class RecentThreadSafe implements Runnable { private static class RecentThreadSafe implements Runnable {
private RecentSearchDao recentSearchDao; private RecentSearchDao recentSearchDao;
private int limit; private int limit;

View file

@ -32,14 +32,14 @@ public class SearchingClient {
this.searchingService = retrofit.create(SearchingService.class); this.searchingService = retrofit.create(SearchingService.class);
} }
public Call<SubsonicResponse> search2(String query) { public Call<SubsonicResponse> search2(String query, int songCount, int albumCount, int artistCount) {
Log.d(TAG, "search2()"); Log.d(TAG, "search2()");
return searchingService.search2(subsonic.getParams(), query); return searchingService.search2(subsonic.getParams(), query, songCount, albumCount, artistCount);
} }
public Call<SubsonicResponse> search3(String query) { public Call<SubsonicResponse> search3(String query, int songCount, int albumCount, int artistCount) {
Log.d(TAG, "search3()"); Log.d(TAG, "search3()");
return searchingService.search3(subsonic.getParams(), query); return searchingService.search3(subsonic.getParams(), query, songCount, albumCount, artistCount);
} }
private TikXml getParser() { private TikXml getParser() {

View file

@ -6,12 +6,13 @@ import java.util.Map;
import retrofit2.Call; import retrofit2.Call;
import retrofit2.http.GET; import retrofit2.http.GET;
import retrofit2.http.Query;
import retrofit2.http.QueryMap; import retrofit2.http.QueryMap;
public interface SearchingService { public interface SearchingService {
@GET("search2?query={query}") @GET("search2")
Call<SubsonicResponse> search2(@QueryMap Map<String, String> params, String query); Call<SubsonicResponse> search2(@QueryMap Map<String, String> params, @Query("query") String query, @Query("songCount") int songCount, @Query("albumCount") int albumCount, @Query("artistCount") int artistCount);
@GET("search3?query={query}") @GET("search3")
Call<SubsonicResponse> search3(@QueryMap Map<String, String> params, String query); Call<SubsonicResponse> search3(@QueryMap Map<String, String> params, @Query("query") String query, @Query("songCount") int songCount, @Query("albumCount") int albumCount, @Query("artistCount") int artistCount);
} }

View file

@ -1,91 +1,50 @@
package com.cappielloantonio.play.subsonic.models; package com.cappielloantonio.play.subsonic.models;
import com.tickaroo.tikxml.annotation.Element;
import com.tickaroo.tikxml.annotation.Xml;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@Xml
public class SearchResult2 { public class SearchResult2 {
@Element(name = "artist")
protected List<Artist> artists; protected List<Artist> artists;
@Element(name = "album")
protected List<Child> albums; protected List<Child> albums;
@Element(name = "song")
protected List<Child> songs; protected List<Child> songs;
/**
* Gets the value of the artists property.
*
* <p>
* This accessor method returns a reference to the live list,
* not a snapshot. Therefore any modification you make to the
* returned list will be present inside the JAXB object.
* This is why there is not a <CODE>set</CODE> method for the artists property.
*
* <p>
* For example, to add a new item, do as follows:
* <pre>
* getArtists().add(newItem);
* </pre>
*
*
* <p>
* Objects of the following type(s) are allowed in the list
* {@link Artist }
*/
public List<Artist> getArtists() { public List<Artist> getArtists() {
if (artists == null) { if (artists == null) {
artists = new ArrayList<Artist>(); artists = new ArrayList<>();
} }
return this.artists; return this.artists;
} }
/**
* Gets the value of the albums property.
*
* <p>
* This accessor method returns a reference to the live list,
* not a snapshot. Therefore any modification you make to the
* returned list will be present inside the JAXB object.
* This is why there is not a <CODE>set</CODE> method for the albums property.
*
* <p>
* For example, to add a new item, do as follows:
* <pre>
* getAlbums().add(newItem);
* </pre>
*
*
* <p>
* Objects of the following type(s) are allowed in the list
* {@link Child }
*/
public List<Child> getAlbums() { public List<Child> getAlbums() {
if (albums == null) { if (albums == null) {
albums = new ArrayList<Child>(); albums = new ArrayList<>();
} }
return this.albums; return this.albums;
} }
/**
* Gets the value of the songs property.
*
* <p>
* This accessor method returns a reference to the live list,
* not a snapshot. Therefore any modification you make to the
* returned list will be present inside the JAXB object.
* This is why there is not a <CODE>set</CODE> method for the songs property.
*
* <p>
* For example, to add a new item, do as follows:
* <pre>
* getSongs().add(newItem);
* </pre>
*
*
* <p>
* Objects of the following type(s) are allowed in the list
* {@link Child }
*/
public List<Child> getSongs() { public List<Child> getSongs() {
if (songs == null) { if (songs == null) {
songs = new ArrayList<Child>(); songs = new ArrayList<>();
} }
return this.songs; return this.songs;
} }
public void setArtists(List<Artist> artists) {
this.artists = artists;
}
public void setAlbums(List<Child> albums) {
this.albums = albums;
}
public void setSongs(List<Child> songs) {
this.songs = songs;
}
} }

View file

@ -1,91 +1,50 @@
package com.cappielloantonio.play.subsonic.models; package com.cappielloantonio.play.subsonic.models;
import com.tickaroo.tikxml.annotation.Element;
import com.tickaroo.tikxml.annotation.Xml;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@Xml
public class SearchResult3 { public class SearchResult3 {
@Element(name = "artist")
protected List<ArtistID3> artists; protected List<ArtistID3> artists;
@Element(name = "album")
protected List<AlbumID3> albums; protected List<AlbumID3> albums;
@Element(name = "song")
protected List<Child> songs; protected List<Child> songs;
/**
* Gets the value of the artists property.
*
* <p>
* This accessor method returns a reference to the live list,
* not a snapshot. Therefore any modification you make to the
* returned list will be present inside the JAXB object.
* This is why there is not a <CODE>set</CODE> method for the artists property.
*
* <p>
* For example, to add a new item, do as follows:
* <pre>
* getArtists().add(newItem);
* </pre>
*
*
* <p>
* Objects of the following type(s) are allowed in the list
* {@link ArtistID3 }
*/
public List<ArtistID3> getArtists() { public List<ArtistID3> getArtists() {
if (artists == null) { if (artists == null) {
artists = new ArrayList<ArtistID3>(); artists = new ArrayList<>();
} }
return this.artists; return this.artists;
} }
/**
* Gets the value of the albums property.
*
* <p>
* This accessor method returns a reference to the live list,
* not a snapshot. Therefore any modification you make to the
* returned list will be present inside the JAXB object.
* This is why there is not a <CODE>set</CODE> method for the albums property.
*
* <p>
* For example, to add a new item, do as follows:
* <pre>
* getAlbums().add(newItem);
* </pre>
*
*
* <p>
* Objects of the following type(s) are allowed in the list
* {@link AlbumID3 }
*/
public List<AlbumID3> getAlbums() { public List<AlbumID3> getAlbums() {
if (albums == null) { if (albums == null) {
albums = new ArrayList<AlbumID3>(); albums = new ArrayList<>();
} }
return this.albums; return this.albums;
} }
/**
* Gets the value of the songs property.
*
* <p>
* This accessor method returns a reference to the live list,
* not a snapshot. Therefore any modification you make to the
* returned list will be present inside the JAXB object.
* This is why there is not a <CODE>set</CODE> method for the songs property.
*
* <p>
* For example, to add a new item, do as follows:
* <pre>
* getSongs().add(newItem);
* </pre>
*
*
* <p>
* Objects of the following type(s) are allowed in the list
* {@link Child }
*/
public List<Child> getSongs() { public List<Child> getSongs() {
if (songs == null) { if (songs == null) {
songs = new ArrayList<Child>(); songs = new ArrayList<>();
} }
return this.songs; return this.songs;
} }
public void setArtists(List<ArtistID3> artists) {
this.artists = artists;
}
public void setAlbums(List<AlbumID3> albums) {
this.albums = albums;
}
public void setSongs(List<Child> songs) {
this.songs = songs;
}
} }

View file

@ -44,7 +44,9 @@ public class SubsonicResponse {
private PlaylistWithSongs playlist; private PlaylistWithSongs playlist;
@Element @Element
private Playlists playlists; private Playlists playlists;
@Element
private SearchResult3 searchResult3; private SearchResult3 searchResult3;
@Element
private SearchResult2 searchResult2; private SearchResult2 searchResult2;
private SearchResult searchResult; private SearchResult searchResult;
private NowPlaying nowPlaying; private NowPlaying nowPlaying;

View file

@ -37,7 +37,6 @@ public class SearchFragment extends Fragment {
private SongHorizontalAdapter songHorizontalAdapter; private SongHorizontalAdapter songHorizontalAdapter;
private AlbumAdapter albumAdapter; private AlbumAdapter albumAdapter;
private ArtistAdapter artistAdapter; private ArtistAdapter artistAdapter;
private GenreCatalogueAdapter genreCatalogueAdapter;
@Nullable @Nullable
@Override @Override
@ -93,20 +92,6 @@ public class SearchFragment extends Fragment {
artistAdapter = new ArtistAdapter(requireContext()); artistAdapter = new ArtistAdapter(requireContext());
bind.searchResultArtistRecyclerView.setAdapter(artistAdapter); bind.searchResultArtistRecyclerView.setAdapter(artistAdapter);
// Genres
bind.searchResultGenreRecyclerView.setLayoutManager(new GridLayoutManager(requireContext(), 2));
bind.searchResultGenreRecyclerView.addItemDecoration(new GridItemDecoration(2, 16, false));
bind.searchResultGenreRecyclerView.setHasFixedSize(true);
genreCatalogueAdapter = new GenreCatalogueAdapter(activity, requireContext());
genreCatalogueAdapter.setClickListener((view, position) -> {
Bundle bundle = new Bundle();
bundle.putString(Song.BY_GENRE, Song.BY_GENRE);
bundle.putParcelable("genre_object", genreCatalogueAdapter.getItem(position));
activity.navController.navigate(R.id.action_searchFragment_to_songListPageFragment, bundle);
});
bind.searchResultGenreRecyclerView.setAdapter(genreCatalogueAdapter);
} }
private void initSearchView() { private void initSearchView() {
@ -119,7 +104,9 @@ public class SearchFragment extends Fragment {
bind.persistentSearchView.setOnSearchQueryChangeListener((searchView, oldQuery, newQuery) -> { bind.persistentSearchView.setOnSearchQueryChangeListener((searchView, oldQuery, newQuery) -> {
if (!newQuery.trim().equals("") && newQuery.trim().length() > 1) { if (!newQuery.trim().equals("") && newQuery.trim().length() > 1) {
searchView.setSuggestions(SuggestionCreationUtil.asRegularSearchSuggestions(searchViewModel.getSearchSuggestion(newQuery)), false); searchViewModel.getSearchSuggestion(newQuery).observe(requireActivity(), suggestions -> {
searchView.setSuggestions(SuggestionCreationUtil.asRegularSearchSuggestions(suggestions), false);
});
} else { } else {
setSuggestions(); setSuggestions();
} }
@ -173,22 +160,18 @@ public class SearchFragment extends Fragment {
} }
private void performSearch(String query) { private void performSearch(String query) {
searchViewModel.searchSong(query, requireContext()).observe(requireActivity(), songs -> { searchViewModel.searchSong(query).observe(requireActivity(), songs -> {
if(bind != null) bind.searchSongSector.setVisibility(!songs.isEmpty() ? View.VISIBLE : View.GONE); if(bind != null) bind.searchSongSector.setVisibility(!songs.isEmpty() ? View.VISIBLE : View.GONE);
songHorizontalAdapter.setItems(songs); songHorizontalAdapter.setItems(songs);
}); });
searchViewModel.searchAlbum(query, requireContext()).observe(requireActivity(), albums -> { searchViewModel.searchAlbum(query).observe(requireActivity(), albums -> {
if(bind != null) bind.searchAlbumSector.setVisibility(!albums.isEmpty() ? View.VISIBLE : View.GONE); if(bind != null) bind.searchAlbumSector.setVisibility(!albums.isEmpty() ? View.VISIBLE : View.GONE);
albumAdapter.setItems(albums); albumAdapter.setItems(albums);
}); });
searchViewModel.searchArtist(query, requireContext()).observe(requireActivity(), artists -> { searchViewModel.searchArtist(query).observe(requireActivity(), artists -> {
if(bind != null) bind.searchArtistSector.setVisibility(!artists.isEmpty() ? View.VISIBLE : View.GONE); if(bind != null) bind.searchArtistSector.setVisibility(!artists.isEmpty() ? View.VISIBLE : View.GONE);
artistAdapter.setItems(artists); artistAdapter.setItems(artists);
}); });
searchViewModel.searchGenre(query, requireContext()).observe(requireActivity(), genres -> {
if(bind != null) bind.searchGenreSector.setVisibility(!genres.isEmpty() ? View.VISIBLE : View.GONE);
genreCatalogueAdapter.setItems(genres);
});
bind.searchResultLayout.setVisibility(View.VISIBLE); bind.searchResultLayout.setVisibility(View.VISIBLE);
} }

View file

@ -6,6 +6,7 @@ import android.content.Context;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.lifecycle.AndroidViewModel; import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData; import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import com.cappielloantonio.play.model.Album; import com.cappielloantonio.play.model.Album;
import com.cappielloantonio.play.model.Artist; import com.cappielloantonio.play.model.Artist;
@ -33,10 +34,10 @@ public class SearchViewModel extends AndroidViewModel {
private GenreRepository genreRepository; private GenreRepository genreRepository;
private SearchingRepository searchingRepository; private SearchingRepository searchingRepository;
private LiveData<List<Song>> searchSong; private LiveData<List<Song>> searchSong = new MutableLiveData<>(new ArrayList<>());
private LiveData<List<Album>> searchAlbum; private LiveData<List<Album>> searchAlbum = new MutableLiveData<>(new ArrayList<>());
private LiveData<List<Artist>> searchArtist; private LiveData<List<Artist>> searchArtist = new MutableLiveData<>(new ArrayList<>());
private LiveData<List<Genre>> searchGenre; private LiveData<List<Genre>> searchGenre = new MutableLiveData<>(new ArrayList<>());
public SearchViewModel(@NonNull Application application) { public SearchViewModel(@NonNull Application application) {
super(application); super(application);
@ -60,26 +61,21 @@ public class SearchViewModel extends AndroidViewModel {
} }
} }
public LiveData<List<Song>> searchSong(String title, Context context) { public LiveData<List<Song>> searchSong(String title) {
// searchSong = songRepository.searchListLiveSong(title, PreferenceUtil.getInstance(context).getSearchElementPerCategory()); searchSong = searchingRepository.getSearchedSongs(title);
return searchSong; return searchSong;
} }
public LiveData<List<Album>> searchAlbum(String name, Context context) { public LiveData<List<Album>> searchAlbum(String name) {
// searchAlbum = albumRepository.searchListLiveAlbum(name, PreferenceUtil.getInstance(context).getSearchElementPerCategory()); searchAlbum = searchingRepository.getSearchedAlbums(name);
return searchAlbum; return searchAlbum;
} }
public LiveData<List<Artist>> searchArtist(String name, Context context) { public LiveData<List<Artist>> searchArtist(String name) {
// searchArtist = artistRepository.searchListLiveArtist(name, PreferenceUtil.getInstance(context).getSearchElementPerCategory()); searchArtist = searchingRepository.getSearchedArtists(name);
return searchArtist; return searchArtist;
} }
public LiveData<List<Genre>> searchGenre(String name, Context context) {
// searchGenre = genreRepository.searchListLiveGenre(name, PreferenceUtil.getInstance(context).getSearchElementPerCategory());
return searchGenre;
}
public void insertNewSearch(String search) { public void insertNewSearch(String search) {
searchingRepository.insert(new RecentSearch(search)); searchingRepository.insert(new RecentSearch(search));
} }
@ -88,17 +84,8 @@ public class SearchViewModel extends AndroidViewModel {
searchingRepository.delete(new RecentSearch(search)); searchingRepository.delete(new RecentSearch(search));
} }
public List<String> getSearchSuggestion(String query) { public LiveData<List<String>> getSearchSuggestion(String query) {
ArrayList<String> suggestions = new ArrayList<>(); return searchingRepository.getSuggestions(query);
// suggestions.addAll(songRepository.getSearchSuggestion(query));
// suggestions.addAll(albumRepository.getSearchSuggestion(query));
// suggestions.addAll(artistRepository.getSearchSuggestion(query));
// suggestions.addAll(genreRepository.getSearchSuggestion(query));
LinkedHashSet<String> hashSet = new LinkedHashSet<>(suggestions);
ArrayList<String> suggestionsWithoutDuplicates = new ArrayList<>(hashSet);
return suggestionsWithoutDuplicates;
} }
public List<String> getRecentSearchSuggestion() { public List<String> getRecentSearchSuggestion() {

View file

@ -139,37 +139,6 @@
android:paddingEnd="8dp" android:paddingEnd="8dp"
android:paddingBottom="8dp" /> android:paddingBottom="8dp" />
</LinearLayout> </LinearLayout>
<!-- Genre -->
<LinearLayout
android:id="@+id/search_genre_sector"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingBottom="8dp"
android:visibility="gone">
<TextView
style="@style/HeadlineTextView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="16dp"
android:paddingTop="20dp"
android:paddingEnd="16dp"
android:text="Genres" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/search_result_genre_recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:clipToPadding="false"
android:paddingStart="16dp"
android:paddingTop="8dp"
android:paddingEnd="16dp"
android:paddingBottom="8dp" />
</LinearLayout>
</LinearLayout> </LinearLayout>
</androidx.core.widget.NestedScrollView> </androidx.core.widget.NestedScrollView>
</RelativeLayout> </RelativeLayout>