diff --git a/app/src/androidTest/java/com/github/ashutoshgngwr/noice/EspressoX.kt b/app/src/androidTest/java/com/github/ashutoshgngwr/noice/EspressoX.kt index aaeed84c2..b468ca57c 100644 --- a/app/src/androidTest/java/com/github/ashutoshgngwr/noice/EspressoX.kt +++ b/app/src/androidTest/java/com/github/ashutoshgngwr/noice/EspressoX.kt @@ -1,7 +1,6 @@ package com.github.ashutoshgngwr.noice import android.view.View -import android.widget.SeekBar import androidx.annotation.IdRes import androidx.annotation.StringRes import androidx.test.espresso.Espresso.onView @@ -13,6 +12,7 @@ import androidx.test.espresso.action.MotionEvents import androidx.test.espresso.matcher.ViewMatchers.* import androidx.test.espresso.util.TreeIterables import com.github.ashutoshgngwr.noice.widget.DurationPicker +import com.google.android.material.slider.Slider import com.google.android.material.textfield.TextInputLayout import org.hamcrest.Description import org.hamcrest.Matcher @@ -26,9 +26,10 @@ import org.hamcrest.TypeSafeMatcher object EspressoX { /** - * [clickOn] performs a click action on item with the given [viewId]. + * [clickInItem] performs a click action on item with the given [viewId] inside currently + * matched view. */ - fun clickOn(@IdRes viewId: Int): ViewAction { + fun clickInItem(@IdRes viewId: Int): ViewAction { return object : ViewAction { override fun getDescription() = "Click on view with specified id" override fun getConstraints() = hasDescendant(withId(viewId)) @@ -40,28 +41,28 @@ object EspressoX { } /** - * [seekProgress] performs a seek action on a [SeekBar] with given [seekBarId] inside currently + * [slideInItem] performs a slide action on a [Slider] with given [sliderID] inside currently * matched view. */ - fun seekProgress(@IdRes seekBarId: Int, progress: Int): ViewAction { + fun slideInItem(@IdRes sliderID: Int, value: Float): ViewAction { return object : ViewAction { override fun getDescription() = "Emulate user input on a seek bar" override fun getConstraints() = - hasDescendant(allOf(withId(seekBarId), instanceOf(SeekBar::class.java))) + hasDescendant(allOf(withId(sliderID), instanceOf(Slider::class.java))) override fun perform(uiController: UiController, view: View) { - val seekBar = view.findViewById(seekBarId) - val width = seekBar.width - seekBar.paddingStart - seekBar.paddingEnd - val height = seekBar.height - seekBar.paddingTop - seekBar.paddingBottom + val slider = view.findViewById(sliderID) + val height = slider.height - slider.paddingTop - slider.paddingBottom val location = intArrayOf(0, 0) - seekBar.getLocationOnScreen(location) + slider.getLocationOnScreen(location) - val xOffset = location[0].toFloat() + seekBar.paddingStart - val xStart = ((seekBar.progress.toFloat() / seekBar.max) * width) + xOffset + val xOffset = location[0].toFloat() + slider.paddingStart + slider.trackSidePadding + val range = slider.valueTo - slider.valueFrom + val xStart = (((slider.value - slider.valueFrom) / range) * slider.trackWidth) + xOffset - val x = ((progress.toFloat() / seekBar.max) * width) + xOffset - val y = location[1] + seekBar.paddingTop + (height.toFloat() / 2) + val x = (((value - slider.valueFrom) / range) * slider.trackWidth) + xOffset + val y = location[1] + slider.paddingTop + (height.toFloat() / 2) val startCoordinates = floatArrayOf(xStart, y) val endCoordinates = floatArrayOf(x, y) diff --git a/app/src/androidTest/java/com/github/ashutoshgngwr/noice/fragment/PresetFragmentTest.kt b/app/src/androidTest/java/com/github/ashutoshgngwr/noice/fragment/PresetFragmentTest.kt index 5cf09d3ad..0accef0b9 100644 --- a/app/src/androidTest/java/com/github/ashutoshgngwr/noice/fragment/PresetFragmentTest.kt +++ b/app/src/androidTest/java/com/github/ashutoshgngwr/noice/fragment/PresetFragmentTest.kt @@ -87,7 +87,7 @@ class PresetFragmentTest { onView(withId(R.id.list_presets)).perform( RecyclerViewActions.actionOnItem( hasDescendant(allOf(withId(R.id.title), withText("test"))), - EspressoX.clickOn(R.id.button_play) + EspressoX.clickInItem(R.id.button_play) ) ) @@ -109,7 +109,7 @@ class PresetFragmentTest { onView(withId(R.id.list_presets)).perform( RecyclerViewActions.actionOnItem( hasDescendant(allOf(withId(R.id.title), withText("test"))), - EspressoX.clickOn(R.id.button_play) + EspressoX.clickInItem(R.id.button_play) ) ) @@ -122,7 +122,7 @@ class PresetFragmentTest { onView(withId(R.id.list_presets)).perform( RecyclerViewActions.actionOnItem( hasDescendant(allOf(withId(R.id.title), withText("test"))), - EspressoX.clickOn(R.id.button_menu) + EspressoX.clickInItem(R.id.button_menu) ) ) @@ -150,7 +150,7 @@ class PresetFragmentTest { onView(withId(R.id.list_presets)).perform( RecyclerViewActions.actionOnItem( hasDescendant(allOf(withId(R.id.title), withText("test"))), - EspressoX.clickOn(R.id.button_menu) + EspressoX.clickInItem(R.id.button_menu) ) ) @@ -174,7 +174,7 @@ class PresetFragmentTest { onView(withId(R.id.list_presets)).perform( RecyclerViewActions.actionOnItem( hasDescendant(allOf(withId(R.id.title), withText("test"))), - EspressoX.clickOn(R.id.button_menu) + EspressoX.clickInItem(R.id.button_menu) ) ) diff --git a/app/src/androidTest/java/com/github/ashutoshgngwr/noice/fragment/SoundLibraryFragmentTest.kt b/app/src/androidTest/java/com/github/ashutoshgngwr/noice/fragment/SoundLibraryFragmentTest.kt index be23f63fd..24873fc05 100644 --- a/app/src/androidTest/java/com/github/ashutoshgngwr/noice/fragment/SoundLibraryFragmentTest.kt +++ b/app/src/androidTest/java/com/github/ashutoshgngwr/noice/fragment/SoundLibraryFragmentTest.kt @@ -55,7 +55,7 @@ class SoundLibraryFragmentTest { onView(withId(R.id.list_sound)).perform( RecyclerViewActions.actionOnItem( hasDescendant(allOf(withId(R.id.title), withText(R.string.birds))), - EspressoX.clickOn(R.id.button_play) + EspressoX.clickInItem(R.id.button_play) ) ) @@ -73,7 +73,7 @@ class SoundLibraryFragmentTest { onView(withId(R.id.list_sound)).perform( RecyclerViewActions.actionOnItem( hasDescendant(allOf(withId(R.id.title), withText(R.string.birds))), - EspressoX.clickOn(R.id.button_play) + EspressoX.clickInItem(R.id.button_play) ) ) @@ -81,7 +81,7 @@ class SoundLibraryFragmentTest { } @Test - fun testRecyclerViewItem_volumeSeekBar() { + fun testRecyclerViewItem_volumeSlider() { val mockPlayer = mockk(relaxed = true) { every { volume } returns Player.DEFAULT_VOLUME } @@ -96,7 +96,7 @@ class SoundLibraryFragmentTest { onView(withId(R.id.list_sound)).perform( RecyclerViewActions.actionOnItem( hasDescendant(allOf(withId(R.id.title), withText(R.string.birds))), - EspressoX.seekProgress(R.id.seekbar_volume, expectedVolume) + EspressoX.slideInItem(R.id.slider_volume, expectedVolume.toFloat()) ) ) @@ -105,7 +105,7 @@ class SoundLibraryFragmentTest { } @Test - fun testRecyclerViewItem_timePeriodSeekBar() { + fun testRecyclerViewItem_timePeriodSlider() { val mockPlayer = mockk(relaxed = true) { every { timePeriod } returns Player.DEFAULT_TIME_PERIOD } @@ -118,7 +118,8 @@ class SoundLibraryFragmentTest { val expectedTimePeriods = arrayOf( Player.MIN_TIME_PERIOD, Player.MAX_TIME_PERIOD, - Random.nextInt(Player.MIN_TIME_PERIOD, Player.MAX_TIME_PERIOD) + // following because step size of the slider is 10s + Random.nextInt(Player.MIN_TIME_PERIOD / 10, Player.MAX_TIME_PERIOD / 10) * 10 ) for (expectedTimePeriod in expectedTimePeriods) { @@ -129,9 +130,7 @@ class SoundLibraryFragmentTest { ), RecyclerViewActions.actionOnItem( hasDescendant(allOf(withId(R.id.title), withText(R.string.rolling_thunder))), - EspressoX.seekProgress( - R.id.seekbar_time_period, expectedTimePeriod - Player.MIN_TIME_PERIOD - ) + EspressoX.slideInItem(R.id.slider_time_period, expectedTimePeriod.toFloat()) ) ) diff --git a/app/src/androidTest/java/com/github/ashutoshgngwr/noice/fragment/WakeUpTimerFragmentTest.kt b/app/src/androidTest/java/com/github/ashutoshgngwr/noice/fragment/WakeUpTimerFragmentTest.kt index 19b805d93..68f990fb5 100644 --- a/app/src/androidTest/java/com/github/ashutoshgngwr/noice/fragment/WakeUpTimerFragmentTest.kt +++ b/app/src/androidTest/java/com/github/ashutoshgngwr/noice/fragment/WakeUpTimerFragmentTest.kt @@ -93,6 +93,17 @@ class WakeUpTimerFragmentTest { .check(matches(not(isEnabled()))) } + @Test + fun testSelectPreset_withoutSavedPresets() { + every { Preset.readAllFromUserPreferences(any()) } returns arrayOf() + onView(withId(R.id.button_select_preset)) + .check(matches(withText(R.string.select_preset))) + .perform(click()) + + EspressoX.waitForView(withText(R.string.preset_info__description), 100, 5) + .check(matches(isDisplayed())) + } + @Test fun testSetTimer() { every { Preset.readAllFromUserPreferences(any()) } returns arrayOf( diff --git a/app/src/androidTestPlaystore/java/com/github/ashutoshgngwr/noice/GenerateScreenshots.kt b/app/src/androidTestPlaystore/java/com/github/ashutoshgngwr/noice/GenerateScreenshots.kt index 3fd76d65b..2232f4825 100644 --- a/app/src/androidTestPlaystore/java/com/github/ashutoshgngwr/noice/GenerateScreenshots.kt +++ b/app/src/androidTestPlaystore/java/com/github/ashutoshgngwr/noice/GenerateScreenshots.kt @@ -154,14 +154,17 @@ class GenerateScreenshots { onView(withId(R.id.list_sound)).perform( actionOnItem( ViewMatchers.hasDescendant(allOf(withId(R.id.title), withText(R.string.airplane_inflight))), - EspressoX.clickOn(R.id.button_play) + EspressoX.clickInItem(R.id.button_play) ) ) onView(withId(R.id.list_sound)).perform( actionOnItem( ViewMatchers.hasDescendant(allOf(withId(R.id.title), withText(R.string.airplane_inflight))), - EspressoX.seekProgress(R.id.seekbar_volume, Player.MAX_VOLUME - Player.DEFAULT_VOLUME) + EspressoX.slideInItem( + R.id.slider_volume, + Player.MAX_VOLUME.toFloat() - Player.DEFAULT_VOLUME + ) ) ) @@ -170,7 +173,7 @@ class GenerateScreenshots { ViewMatchers.hasDescendant( allOf(withId(R.id.title), withText(R.string.airplane_seatbelt_beeps)) ), - EspressoX.clickOn(R.id.button_play) + EspressoX.clickInItem(R.id.button_play) ) ) @@ -179,7 +182,7 @@ class GenerateScreenshots { ViewMatchers.hasDescendant( allOf(withId(R.id.title), withText(R.string.airplane_seatbelt_beeps)) ), - EspressoX.seekProgress(R.id.seekbar_volume, Player.MAX_VOLUME) + EspressoX.slideInItem(R.id.slider_volume, Player.MAX_VOLUME.toFloat()) ) ) @@ -188,9 +191,7 @@ class GenerateScreenshots { ViewMatchers.hasDescendant( allOf(withId(R.id.title), withText(R.string.airplane_seatbelt_beeps)) ), - EspressoX.seekProgress( - R.id.seekbar_time_period, Player.MAX_TIME_PERIOD - Player.MIN_TIME_PERIOD - 30 - ) + EspressoX.slideInItem(R.id.slider_time_period, Player.MAX_TIME_PERIOD.toFloat() - 300) ) ) @@ -210,7 +211,9 @@ class GenerateScreenshots { EspressoX.waitForView(withId(R.id.list_presets), 100, 5) .perform( - actionOnItemAtPosition(1, EspressoX.clickOn(R.id.button_play)) + actionOnItemAtPosition( + 1, EspressoX.clickInItem(R.id.button_play) + ) ) Thread.sleep(SLEEP_PERIOD_BEFORE_SCREENGRAB) @@ -222,7 +225,7 @@ class GenerateScreenshots { onView(withId(R.id.list_sound)).perform( actionOnItem( ViewMatchers.hasDescendant(allOf(withId(R.id.title), withText(R.string.birds))), - EspressoX.clickOn(R.id.button_play) + EspressoX.clickInItem(R.id.button_play) ) ) diff --git a/app/src/main/java/com/github/ashutoshgngwr/noice/MainActivity.kt b/app/src/main/java/com/github/ashutoshgngwr/noice/MainActivity.kt index 6fb1279d7..f868178e2 100644 --- a/app/src/main/java/com/github/ashutoshgngwr/noice/MainActivity.kt +++ b/app/src/main/java/com/github/ashutoshgngwr/noice/MainActivity.kt @@ -35,7 +35,6 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte */ const val EXTRA_CURRENT_NAVIGATED_FRAGMENT = "current_fragment" - private const val TAG = "MainActivity" private const val PREF_APP_THEME = "app_theme" private const val APP_THEME_LIGHT = 0 private const val APP_THEME_DARK = 1 diff --git a/app/src/main/java/com/github/ashutoshgngwr/noice/fragment/SoundLibraryFragment.kt b/app/src/main/java/com/github/ashutoshgngwr/noice/fragment/SoundLibraryFragment.kt index 9fc7655b3..fdab70ddc 100644 --- a/app/src/main/java/com/github/ashutoshgngwr/noice/fragment/SoundLibraryFragment.kt +++ b/app/src/main/java/com/github/ashutoshgngwr/noice/fragment/SoundLibraryFragment.kt @@ -5,7 +5,6 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.SeekBar import android.widget.TextView import androidx.annotation.LayoutRes import androidx.fragment.app.Fragment @@ -17,6 +16,7 @@ import com.github.ashutoshgngwr.noice.sound.Sound import com.github.ashutoshgngwr.noice.sound.player.Player import com.github.ashutoshgngwr.noice.sound.player.PlayerManager import com.google.android.material.floatingactionbutton.FloatingActionButton +import com.google.android.material.slider.Slider import com.google.android.material.snackbar.Snackbar import kotlinx.android.synthetic.main.fragment_sound_list.view.* import kotlinx.android.synthetic.main.layout_list_item__sound.view.* @@ -166,18 +166,17 @@ class SoundLibraryFragment : Fragment(R.layout.fragment_sound_list) { val sound = Sound.get(soundKey) val isPlaying = players.containsKey(soundKey) holder.itemView.title.text = context.getString(sound.titleResId) - holder.itemView.seekbar_volume.isEnabled = isPlaying - holder.itemView.seekbar_time_period.isEnabled = isPlaying + holder.itemView.slider_volume.isEnabled = isPlaying + holder.itemView.slider_time_period.isEnabled = isPlaying holder.itemView.button_play.isChecked = isPlaying if (isPlaying) { requireNotNull(players[soundKey]).also { - holder.itemView.seekbar_volume.progress = it.volume - holder.itemView.seekbar_time_period.progress = it.timePeriod - Player.MIN_TIME_PERIOD + holder.itemView.slider_volume.value = it.volume.toFloat() + holder.itemView.slider_time_period.value = it.timePeriod.toFloat() } } else { - holder.itemView.seekbar_volume.progress = Player.DEFAULT_VOLUME - holder.itemView.seekbar_time_period.progress = - Player.DEFAULT_TIME_PERIOD - Player.MIN_TIME_PERIOD + holder.itemView.slider_volume.value = Player.DEFAULT_VOLUME.toFloat() + holder.itemView.slider_time_period.value = Player.DEFAULT_TIME_PERIOD.toFloat() } holder.itemView.layout_time_period.visibility = if (sound.isLooping) { @@ -191,29 +190,23 @@ class SoundLibraryFragment : Fragment(R.layout.fragment_sound_list) { inner class ViewHolder(view: View, @LayoutRes layoutID: Int) : RecyclerView.ViewHolder(view) { // set listeners in holders to avoid object recreation on view recycle - private val seekBarChangeListener = object : SeekBar.OnSeekBarChangeListener { + private val sliderChangeListener = object : Slider.OnChangeListener { - override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) { + override fun onValueChange(slider: Slider, value: Float, fromUser: Boolean) { if (!fromUser) { return } val player = players[dataSet[adapterPosition].data] ?: return - when (seekBar.id) { - R.id.seekbar_volume -> { - player.setVolume(progress) + when (slider.id) { + R.id.slider_volume -> { + player.setVolume(value.toInt()) } - R.id.seekbar_time_period -> { - player.timePeriod = Player.MIN_TIME_PERIOD + progress + R.id.slider_time_period -> { + player.timePeriod = value.toInt() } } } - - // unsubscribe from events during the seek action or it will cause adapter to refresh during - // the action causing adapterPosition to become -1 (POSITION_NONE). On resubscribing, - // this will also cause an update (#onPlayerManagerUpdate) since those events are sticky. - override fun onStartTrackingTouch(seekBar: SeekBar?) = unregisterFromEventBus() - override fun onStopTrackingTouch(seekBar: SeekBar?) = registerOnEventBus() } init { @@ -223,10 +216,14 @@ class SoundLibraryFragment : Fragment(R.layout.fragment_sound_list) { } private fun initSoundItem(view: View) { - view.seekbar_volume.max = Player.MAX_VOLUME - view.seekbar_volume.setOnSeekBarChangeListener(seekBarChangeListener) - view.seekbar_time_period.max = Player.MAX_TIME_PERIOD - Player.MIN_TIME_PERIOD - view.seekbar_time_period.setOnSeekBarChangeListener(seekBarChangeListener) + view.slider_volume.valueFrom = 0.0f + view.slider_volume.valueTo = Player.MAX_VOLUME.toFloat() + view.slider_volume.addOnChangeListener(sliderChangeListener) + view.slider_volume.setLabelFormatter { "${(it * 100).toInt() / Player.MAX_VOLUME}%" } + view.slider_time_period.valueFrom = Player.MIN_TIME_PERIOD.toFloat() + view.slider_time_period.valueTo = Player.MAX_TIME_PERIOD.toFloat() + view.slider_time_period.addOnChangeListener(sliderChangeListener) + view.slider_time_period.setLabelFormatter { "${it.toInt() / 60}m ${it.toInt() % 60}s" } view.button_play.setOnClickListener { val listItem = dataSet.getOrNull(adapterPosition) ?: return@setOnClickListener if (players.containsKey(listItem.data)) { diff --git a/app/src/main/java/com/github/ashutoshgngwr/noice/fragment/WakeUpTimerFragment.kt b/app/src/main/java/com/github/ashutoshgngwr/noice/fragment/WakeUpTimerFragment.kt index 626d8e266..39ed693e2 100644 --- a/app/src/main/java/com/github/ashutoshgngwr/noice/fragment/WakeUpTimerFragment.kt +++ b/app/src/main/java/com/github/ashutoshgngwr/noice/fragment/WakeUpTimerFragment.kt @@ -41,9 +41,14 @@ class WakeUpTimerFragment : Fragment(R.layout.fragment_wake_up_timer) { DialogFragment().show(childFragmentManager) { val presets = Preset.readAllFromUserPreferences(requireContext()).map { it.name } title(R.string.select_preset) - singleChoiceItems(presets.toTypedArray(), presets.indexOf(selectedPresetName)) { choice -> - selectedPresetName = presets[choice] - notifyUpdate() + if (presets.isNotEmpty()) { + singleChoiceItems(presets.toTypedArray(), presets.indexOf(selectedPresetName)) { choice -> + selectedPresetName = presets[choice] + notifyUpdate() + } + } else { + message(R.string.preset_info__description) + positiveButton(android.R.string.ok) } } } diff --git a/app/src/main/java/com/github/ashutoshgngwr/noice/sound/player/Player.kt b/app/src/main/java/com/github/ashutoshgngwr/noice/sound/player/Player.kt index 30d613c3b..1c1a98031 100644 --- a/app/src/main/java/com/github/ashutoshgngwr/noice/sound/player/Player.kt +++ b/app/src/main/java/com/github/ashutoshgngwr/noice/sound/player/Player.kt @@ -20,9 +20,9 @@ class Player(val soundKey: String, playbackStrategyFactory: PlaybackStrategyFact const val DEFAULT_VOLUME = 4 const val MAX_VOLUME = 20 - const val DEFAULT_TIME_PERIOD = 60 + const val DEFAULT_TIME_PERIOD = 300 const val MIN_TIME_PERIOD = 30 - const val MAX_TIME_PERIOD = 300 + const val MAX_TIME_PERIOD = 1200 } var volume = DEFAULT_VOLUME diff --git a/app/src/main/res/layout/layout_list_item__sound.xml b/app/src/main/res/layout/layout_list_item__sound.xml index c5acde2a7..d789afc30 100644 --- a/app/src/main/res/layout/layout_list_item__sound.xml +++ b/app/src/main/res/layout/layout_list_item__sound.xml @@ -1,5 +1,6 @@ @@ -40,7 +41,6 @@ @@ -49,13 +49,15 @@ android:layout_height="wrap_content" android:contentDescription="@string/volume" android:src="@drawable/ic_action_volume" - android:tint="?attr/colorAccent" /> + app:tint="?attr/colorAccent" /> - + android:layout_weight="1" + android:stepSize="1.0" /> @@ -63,7 +65,6 @@ android:id="@+id/layout_time_period" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginTop="7dp" android:gravity="center_vertical" android:orientation="horizontal" android:visibility="gone"> @@ -73,13 +74,15 @@ android:layout_height="wrap_content" android:contentDescription="@string/repeat_time_period" android:src="@drawable/ic_action_time_period" - android:tint="?attr/colorAccent" /> + app:tint="?attr/colorAccent" /> - + android:layout_weight="1" + android:stepSize="10.0" /> diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index b5e3449e4..5bf068939 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -93,4 +93,10 @@ @style/ThemeOverlay.MediaRouter.Light + + diff --git a/app/src/playstore/java/com/github/ashutoshgngwr/noice/cast/CastOptionsProvider.kt b/app/src/playstore/java/com/github/ashutoshgngwr/noice/cast/CastOptionsProvider.kt index 7c6d15122..2a5bb60dc 100644 --- a/app/src/playstore/java/com/github/ashutoshgngwr/noice/cast/CastOptionsProvider.kt +++ b/app/src/playstore/java/com/github/ashutoshgngwr/noice/cast/CastOptionsProvider.kt @@ -5,6 +5,7 @@ import com.github.ashutoshgngwr.noice.BuildConfig import com.github.ashutoshgngwr.noice.R import com.google.android.gms.cast.framework.CastOptions import com.google.android.gms.cast.framework.OptionsProvider +import com.google.android.gms.cast.framework.SessionProvider import com.google.android.gms.cast.framework.media.CastMediaOptions @Suppress("unused") @@ -34,5 +35,5 @@ class CastOptionsProvider : OptionsProvider { } } - override fun getAdditionalSessionProviders(context: Context?) = null + override fun getAdditionalSessionProviders(context: Context?): List? = null }