From 048b51bd422108fbc577d47b4daa6b040be6c3e4 Mon Sep 17 00:00:00 2001 From: Sergei Nezhinskikh Date: Thu, 2 Jan 2020 22:27:27 +0600 Subject: [PATCH] fix like/dislike --- CHANGELOG.md | 6 + app/build.gradle | 12 +- app/src/main/AndroidManifest.xml | 11 +- .../kg/delletenebre/yamus/MainActivity.kt | 137 ++++++++++++- .../delletenebre/yamus/ScopedAppActivity.kt | 24 +++ .../yamus/fragments/NowPlayingFragment.kt | 102 --------- .../yamus/ui/search/SearchFragment.kt | 55 ++++- .../yamus/ui/search/SearchViewModel.kt | 69 +++++++ .../yamus/ui/stations/StationsFragment.kt | 4 + .../delletenebre/yamus/utils/InjectorUtils.kt | 19 -- .../viewmodels/NowPlayingFragmentViewModel.kt | 194 ------------------ .../yamus/viewmodels/NowPlayingViewModel.kt | 148 ------------- .../main/res/layout-land/home_fragment.xml | 2 +- app/src/main/res/layout/activity_main.xml | 25 +-- .../main/res/layout/fragment_nowplaying.xml | 116 ----------- app/src/main/res/layout/home_fragment.xml | 3 +- app/src/main/res/layout/my_music_fragment.xml | 2 +- app/src/main/res/layout/playlist_fragment.xml | 2 +- app/src/main/res/layout/playlist_item.xml | 2 +- app/src/main/res/layout/profile_fragment.xml | 2 +- app/src/main/res/layout/search_fragment.xml | 63 ++++-- app/src/main/res/layout/search_item.xml | 23 +++ .../main/res/layout/search_playlist_item.xml | 51 +++++ app/src/main/res/layout/stations_fragment.xml | 2 +- ...igation.xml => bottom_navigation_menu.xml} | 0 .../res/menu/{menu_main.xml => main_menu.xml} | 19 +- .../{menu_playlist.xml => playlist_menu.xml} | 0 .../{menu_profile.xml => profile_menu.xml} | 0 app/src/main/res/values-ru/strings.xml | 4 +- app/src/main/res/values/strings.xml | 4 +- app/src/main/res/xml/searchable.xml | 5 + build.gradle | 6 +- common/build.gradle | 2 +- .../java/kg/delletenebre/yamus/api/YaApi.kt | 43 ++-- .../delletenebre/yamus/api/responses/Mix.kt | 3 +- .../media/datasource/YandexDataSource.kt | 10 +- .../yamus/media/library/CurrentPlaylist.kt | 4 - .../yamus/media/library/MediaLibrary.kt | 3 +- 38 files changed, 495 insertions(+), 682 deletions(-) create mode 100644 app/src/main/java/kg/delletenebre/yamus/ScopedAppActivity.kt delete mode 100644 app/src/main/java/kg/delletenebre/yamus/fragments/NowPlayingFragment.kt create mode 100644 app/src/main/java/kg/delletenebre/yamus/ui/search/SearchViewModel.kt delete mode 100644 app/src/main/java/kg/delletenebre/yamus/viewmodels/NowPlayingFragmentViewModel.kt delete mode 100644 app/src/main/java/kg/delletenebre/yamus/viewmodels/NowPlayingViewModel.kt delete mode 100644 app/src/main/res/layout/fragment_nowplaying.xml create mode 100644 app/src/main/res/layout/search_item.xml create mode 100644 app/src/main/res/layout/search_playlist_item.xml rename app/src/main/res/menu/{menu_bottom_navigation.xml => bottom_navigation_menu.xml} (100%) rename app/src/main/res/menu/{menu_main.xml => main_menu.xml} (63%) rename app/src/main/res/menu/{menu_playlist.xml => playlist_menu.xml} (100%) rename app/src/main/res/menu/{menu_profile.xml => profile_menu.xml} (100%) create mode 100644 app/src/main/res/xml/searchable.xml diff --git a/CHANGELOG.md b/CHANGELOG.md index 99dbaa0..58e3e52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +20.01.02 +======== +* Добавлен базовый функционал поиска +* Исправлено добавление/удаление понравившихся треков +* Исправлены подборки + 19.11.20 ======== * Добавлена возможность изменять расположение кнопок "Нравится" и "Не нравится" в интерфейсе AA diff --git a/app/build.gradle b/app/build.gradle index ad3cd9d..10cbae0 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -10,8 +10,8 @@ android { defaultConfig { multiDexEnabled true applicationId "kg.delletenebre.yamus" - versionCode 29 - versionName "19.11.20" + versionCode 30 + versionName "20.01.02" minSdkVersion rootProject.minSdkVersion targetSdkVersion rootProject.targetSdkVersion @@ -52,19 +52,19 @@ dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" implementation 'androidx.core:core-ktx:1.1.0' implementation "androidx.appcompat:appcompat:1.1.0" - implementation "androidx.recyclerview:recyclerview:1.0.0" + implementation "androidx.recyclerview:recyclerview:1.1.0" implementation "androidx.constraintlayout:constraintlayout:1.1.3" - implementation 'com.google.android.material:material:1.1.0-beta02' + implementation 'com.google.android.material:material:1.0.0' implementation 'androidx.annotation:annotation:1.1.0' implementation 'androidx.legacy:legacy-support-v4:1.0.0' implementation 'androidx.navigation:navigation-fragment-ktx:2.1.0' implementation 'androidx.navigation:navigation-ui-ktx:2.1.0' implementation 'androidx.gridlayout:gridlayout:1.0.0' implementation 'androidx.preference:preference:1.1.0' - implementation 'androidx.fragment:fragment-ktx:1.2.0-rc02' + implementation 'androidx.fragment:fragment-ktx:1.2.0-rc04' // Lifecycle - def lifecycle_version = '2.2.0-rc02' + def lifecycle_version = '2.2.0-rc03' implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version" implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version" implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 9842ffa..3b9251a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -33,13 +33,20 @@ android:label="@string/title_activity_login" android:launchMode="singleTop" /> + android:name=".MainActivity" + android:launchMode="singleTop"> - + + + + + + diff --git a/app/src/main/java/kg/delletenebre/yamus/MainActivity.kt b/app/src/main/java/kg/delletenebre/yamus/MainActivity.kt index 1ce1e76..07e68d5 100644 --- a/app/src/main/java/kg/delletenebre/yamus/MainActivity.kt +++ b/app/src/main/java/kg/delletenebre/yamus/MainActivity.kt @@ -1,20 +1,34 @@ package kg.delletenebre.yamus +import android.app.SearchManager +import android.content.Context import android.content.Intent import android.content.pm.PackageManager +import android.database.Cursor +import android.database.MatrixCursor import android.media.AudioManager import android.os.Bundle +import android.provider.BaseColumns import android.util.Log +import android.view.Menu import android.view.View +import android.view.inputmethod.InputMethodManager +import android.widget.AutoCompleteTextView import android.widget.LinearLayout import androidx.appcompat.app.AlertDialog -import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatDelegate +import androidx.appcompat.widget.SearchView +import androidx.appcompat.widget.SearchView.OnQueryTextListener import androidx.appcompat.widget.Toolbar import androidx.core.app.ActivityCompat +import androidx.core.os.bundleOf +import androidx.cursoradapter.widget.CursorAdapter +import androidx.cursoradapter.widget.SimpleCursorAdapter import androidx.databinding.DataBindingUtil +import androidx.fragment.app.Fragment import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.get import androidx.navigation.NavController import androidx.navigation.findNavController import androidx.navigation.ui.setupWithNavController @@ -32,20 +46,22 @@ import kg.delletenebre.yamus.databinding.ActivityMainBinding import kg.delletenebre.yamus.media.actions.CustomActionsHelper import kg.delletenebre.yamus.media.library.CurrentPlaylist import kg.delletenebre.yamus.ui.login.LoginActivity +import kg.delletenebre.yamus.ui.search.SearchViewModel import kg.delletenebre.yamus.ui.settings.SettingsActivity import kg.delletenebre.yamus.utils.InjectorUtils +import kg.delletenebre.yamus.utils.UI import kg.delletenebre.yamus.viewmodels.MainActivityViewModel -import kg.delletenebre.yamus.viewmodels.NowPlayingViewModel +import kotlinx.coroutines.launch import kotlin.system.exitProcess -class MainActivity : AppCompatActivity() { +class MainActivity : ScopedAppActivity() { private val PERMISSION_WRITE_EXTERNAL_STORAGE_REQUEST_CODE = 1 private lateinit var navigationController: NavController private lateinit var binding: ActivityMainBinding private lateinit var viewModel: MainActivityViewModel - private lateinit var nowPlayingViewModel: NowPlayingViewModel + private var searchViewModel: SearchViewModel? = null private lateinit var firebaseAnalytics: FirebaseAnalytics override fun onCreate(savedInstanceState: Bundle?) { @@ -59,15 +75,20 @@ class MainActivity : AppCompatActivity() { firebaseAnalytics = FirebaseAnalytics.getInstance(this) - viewModel = ViewModelProvider(this, InjectorUtils.provideMainActivityViewModel(this)) - .get(MainActivityViewModel::class.java) - nowPlayingViewModel = ViewModelProvider(this, InjectorUtils.provideNowPlayingViewModel(this)) - .get(NowPlayingViewModel::class.java) + viewModel = ViewModelProvider(this, InjectorUtils.provideMainActivityViewModel(this)).get() binding = DataBindingUtil.setContentView(this, R.layout.activity_main) binding.lifecycleOwner = this binding.viewModel = viewModel - binding.nowPlayingModel = nowPlayingViewModel + binding.currentPlaylist = CurrentPlaylist + + CurrentPlaylist.currentTrack.observe(this, Observer { + if (it == null) { + + } else { + + } + }) volumeControlStream = AudioManager.STREAM_MUSIC @@ -157,7 +178,10 @@ class MainActivity : AppCompatActivity() { } } - fun setupMainToolbar(toolbar: Toolbar) { + fun setupMainToolbar(toolbar: Toolbar, searchQuery: String? = null) { + initSearch(toolbar.menu, searchQuery) + + UI.setMenuIconsColor(this, toolbar.menu) toolbar.setOnMenuItemClickListener { when (it.itemId) { R.id.action_settings -> { @@ -176,6 +200,8 @@ class MainActivity : AppCompatActivity() { } } + + override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { when (requestCode) { PERMISSION_WRITE_EXTERNAL_STORAGE_REQUEST_CODE -> { @@ -210,4 +236,95 @@ class MainActivity : AppCompatActivity() { ) } } + + private fun initSearch(menu: Menu, searchQuery: String? = null) { + val searchItem = menu.findItem(R.id.action_search) + val searchView = searchItem?.actionView as SearchView + val searchManager = getSystemService(Context.SEARCH_SERVICE) as SearchManager + + val from = arrayOf(SearchManager.SUGGEST_COLUMN_TEXT_1) + val to = intArrayOf(R.id.item_label) + val cursorAdapter = SimpleCursorAdapter( + this, + R.layout.search_item, + null, + from, + to, + CursorAdapter.FLAG_REGISTER_CONTENT_OBSERVER + ) + searchView.suggestionsAdapter = cursorAdapter + searchView.setOnQueryTextListener(object : OnQueryTextListener { + override fun onQueryTextSubmit(query: String?): Boolean { + hideKeyboard(searchView) + + if (navigationController.currentDestination?.id != R.id.fragmentSearch) { + val bundle = bundleOf("searchQuery" to query) + navigationController.navigate(R.id.fragmentSearch, bundle) + } else { + searchViewModel?.search(query ?: "") + } + + return false + } + + override fun onQueryTextChange(query: String?): Boolean { + launch { + val cursor = MatrixCursor(arrayOf(BaseColumns._ID, SearchManager.SUGGEST_COLUMN_TEXT_1)) + query?.let { + if (query.length > 2) { + YaApi.searchSuggest(it).forEachIndexed { index, suggestion -> + cursor.addRow(arrayOf(index, suggestion)) + } + } + } + cursorAdapter.changeCursor(cursor) + cursorAdapter.notifyDataSetChanged() + } + return true + } + }) + + searchView.setOnSuggestionListener(object: SearchView.OnSuggestionListener { + override fun onSuggestionSelect(position: Int): Boolean { + return false + } + + override fun onSuggestionClick(position: Int): Boolean { + hideKeyboard(searchView) + val cursor = searchView.suggestionsAdapter.getItem(position) as Cursor + val selection = cursor.getString(cursor.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_1)) + searchView.setQuery(selection, false) + + // Do something with selection + return true + } + }) + + searchView.apply { + setSearchableInfo(searchManager.getSearchableInfo(componentName)) + val textView = findViewById(R.id.search_src_text) + textView.threshold = 3 + if (searchQuery != null) { + searchItem.expandActionView() + searchView.setQuery(searchQuery, true) + searchView.clearFocus() + } + + } + } + + fun setSearchViewModel(viewModel: SearchViewModel?) { + searchViewModel = viewModel + } + + fun Context.hideKeyboard(view: View) { + val inputMethodManager = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager + inputMethodManager.hideSoftInputFromWindow(view.windowToken, 0) + } + + fun Fragment.hideKeyboard() { + view?.let { + activity?.hideKeyboard(it) + } + } } diff --git a/app/src/main/java/kg/delletenebre/yamus/ScopedAppActivity.kt b/app/src/main/java/kg/delletenebre/yamus/ScopedAppActivity.kt new file mode 100644 index 0000000..59815a2 --- /dev/null +++ b/app/src/main/java/kg/delletenebre/yamus/ScopedAppActivity.kt @@ -0,0 +1,24 @@ +package kg.delletenebre.yamus + +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlin.coroutines.CoroutineContext + +abstract class ScopedAppActivity: AppCompatActivity(), CoroutineScope { + protected lateinit var job: Job + override val coroutineContext: CoroutineContext + get() = job + Dispatchers.Main + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + job = Job() + } + + override fun onDestroy() { + super.onDestroy() + job.cancel() + } +} \ No newline at end of file diff --git a/app/src/main/java/kg/delletenebre/yamus/fragments/NowPlayingFragment.kt b/app/src/main/java/kg/delletenebre/yamus/fragments/NowPlayingFragment.kt deleted file mode 100644 index 815f17c..0000000 --- a/app/src/main/java/kg/delletenebre/yamus/fragments/NowPlayingFragment.kt +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Copyright 2019 Google Inc. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package kg.delletenebre.yamus.fragments - -import android.net.Uri -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.ImageButton -import android.widget.ImageView -import android.widget.TextView -import androidx.fragment.app.Fragment -import androidx.lifecycle.Observer -import androidx.lifecycle.ViewModelProvider -import com.bumptech.glide.Glide -import kg.delletenebre.yamus.R -import kg.delletenebre.yamus.utils.InjectorUtils -import kg.delletenebre.yamus.viewmodels.MainActivityViewModel -import kg.delletenebre.yamus.viewmodels.NowPlayingFragmentViewModel -import kg.delletenebre.yamus.viewmodels.NowPlayingFragmentViewModel.NowPlayingMetadata - -/** - * A fragment representing the current media item being played. - */ -class NowPlayingFragment : Fragment() { - private lateinit var mainActivityViewModel: MainActivityViewModel - private lateinit var nowPlayingViewModel: NowPlayingFragmentViewModel - private lateinit var positionTextView: TextView - - companion object { - fun newInstance() = NowPlayingFragment() - } - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle?): View? { - return inflater.inflate(R.layout.fragment_nowplaying, container, false) - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - // Always true, but lets lint know that as well. - val context = activity ?: return - - // Inject our activity and view models into this fragment - mainActivityViewModel = ViewModelProvider(context, InjectorUtils.provideMainActivityViewModel(context)) - .get(MainActivityViewModel::class.java) - nowPlayingViewModel = ViewModelProvider(context, InjectorUtils.provideNowPlayingFragmentViewModel(context)) - .get(NowPlayingFragmentViewModel::class.java) - - // Attach observers to the LiveData coming from this ViewModel - nowPlayingViewModel.mediaMetadata.observe(this, - Observer { mediaItem -> updateUI(view, mediaItem) }) - nowPlayingViewModel.mediaButtonRes.observe(this, - Observer { res -> view.findViewById(R.id.media_button).setImageResource(res) }) - nowPlayingViewModel.mediaPosition.observe(this, - Observer { pos -> positionTextView.text = - NowPlayingMetadata.timestampToMSS(context, pos) }) - - // Setup UI handlers for buttons - view.findViewById(R.id.media_button).setOnClickListener { - nowPlayingViewModel.mediaMetadata.value?.let { mainActivityViewModel.playMediaId(it.id) } } - - // Initialize playback duration and position to zero - view.findViewById(R.id.duration).text = - NowPlayingMetadata.timestampToMSS(context, 0L) - positionTextView = view.findViewById(R.id.position) - .apply { text = NowPlayingMetadata.timestampToMSS(context, 0L) } - } - - /** - * Internal function used to update all UI elements except for the current item playback - */ - private fun updateUI(view: View, metadata: NowPlayingMetadata) { - val albumArtView = view.findViewById(R.id.albumArt) - if (metadata.albumArtUri == Uri.EMPTY) { - albumArtView.setImageResource(R.drawable.ic_album) - } else { - Glide.with(view) - .load(metadata.albumArtUri) - .into(albumArtView) - } - view.findViewById(R.id.title).text = metadata.title - view.findViewById(R.id.subtitle).text = metadata.subtitle - view.findViewById(R.id.duration).text = metadata.duration - } -} \ No newline at end of file diff --git a/app/src/main/java/kg/delletenebre/yamus/ui/search/SearchFragment.kt b/app/src/main/java/kg/delletenebre/yamus/ui/search/SearchFragment.kt index 9dc0e04..f8a0c6a 100644 --- a/app/src/main/java/kg/delletenebre/yamus/ui/search/SearchFragment.kt +++ b/app/src/main/java/kg/delletenebre/yamus/ui/search/SearchFragment.kt @@ -2,25 +2,62 @@ package kg.delletenebre.yamus.ui.search import android.os.Bundle +import android.support.v4.media.MediaBrowserCompat import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.core.os.bundleOf import androidx.fragment.app.Fragment +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.get +import androidx.navigation.fragment.findNavController import kg.delletenebre.yamus.MainActivity import kg.delletenebre.yamus.R +import kg.delletenebre.yamus.databinding.SearchFragmentBinding +import kg.delletenebre.yamus.media.library.MediaLibrary +import kg.delletenebre.yamus.ui.OnMediaItemClickListener -/** - * A simple [Fragment] subclass. - */ class SearchFragment : Fragment() { + private lateinit var viewModel: SearchViewModel - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle?): View? { - return inflater.inflate(R.layout.search_fragment, container, false) + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + viewModel = ViewModelProvider(this).get() } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - (activity as MainActivity).setupMainToolbar(view.findViewById(R.id.toolbar)) + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + val root = SearchFragmentBinding.inflate(inflater, container, false).also { + it.lifecycleOwner = this + it.viewModel = viewModel + it.executePendingBindings() + }.root + + viewModel.itemClickListenerOfFragment = object : OnMediaItemClickListener { + override fun onClick(item: MediaBrowserCompat.MediaItem) { + val path = item.mediaId + if (path != null) { + val bundle = bundleOf( + "title" to item.description.title, + "path" to item.mediaId + ) + when { + path.startsWith(MediaLibrary.PATH_PLAYLIST) -> + findNavController().navigate(R.id.fragmentPlaylist, bundle) + } + } + } + } + + val mainActivity = (activity as MainActivity) + val searchQuery = arguments?.getString("searchQuery") + + mainActivity.setSearchViewModel(viewModel) + mainActivity.setupMainToolbar(root.findViewById(R.id.toolbar), searchQuery) + + return root } } diff --git a/app/src/main/java/kg/delletenebre/yamus/ui/search/SearchViewModel.kt b/app/src/main/java/kg/delletenebre/yamus/ui/search/SearchViewModel.kt new file mode 100644 index 0000000..b2f6a81 --- /dev/null +++ b/app/src/main/java/kg/delletenebre/yamus/ui/search/SearchViewModel.kt @@ -0,0 +1,69 @@ +package kg.delletenebre.yamus.ui.search + +import android.support.v4.media.MediaBrowserCompat +import android.support.v4.media.MediaMetadataCompat +import androidx.databinding.library.baseAdapters.BR +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import androidx.recyclerview.widget.DiffUtil +import kg.delletenebre.yamus.R +import kg.delletenebre.yamus.api.YaApi +import kg.delletenebre.yamus.api.responses.Playlist +import kg.delletenebre.yamus.media.extensions.id +import kg.delletenebre.yamus.media.library.MediaLibrary.createPlaylistMediaItem +import kg.delletenebre.yamus.ui.OnMediaItemClickListener +import kg.delletenebre.yamus.utils.toCoverUri +import kotlinx.coroutines.launch +import me.tatarka.bindingcollectionadapter2.ItemBinding + + +class SearchViewModel : ViewModel(), OnMediaItemClickListener { + var itemClickListenerOfFragment: OnMediaItemClickListener? = null + override fun onClick(item: MediaBrowserCompat.MediaItem) { + itemClickListenerOfFragment?.onClick(item) + } + + + val diff: DiffUtil.ItemCallback = object : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: MediaMetadataCompat, newItem: MediaMetadataCompat): Boolean { + return oldItem.id == newItem.id + } + + override fun areContentsTheSame(oldItem: MediaMetadataCompat, newItem: MediaMetadataCompat): Boolean { + return oldItem.equals(newItem) + } + } + val diffMediaItem: DiffUtil.ItemCallback = object : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: MediaBrowserCompat.MediaItem, newItem: MediaBrowserCompat.MediaItem): Boolean { + return oldItem.mediaId == newItem.mediaId + } + + override fun areContentsTheSame(oldItem: MediaBrowserCompat.MediaItem, newItem: MediaBrowserCompat.MediaItem): Boolean { + return oldItem.equals(newItem) + } + } + val playlists: MutableLiveData> = MutableLiveData() + val playlistsBinding = ItemBinding.of(BR.item, R.layout.search_playlist_item) + .bindExtra(BR.listener, this) + + + fun search(query: String) { + viewModelScope.launch { + YaApi.search(query).forEach { + if (it.type == "playlists") { + val items = (it.result as List).map { playlist -> + createPlaylistMediaItem( + id = "/playlist/${playlist.uid}/${playlist.kind}", + title = playlist.title, +// subtitle = getString(R.string.updated_at, updatedAt), + icon = playlist.ogImage.toCoverUri(200) +// groupTitle = resources.getString(R.string.personal_playlists_group) + ) + } + playlists.postValue(items) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/kg/delletenebre/yamus/ui/stations/StationsFragment.kt b/app/src/main/java/kg/delletenebre/yamus/ui/stations/StationsFragment.kt index 71b890b..a935d72 100644 --- a/app/src/main/java/kg/delletenebre/yamus/ui/stations/StationsFragment.kt +++ b/app/src/main/java/kg/delletenebre/yamus/ui/stations/StationsFragment.kt @@ -9,6 +9,8 @@ import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.get import com.google.android.material.tabs.TabLayout +import kg.delletenebre.yamus.MainActivity +import kg.delletenebre.yamus.R import kg.delletenebre.yamus.databinding.StationsFragmentBinding import kg.delletenebre.yamus.ui.OnMediaItemClickListener import kg.delletenebre.yamus.utils.InjectorUtils @@ -46,6 +48,8 @@ class StationsFragment : Fragment() { } } + (activity as MainActivity).setupMainToolbar(root.findViewById(R.id.toolbar)) + return root } } \ No newline at end of file diff --git a/app/src/main/java/kg/delletenebre/yamus/utils/InjectorUtils.kt b/app/src/main/java/kg/delletenebre/yamus/utils/InjectorUtils.kt index 84e595d..e9b1f98 100644 --- a/app/src/main/java/kg/delletenebre/yamus/utils/InjectorUtils.kt +++ b/app/src/main/java/kg/delletenebre/yamus/utils/InjectorUtils.kt @@ -16,15 +16,12 @@ package kg.delletenebre.yamus.utils -import android.app.Application import android.content.ComponentName import android.content.Context import kg.delletenebre.yamus.common.MediaSessionConnection import kg.delletenebre.yamus.media.MusicService import kg.delletenebre.yamus.viewmodels.MainActivityViewModel import kg.delletenebre.yamus.viewmodels.MediaItemFragmentViewModel -import kg.delletenebre.yamus.viewmodels.NowPlayingFragmentViewModel -import kg.delletenebre.yamus.viewmodels.NowPlayingViewModel /** * Static methods used to inject classes needed for various Activities and Fragments. @@ -47,20 +44,4 @@ object InjectorUtils { val mediaSessionConnection = provideMediaSessionConnection(applicationContext) return MediaItemFragmentViewModel.Factory(mediaId, mediaSessionConnection) } - - fun provideNowPlayingFragmentViewModel(context: Context) - : NowPlayingFragmentViewModel.Factory { - val applicationContext = context.applicationContext - val mediaSessionConnection = provideMediaSessionConnection(applicationContext) - return NowPlayingFragmentViewModel.Factory( - applicationContext as Application, mediaSessionConnection) - } - - fun provideNowPlayingViewModel(context: Context) - : NowPlayingViewModel.Factory { - val applicationContext = context.applicationContext - val mediaSessionConnection = provideMediaSessionConnection(applicationContext) - return NowPlayingViewModel.Factory( - applicationContext as Application, mediaSessionConnection) - } } \ No newline at end of file diff --git a/app/src/main/java/kg/delletenebre/yamus/viewmodels/NowPlayingFragmentViewModel.kt b/app/src/main/java/kg/delletenebre/yamus/viewmodels/NowPlayingFragmentViewModel.kt deleted file mode 100644 index a65656d..0000000 --- a/app/src/main/java/kg/delletenebre/yamus/viewmodels/NowPlayingFragmentViewModel.kt +++ /dev/null @@ -1,194 +0,0 @@ -/* - * Copyright 2019 Google Inc. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package kg.delletenebre.yamus.viewmodels - -import android.app.Application -import android.content.Context -import android.net.Uri -import android.os.Handler -import android.os.Looper -import android.support.v4.media.MediaBrowserCompat -import android.support.v4.media.MediaMetadataCompat -import android.support.v4.media.session.PlaybackStateCompat -import androidx.lifecycle.* -import kg.delletenebre.yamus.R -import kg.delletenebre.yamus.common.EMPTY_PLAYBACK_STATE -import kg.delletenebre.yamus.common.MediaSessionConnection -import kg.delletenebre.yamus.common.NOTHING_PLAYING -import kg.delletenebre.yamus.fragments.NowPlayingFragment -import kg.delletenebre.yamus.media.extensions.* - -/** - * [ViewModel] for [NowPlayingFragment] which displays the album art in full size. - * It extends AndroidViewModel and uses the [Application]'s context to be able to reference string - * resources. - */ -class NowPlayingFragmentViewModel( - private val app: Application, - mediaSessionConnection: MediaSessionConnection -) : AndroidViewModel(app) { - - /** - * Utility class used to represent the metadata necessary to display the - * media item currently being played. - */ - data class NowPlayingMetadata( - val id: String, - val albumArtUri: Uri, - val title: String?, - val subtitle: String?, - val duration: String - ) { - - companion object { - /** - * Utility method to convert milliseconds to a display of minutes and seconds - */ - fun timestampToMSS(context: Context, position: Long): String { - val totalSeconds = Math.floor(position / 1E3).toInt() - val minutes = totalSeconds / 60 - val remainingSeconds = totalSeconds - (minutes * 60) - return if (position < 0) context.getString(R.string.duration_unknown) - else context.getString(R.string.duration_format).format(minutes, remainingSeconds) - } - } - } - - private var playbackState: PlaybackStateCompat = EMPTY_PLAYBACK_STATE - val mediaMetadata = MutableLiveData() - val mediaPosition = MutableLiveData().apply { - postValue(0L) - } - val mediaButtonRes = MutableLiveData().apply { - postValue(R.drawable.ic_album) - } - - private var updatePosition = true - private val handler = Handler(Looper.getMainLooper()) - - /** - * When the session's [PlaybackStateCompat] changes, the [mediaItems] need to be updated - * so the correct [MediaItemData.playbackRes] is displayed on the active item. - * (i.e.: play/pause button or blank) - */ - private val playbackStateObserver = Observer { - playbackState = it ?: EMPTY_PLAYBACK_STATE - val metadata = mediaSessionConnection.nowPlaying.value ?: NOTHING_PLAYING - updateState(playbackState, metadata) - } - - /** - * When the session's [MediaMetadataCompat] changes, the [mediaItems] need to be updated - * as it means the currently active item has changed. As a result, the new, and potentially - * old item (if there was one), both need to have their [MediaItemData.playbackRes] - * changed. (i.e.: play/pause button or blank) - */ - private val mediaMetadataObserver = Observer { - updateState(playbackState, it) - } - - /** - * Because there's a complex dance between this [ViewModel] and the [MediaSessionConnection] - * (which is wrapping a [MediaBrowserCompat] object), the usual guidance of using - * [Transformations] doesn't quite work. - * - * Specifically there's three things that are watched that will cause the single piece of - * [LiveData] exposed from this class to be updated. - * - * [MediaSessionConnection.playbackState] changes state based on the playback state of - * the player, which can change the [MediaItemData.playbackRes]s in the list. - * - * [MediaSessionConnection.nowPlaying] changes based on the item that's being played, - * which can also change the [MediaItemData.playbackRes]s in the list. - */ - private val mediaSessionConnection = mediaSessionConnection.also { - it.playbackState.observeForever(playbackStateObserver) - it.nowPlaying.observeForever(mediaMetadataObserver) - checkPlaybackPosition() - } - - /** - * Internal function that recursively calls itself every [POSITION_UPDATE_INTERVAL_MILLIS] ms - * to check the current playback position and updates the corresponding LiveData object when it - * has changed. - */ - private fun checkPlaybackPosition(): Boolean = handler.postDelayed({ - val currPosition = playbackState.currentPlayBackPosition - if (mediaPosition.value != currPosition) - mediaPosition.postValue(currPosition) - if (updatePosition) - checkPlaybackPosition() - }, POSITION_UPDATE_INTERVAL_MILLIS) - - /** - * Since we use [LiveData.observeForever] above (in [mediaSessionConnection]), we want - * to call [LiveData.removeObserver] here to prevent leaking resources when the [ViewModel] - * is not longer in use. - * - * For more details, see the kdoc on [mediaSessionConnection] above. - */ - override fun onCleared() { - super.onCleared() - - // Remove the permanent observers from the MediaSessionConnection. - mediaSessionConnection.playbackState.removeObserver(playbackStateObserver) - mediaSessionConnection.nowPlaying.removeObserver(mediaMetadataObserver) - - // Stop updating the position - updatePosition = false - } - - private fun updateState( - playbackState: PlaybackStateCompat, - mediaMetadata: MediaMetadataCompat - ) { - - // Only update media item once we have duration available - if (mediaMetadata.duration != 0L) { - val nowPlayingMetadata = NowPlayingMetadata( - mediaMetadata.id, - mediaMetadata.albumArtUri, - mediaMetadata.title?.trim(), - mediaMetadata.displaySubtitle?.trim(), - NowPlayingMetadata.timestampToMSS(app, mediaMetadata.duration) - ) - this.mediaMetadata.postValue(nowPlayingMetadata) - } - - // Update the media button resource ID - mediaButtonRes.postValue( - when (playbackState.isPlaying) { - true -> R.drawable.ic_pause - else -> R.drawable.ic_play - } - ) - } - - class Factory( - private val app: Application, - private val mediaSessionConnection: MediaSessionConnection - ) : ViewModelProvider.NewInstanceFactory() { - - @Suppress("unchecked_cast") - override fun create(modelClass: Class): T { - return NowPlayingFragmentViewModel(app, mediaSessionConnection) as T - } - } -} - -private const val TAG = "NowPlayingFragmentVM" -private const val POSITION_UPDATE_INTERVAL_MILLIS = 100L \ No newline at end of file diff --git a/app/src/main/java/kg/delletenebre/yamus/viewmodels/NowPlayingViewModel.kt b/app/src/main/java/kg/delletenebre/yamus/viewmodels/NowPlayingViewModel.kt deleted file mode 100644 index f5fdb5e..0000000 --- a/app/src/main/java/kg/delletenebre/yamus/viewmodels/NowPlayingViewModel.kt +++ /dev/null @@ -1,148 +0,0 @@ -package kg.delletenebre.yamus.viewmodels - -import android.app.Application -import android.os.Handler -import android.os.Looper -import android.support.v4.media.MediaBrowserCompat -import android.support.v4.media.MediaMetadataCompat -import android.support.v4.media.session.PlaybackStateCompat -import androidx.lifecycle.* -import kg.delletenebre.yamus.R -import kg.delletenebre.yamus.common.EMPTY_PLAYBACK_STATE -import kg.delletenebre.yamus.common.MediaSessionConnection -import kg.delletenebre.yamus.common.NOTHING_PLAYING -import kg.delletenebre.yamus.media.extensions.currentPlayBackPosition -import kg.delletenebre.yamus.media.extensions.isPlaying - -class NowPlayingViewModel( - app: Application, - mediaSessionConnection: MediaSessionConnection -) : AndroidViewModel(app) { - - val track = MutableLiveData() - val position = MutableLiveData().apply { - postValue(0L) - } - - val playPauseDrawable = MutableLiveData().apply { - postValue(R.drawable.ic_pause) - } - - val likeUnlikeDrawable = MutableLiveData().apply { - postValue(R.drawable.ic_favorite_border) - } - - - var playbackState = MutableLiveData().apply { - postValue(EMPTY_PLAYBACK_STATE) - } - - private var updatePosition = true - private val handler = Handler(Looper.getMainLooper()) - - /** - * When the session's [PlaybackStateCompat] changes, the [mediaItems] need to be updated - * so the correct [MediaItemData.playbackRes] is displayed on the active item. - * (i.e.: play/pause button or blank) - */ - private val playbackStateObserver = Observer { - val metadata = mediaSessionConnection.nowPlaying.value ?: NOTHING_PLAYING - updateState(it ?: EMPTY_PLAYBACK_STATE, metadata) - } - - /** - * When the session's [MediaMetadataCompat] changes, the [mediaItems] need to be updated - * as it means the currently active item has changed. As a result, the new, and potentially - * old item (if there was one), both need to have their [MediaItemData.playbackRes] - * changed. (i.e.: play/pause button or blank) - */ - private val mediaMetadataObserver = Observer { - updateState(playbackState.value!!, it) - } - - /** - * Because there's a complex dance between this [ViewModel] and the [MediaSessionConnection] - * (which is wrapping a [MediaBrowserCompat] object), the usual guidance of using - * [Transformations] doesn't quite work. - * - * Specifically there's three things that are watched that will cause the single piece of - * [LiveData] exposed from this class to be updated. - * - * [MediaSessionConnection.playbackState] changes state based on the playback state of - * the player, which can change the [MediaItemData.playbackRes]s in the list. - * - * [MediaSessionConnection.nowPlaying] changes based on the item that's being played, - * which can also change the [MediaItemData.playbackRes]s in the list. - */ - private val mediaSessionConnection = mediaSessionConnection.also { - it.playbackState.observeForever(playbackStateObserver) - it.nowPlaying.observeForever(mediaMetadataObserver) - checkPlaybackPosition() - } - - /** - * Internal function that recursively calls itself every [POSITION_UPDATE_INTERVAL_MILLIS] ms - * to check the current playback position and updates the corresponding LiveData object when it - * has changed. - */ - private fun checkPlaybackPosition(): Boolean = handler.postDelayed({ - val currPosition = playbackState.value!!.currentPlayBackPosition - if (position.value != currPosition) - position.postValue(currPosition) - if (updatePosition) - checkPlaybackPosition() - }, POSITION_UPDATE_INTERVAL_MILLIS) - - /** - * Since we use [LiveData.observeForever] above (in [mediaSessionConnection]), we want - * to call [LiveData.removeObserver] here to prevent leaking resources when the [ViewModel] - * is not longer in use. - * - * For more details, see the kdoc on [mediaSessionConnection] above. - */ - override fun onCleared() { - super.onCleared() - - // Remove the permanent observers from the MediaSessionConnection. - mediaSessionConnection.playbackState.removeObserver(playbackStateObserver) - mediaSessionConnection.nowPlaying.removeObserver(mediaMetadataObserver) - - // Stop updating the position - updatePosition = false - } - - private fun updateState( - playbackState: PlaybackStateCompat, - mediaMetadata: MediaMetadataCompat - ) { - this.playbackState.value = playbackState - // Only update media item once we have duration available -// if (mediaMetadata.duration != 0L) { -// val track = CurrentPlaylist.tracks.find { -// it.uniqueId == mediaMetadata.uniqueId -// } -// this.track.postValue(track) -// } - - // Update the media button resource ID - playPauseDrawable.postValue( - when (playbackState.isPlaying) { - true -> R.drawable.avd_play_pause - else -> R.drawable.avd_pause_play - } - ) - } - - class Factory( - private val app: Application, - private val mediaSessionConnection: MediaSessionConnection - ) : ViewModelProvider.NewInstanceFactory() { - - @Suppress("unchecked_cast") - override fun create(modelClass: Class): T { - return NowPlayingViewModel(app, mediaSessionConnection) as T - } - } -} - -private const val POSITION_UPDATE_INTERVAL_MILLIS = 100L \ No newline at end of file diff --git a/app/src/main/res/layout-land/home_fragment.xml b/app/src/main/res/layout-land/home_fragment.xml index 65eae02..7ea88ab 100644 --- a/app/src/main/res/layout-land/home_fragment.xml +++ b/app/src/main/res/layout-land/home_fragment.xml @@ -16,7 +16,7 @@ - + + android:visibility='@{currentPlaylist.currentTrack == null ? View.GONE : View.VISIBLE}'> - - - - - - - + android:text="@{currentPlaylist.currentTrack.getString(MediaMetadataCompat.METADATA_KEY_TITLE)}" /> + android:text="@{currentPlaylist.currentTrack.getString(MediaMetadataCompat.METADATA_KEY_ARTIST)}" /> @@ -114,7 +105,7 @@ android:layout_gravity="center_vertical" android:background="?attr/selectableItemBackground" android:onClick="@{() -> viewModel.playerViewPlayPauseClick()}" - app:animateDrawable="@{nowPlayingModel.playPauseDrawable}" /> + app:animateDrawable='@{currentPlaylist.playbackState == "STATE_PLAYING" || currentPlaylist.playbackState == "STATE_BUFFERING" ? @drawable/avd_play_pause : @drawable/avd_pause_play}' /> @@ -144,7 +135,7 @@ android:layout_height="wrap_content" android:background="?android:attr/windowBackground" app:labelVisibilityMode="labeled" - app:menu="@menu/menu_bottom_navigation" /> + app:menu="@menu/bottom_navigation_menu" /> diff --git a/app/src/main/res/layout/fragment_nowplaying.xml b/app/src/main/res/layout/fragment_nowplaying.xml deleted file mode 100644 index a12fd57..0000000 --- a/app/src/main/res/layout/fragment_nowplaying.xml +++ /dev/null @@ -1,116 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/home_fragment.xml b/app/src/main/res/layout/home_fragment.xml index 3b9a99f..0f46462 100644 --- a/app/src/main/res/layout/home_fragment.xml +++ b/app/src/main/res/layout/home_fragment.xml @@ -16,9 +16,10 @@ + diff --git a/app/src/main/res/layout/my_music_fragment.xml b/app/src/main/res/layout/my_music_fragment.xml index 0cb99ce..0919178 100644 --- a/app/src/main/res/layout/my_music_fragment.xml +++ b/app/src/main/res/layout/my_music_fragment.xml @@ -15,7 +15,7 @@ + app:menu="@menu/playlist_menu" /> diff --git a/app/src/main/res/layout/profile_fragment.xml b/app/src/main/res/layout/profile_fragment.xml index 3f80729..5cd8781 100644 --- a/app/src/main/res/layout/profile_fragment.xml +++ b/app/src/main/res/layout/profile_fragment.xml @@ -16,7 +16,7 @@ diff --git a/app/src/main/res/layout/search_fragment.xml b/app/src/main/res/layout/search_fragment.xml index d59c5ae..57ff002 100644 --- a/app/src/main/res/layout/search_fragment.xml +++ b/app/src/main/res/layout/search_fragment.xml @@ -1,21 +1,48 @@ - - - - - + + + + + + + android:layout_height="wrap_content" + android:orientation="vertical"> + + + + + + + + + + + + + + + - \ No newline at end of file + + \ No newline at end of file diff --git a/app/src/main/res/layout/search_item.xml b/app/src/main/res/layout/search_item.xml new file mode 100644 index 0000000..341cb2b --- /dev/null +++ b/app/src/main/res/layout/search_item.xml @@ -0,0 +1,23 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/search_playlist_item.xml b/app/src/main/res/layout/search_playlist_item.xml new file mode 100644 index 0000000..49c9d7b --- /dev/null +++ b/app/src/main/res/layout/search_playlist_item.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/stations_fragment.xml b/app/src/main/res/layout/stations_fragment.xml index 7947384..02ff287 100644 --- a/app/src/main/res/layout/stations_fragment.xml +++ b/app/src/main/res/layout/stations_fragment.xml @@ -16,7 +16,7 @@ + android:id="@+id/action_search" + android:title="@string/menu_main_search" + android:icon="@drawable/ic_search" + app:showAsAction="ifRoom|collapseActionView" + app:actionViewClass="androidx.appcompat.widget.SearchView" /> + app:showAsAction="never" /> + + + app:showAsAction="never" /> diff --git a/app/src/main/res/menu/menu_playlist.xml b/app/src/main/res/menu/playlist_menu.xml similarity index 100% rename from app/src/main/res/menu/menu_playlist.xml rename to app/src/main/res/menu/playlist_menu.xml diff --git a/app/src/main/res/menu/menu_profile.xml b/app/src/main/res/menu/profile_menu.xml similarity index 100% rename from app/src/main/res/menu/menu_profile.xml rename to app/src/main/res/menu/profile_menu.xml diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index e8ebfb0..c9203b8 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -7,6 +7,7 @@ Ямуз + Песня, альбом, исполнитель Skip back 10s Skip forward 10s @@ -85,7 +86,9 @@ + Поиск музыки Профиль + Настройки Закрыть @@ -94,7 +97,6 @@ Настройки - Настройки Пожалуйста, подождите… diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7fb73fd..5343ab0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -7,6 +7,7 @@ Yamus + Track, album, artist Skip back 10s Skip forward 10s @@ -85,7 +86,9 @@ + Music Search Profile + Settings Close app @@ -94,7 +97,6 @@ Settings - Settings Please wait… diff --git a/app/src/main/res/xml/searchable.xml b/app/src/main/res/xml/searchable.xml new file mode 100644 index 0000000..c01fba8 --- /dev/null +++ b/app/src/main/res/xml/searchable.xml @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/build.gradle b/build.gradle index 0628a2b..31d3fd8 100644 --- a/build.gradle +++ b/build.gradle @@ -8,7 +8,7 @@ buildscript { targetSdkVersion = 29 // Dependency versions. - kotlin_version = '1.3.50' + kotlin_version = '1.3.61' kotlin_coroutines = '1.1.0' } @@ -24,10 +24,10 @@ buildscript { } } dependencies { - classpath 'com.android.tools.build:gradle:3.5.2' + classpath 'com.android.tools.build:gradle:3.5.3' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version" - classpath 'com.google.gms:google-services:4.3.2' // REMOVE IF NOT USE FIREBASE + classpath 'com.google.gms:google-services:4.3.3' // REMOVE IF NOT USE FIREBASE classpath 'io.fabric.tools:gradle:1.31.1' // Crashlytics plugin // NOTE: Do not place your application dependencies here; they belong diff --git a/common/build.gradle b/common/build.gradle index 2d7ffcb..4902e88 100644 --- a/common/build.gradle +++ b/common/build.gradle @@ -52,7 +52,7 @@ dependencies { api 'com.jakewharton.threetenabp:threetenabp:1.2.1' // Room - def room_version = '2.2.1' + def room_version = '2.2.3' api "androidx.room:room-runtime:$room_version" kapt "androidx.room:room-compiler:$room_version" api "androidx.room:room-ktx:$room_version" diff --git a/common/src/main/java/kg/delletenebre/yamus/api/YaApi.kt b/common/src/main/java/kg/delletenebre/yamus/api/YaApi.kt index 5d0ce7e..b180904 100644 --- a/common/src/main/java/kg/delletenebre/yamus/api/YaApi.kt +++ b/common/src/main/java/kg/delletenebre/yamus/api/YaApi.kt @@ -23,6 +23,7 @@ import kg.delletenebre.yamus.media.extensions.from import kg.delletenebre.yamus.utils.HashUtils import kg.delletenebre.yamus.utils.md5 import kotlinx.coroutines.* +import kotlinx.serialization.UnstableDefault import kotlinx.serialization.internal.ArrayListSerializer import kotlinx.serialization.json.Json import org.json.JSONArray @@ -31,6 +32,7 @@ import java.text.SimpleDateFormat import java.util.* +@UnstableDefault object YaApi { private const val TAG = "ahoha" private const val CLIENT_ID = "23cabbbdc6cd418abb4b39c32c41195d" @@ -51,9 +53,6 @@ object YaApi { const val USER_TRACKS_ACTION_ADD = "add-multiple" const val USER_TRACKS_ACTION_REMOVE = "remove" - const val RESULT_OK = "ok" - const val RESULT_ERROR = "error" - private var accessToken: String = "" private var uid: Long = 0L private lateinit var resources: Resources @@ -182,19 +181,19 @@ object YaApi { val searchResults = mutableListOf() var type = "albums" if (json.has(type)) { - searchResults.add(getSearchResult(type, json.getJSONObject(type))) + searchResults.add(getSearchResult(type, json)) } type = "artists" if (json.has(type)) { - searchResults.add(getSearchResult(type, json.getJSONObject(type))) + searchResults.add(getSearchResult(type, json)) } type = "playlists" if (json.has(type)) { - searchResults.add(getSearchResult(type, json.getJSONObject(type))) + searchResults.add(getSearchResult(type, json)) } type = "tracks" if (json.has(type)) { - searchResults.add(getSearchResult(type, json.getJSONObject(type))) + searchResults.add(getSearchResult(type, json)) } searchResults @@ -204,6 +203,19 @@ object YaApi { } } + suspend fun searchSuggest(text: String): List { + val response = makeRequest("/search/suggest?part=$text") + return try { + val json = JSONObject(response).getJSONObject("result").getJSONArray("suggestions") + return Array(json.length()) { i -> + json.optString(i) + }.toList() + } catch (e: Exception) { + Log.e(TAG, "search() exception: ${e.message}") + listOf() + } + } + suspend fun getPersonalPlaylists(): List { //https://api.music.yandex.net/landing3?blocks=personalplaylists,promotions,new-releases,new-playlists,mixes,chart,charts,artists,albums,playlists,play_contexts val response = makeRequest("/landing3?blocks=personalplaylists") @@ -393,7 +405,7 @@ object YaApi { "/play-audio".httpPost(postData).awaitStringResponseResult() } - suspend fun getUserTracksIds(type: String): Pair> { + private suspend fun getUserTracksIds(type: String): Pair> { val cachedTracksIds = database.userTracksIds().get(type) return if (cachedTracksIds != null) { (cachedTracksIds.revision to cachedTracksIds.tracksIds.split(",").toMutableList()) @@ -402,7 +414,7 @@ object YaApi { } } - suspend fun updateUserTracksIds(type: String, revision: Int): Pair> { + private suspend fun updateUserTracksIds(type: String, revision: Int): Pair> { val empty = (0 to mutableListOf()) val response = makeRequest( url = "/users/$uid/${type}s/tracks?if-modified-since-revision=$revision", @@ -445,11 +457,11 @@ object YaApi { USER_TRACKS_ACTION_ADD -> { when (type) { USER_TRACKS_TYPE_LIKE -> { - removeDislike(trackId) + dislikedTracks.second.remove(trackId) likedTracks.second.add(0, trackId) } USER_TRACKS_TYPE_DISLIKE -> { - removeLike(trackId) + likedTracks.second.remove(trackId) dislikedTracks.second.add(0, trackId) } } @@ -702,11 +714,13 @@ object YaApi { return "https://$host/get-mp3/$sign/${downloadInfo.ts}${downloadInfo.path}" } - fun updateUserTrack(action: String, type: String, trackId: String): Result { + private fun updateUserTrack(action: String, type: String, trackId: String): Result { return runBlocking { val url = "/users/$uid/${type}s/tracks/$action" val postData = listOf("track-ids" to trackId) - val (_, _, result) = url.httpPost(postData).awaitStringResponseResult() + val (request, response, result) = url.httpPost(postData).awaitStringResponseResult() + Log.d("ahoha", "request: $request") + Log.d("ahoha", "response: $response") result.fold( { updateUserTracksIds(action, type, trackId) }, { error -> error.printStackTrace() } @@ -758,7 +772,7 @@ object YaApi { } } - fun getCacheHeaders(forceOnline: Boolean = false): Map { + private fun getCacheHeaders(forceOnline: Boolean = false): Map { return if (forceOnline) { mapOf("pragma" to "no-cache", "cache-control" to "no-cache") } else { @@ -787,7 +801,6 @@ object YaApi { } val (_, _, result) = request.header(getCacheHeaders(forceOnline)) .awaitStringResponseResult() - result.fold( { data -> database.httpCache().insert( diff --git a/common/src/main/java/kg/delletenebre/yamus/api/responses/Mix.kt b/common/src/main/java/kg/delletenebre/yamus/api/responses/Mix.kt index 1c75061..a60737e 100644 --- a/common/src/main/java/kg/delletenebre/yamus/api/responses/Mix.kt +++ b/common/src/main/java/kg/delletenebre/yamus/api/responses/Mix.kt @@ -6,5 +6,6 @@ import kotlinx.serialization.Serializable data class Mix( val backgroundImageUri: String, val title: String, - val url: String + val url: String, + val urlScheme: String ) \ No newline at end of file diff --git a/common/src/main/java/kg/delletenebre/yamus/media/datasource/YandexDataSource.kt b/common/src/main/java/kg/delletenebre/yamus/media/datasource/YandexDataSource.kt index 5489574..b4b7e82 100644 --- a/common/src/main/java/kg/delletenebre/yamus/media/datasource/YandexDataSource.kt +++ b/common/src/main/java/kg/delletenebre/yamus/media/datasource/YandexDataSource.kt @@ -47,6 +47,7 @@ class YandexDataSource( private var connection: HttpURLConnection? = null private var inputStream: InputStream? = null private var opened: Boolean = false + private var responseCode: Int = -1 private var bytesToSkip: Long = 0 private var bytesToRead: Long = 0 @@ -81,6 +82,14 @@ class YandexDataSource( requestProperties.remove(name) } + override fun getResponseCode(): Int { + return if (connection == null || this.responseCode <= 0) { + -1 + } else { + responseCode + } + } + override fun clearAllRequestProperties() { requestProperties.clear() } @@ -98,7 +107,6 @@ class YandexDataSource( dataSpec, HttpDataSource.HttpDataSourceException.TYPE_OPEN) } - val responseCode: Int val responseMessage: String try { responseCode = connection!!.responseCode diff --git a/common/src/main/java/kg/delletenebre/yamus/media/library/CurrentPlaylist.kt b/common/src/main/java/kg/delletenebre/yamus/media/library/CurrentPlaylist.kt index 0aed19f..35e2e6e 100644 --- a/common/src/main/java/kg/delletenebre/yamus/media/library/CurrentPlaylist.kt +++ b/common/src/main/java/kg/delletenebre/yamus/media/library/CurrentPlaylist.kt @@ -1,7 +1,6 @@ package kg.delletenebre.yamus.media.library import android.support.v4.media.MediaMetadataCompat -import android.util.Log import androidx.lifecycle.MutableLiveData import com.google.android.exoplayer2.ExoPlayer import com.google.android.exoplayer2.source.ConcatenatingMediaSource @@ -42,8 +41,6 @@ object CurrentPlaylist { private val httpDataSourceFactory = YandexDataSourceFactory(YAMUS_USER_AGENT) init { - httpDataSourceFactory.defaultRequestProperties - .set("X-Yandex-Music-Client", YAMUS_HEADER_X_YANDEX_MUSIC_CLIENT) } fun updatePlaylist( @@ -76,7 +73,6 @@ object CurrentPlaylist { fun updateTrack(track: MediaMetadataCompat) { val index = tracks.indexOfFirst { it.id == track.id } if (index > -1) { - Log.d("ahoha", "updateTrack: ${track.mediaUri}") tracks[index] = track mediaSource.removeMediaSource(index) mediaSource.addMediaSource(index, track.toMediaSource()) diff --git a/common/src/main/java/kg/delletenebre/yamus/media/library/MediaLibrary.kt b/common/src/main/java/kg/delletenebre/yamus/media/library/MediaLibrary.kt index 6e6f861..9a1d210 100644 --- a/common/src/main/java/kg/delletenebre/yamus/media/library/MediaLibrary.kt +++ b/common/src/main/java/kg/delletenebre/yamus/media/library/MediaLibrary.kt @@ -156,8 +156,9 @@ object MediaLibrary { suspend fun getMixes(path: String = ""): List { return if (path.isEmpty() || path == PATH_RECOMMENDED_MIXES) { YaApi.getMixes().map { + val scheme = it.urlScheme.toUri() createBrowsableMediaItem( - id = "${PATH_RECOMMENDED_MIXES}${it.url}", + id = "${PATH_RECOMMENDED_MIXES}/${scheme.host}${scheme.path}", title = it.title, icon = it.backgroundImageUri.toCoverUri(200) )