Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions android/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,25 @@ android {
checkReleaseBuilds = false
}

testOptions {
managedDevices {
localDevices {
create("pixel6Api34Atd") {
device = "Pixel 6"
apiLevel = 34
systemImageSource = "aosp-atd"
}
}
groups {
create("smoke") {
targetDevices.add(devices["pixel6Api34Atd"])
}
}
}
execution = "ANDROIDX_TEST_ORCHESTRATOR"
animationsDisabled = true
}

buildFeatures {
compose = true
}
Expand Down Expand Up @@ -267,6 +286,12 @@ android {
androidTestImplementation(libs.androidx.rules)
androidTestImplementation(libs.androidx.core.ktx)
androidTestImplementation(libs.hamcrest.library)
androidTestImplementation(libs.androidx.espresso.core)
androidTestImplementation(libs.androidx.espresso.contrib)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.ui.test.junit4)
debugImplementation(libs.androidx.ui.test.manifest)
androidTestUtil("androidx.test:orchestrator:1.5.1")

// Remote config
implementation(project(":android:remote-config"))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.simplecityapps.shuttle.di

import com.simplecityapps.shuttle.appinitializers.AppInitializer
import dagger.Module
import dagger.Provides
import dagger.hilt.components.SingletonComponent
import dagger.hilt.testing.TestInstallIn
import dagger.multibindings.ElementsIntoSet

@Module
@TestInstallIn(
components = [SingletonComponent::class],
replaces = [AppModuleBinds::class]
)
class TestAppModuleBinds {
@Provides
@ElementsIntoSet
fun provideEmptyInitializers(): Set<AppInitializer> = emptySet()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.simplecityapps.shuttle.di

import android.content.Context
import au.com.simplecityapps.shuttle.imageloading.ArtworkImageLoader
import com.simplecityapps.mediaprovider.AggregateMediaInfoProvider
import com.simplecityapps.mediaprovider.repository.songs.SongRepository
import com.simplecityapps.playback.PlaybackManager
import com.simplecityapps.playback.chromecast.CastService
import com.simplecityapps.playback.chromecast.CastSessionManager
import com.simplecityapps.playback.chromecast.HttpServer
import com.simplecityapps.playback.di.CastModule
import com.simplecityapps.playback.exoplayer.ExoPlayerPlayback
import dagger.Module
import dagger.Provides
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import dagger.hilt.testing.TestInstallIn
import javax.inject.Singleton

@Module
@TestInstallIn(
components = [SingletonComponent::class],
replaces = [CastModule::class]
)
class TestCastModule {

@Singleton
@Provides
fun provideCastService(
@ApplicationContext context: Context,
songRepository: SongRepository,
artworkImageLoader: ArtworkImageLoader
): CastService = CastService(context, songRepository, artworkImageLoader)

@Singleton
@Provides
fun provideHttpServer(castService: CastService): HttpServer = HttpServer(castService)

@Singleton
@Provides
fun provideCastSessionManager(
@ApplicationContext context: Context,
playbackManager: PlaybackManager,
httpServer: HttpServer,
exoPlayerPlayback: ExoPlayerPlayback,
mediaInfoProvider: AggregateMediaInfoProvider
): CastSessionManager = CastSessionManager(playbackManager, context, httpServer, exoPlayerPlayback, mediaInfoProvider)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.simplecityapps.shuttle.di

import android.content.Context
import androidx.room.Room
import com.simplecityapps.localmediaprovider.local.data.room.database.MediaDatabase
import dagger.Module
import dagger.Provides
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import dagger.hilt.testing.TestInstallIn
import javax.inject.Singleton

@Module
@TestInstallIn(
components = [SingletonComponent::class],
replaces = [DatabaseModule::class]
)
class TestDatabaseModule {
@Provides
@Singleton
fun provideMediaDatabase(
@ApplicationContext context: Context
): MediaDatabase = Room.inMemoryDatabaseBuilder(context, MediaDatabase::class.java)
.allowMainThreadQueries()
.build()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package com.simplecityapps.shuttle.di

import android.content.Context
import android.media.AudioManager
import com.simplecityapps.mediaprovider.AggregateMediaInfoProvider
import com.simplecityapps.playback.AudioEffectSessionManager
import com.simplecityapps.playback.Playback
import com.simplecityapps.playback.PlaybackManager
import com.simplecityapps.playback.PlaybackWatcher
import com.simplecityapps.playback.audiofocus.AudioFocusHelper
import com.simplecityapps.playback.di.PlaybackEngineModule
import com.simplecityapps.playback.dsp.replaygain.ReplayGainAudioProcessor
import com.simplecityapps.playback.dsp.replaygain.ReplayGainMode
import com.simplecityapps.playback.exoplayer.EqualizerAudioProcessor
import com.simplecityapps.playback.exoplayer.ExoPlayerPlayback
import com.simplecityapps.playback.persistence.PlaybackPreferenceManager
import com.simplecityapps.playback.queue.QueueManager
import com.simplecityapps.playback.queue.QueueWatcher
import com.simplecityapps.shuttle.fake.FakeAudioFocusHelper
import com.simplecityapps.shuttle.fake.FakePlayback
import dagger.Module
import dagger.Provides
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import dagger.hilt.testing.TestInstallIn
import javax.inject.Singleton
import kotlinx.coroutines.CoroutineScope

@Module
@TestInstallIn(
components = [SingletonComponent::class],
replaces = [PlaybackEngineModule::class]
)
class TestPlaybackEngineModule {

@Singleton
@Provides
fun providePlayback(): Playback = FakePlayback()

@Singleton
@Provides
fun provideAudioFocusHelper(): AudioFocusHelper = FakeAudioFocusHelper()

@Singleton
@Provides
fun provideEqualizerAudioProcessor(): EqualizerAudioProcessor =
EqualizerAudioProcessor(false)

@Singleton
@Provides
fun provideReplayGainAudioProcessor(): ReplayGainAudioProcessor =
ReplayGainAudioProcessor(ReplayGainMode.Off, 0.0)

@Singleton
@Provides
fun provideAggregateMediaInfoProvider(): AggregateMediaInfoProvider =
AggregateMediaInfoProvider(mutableSetOf())

@Provides
fun provideExoPlayerPlayback(
@ApplicationContext context: Context,
equalizerAudioProcessor: EqualizerAudioProcessor,
replayGainAudioProcessor: ReplayGainAudioProcessor,
mediaInfoProvider: AggregateMediaInfoProvider
): ExoPlayerPlayback = ExoPlayerPlayback(context, equalizerAudioProcessor, replayGainAudioProcessor, mediaInfoProvider)

@Singleton
@Provides
fun providePlaybackManager(
queueManager: QueueManager,
playback: Playback,
playbackWatcher: PlaybackWatcher,
audioFocusHelper: AudioFocusHelper,
playbackPreferenceManager: PlaybackPreferenceManager,
audioEffectSessionManager: AudioEffectSessionManager,
@AppCoroutineScope coroutineScope: CoroutineScope,
queueWatcher: QueueWatcher,
audioManager: AudioManager?
): PlaybackManager = PlaybackManager(
queueManager,
playbackWatcher,
audioFocusHelper,
playbackPreferenceManager,
audioEffectSessionManager,
coroutineScope,
playback,
queueWatcher,
audioManager
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.simplecityapps.shuttle.fake

import com.simplecityapps.playback.audiofocus.AudioFocusHelper

class FakeAudioFocusHelper : AudioFocusHelper {
override var listener: AudioFocusHelper.Listener? = null
override var enabled: Boolean = true
override var resumeOnFocusGain: Boolean = true

override fun requestAudioFocus(): Boolean = true
override fun abandonAudioFocus() {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package com.simplecityapps.shuttle.fake

import com.simplecityapps.playback.Playback
import com.simplecityapps.playback.PlaybackState
import com.simplecityapps.playback.queue.QueueManager
import com.simplecityapps.shuttle.model.Song

class FakePlayback : Playback {
override var callback: Playback.Callback? = null
override var isReleased: Boolean = false

private var state: PlaybackState = PlaybackState.Paused
private var progress: Int = 0
private var duration: Int = 0
private var volume: Float = 1.0f
private var speed: Float = 1.0f
private var repeatMode: QueueManager.RepeatMode = QueueManager.RepeatMode.Off

override suspend fun load(
current: Song,
next: Song?,
seekPosition: Int,
completion: (Result<Any?>) -> Unit
) {
isReleased = false
progress = seekPosition
duration = current.duration
state = PlaybackState.Loading
callback?.onPlaybackStateChanged(state)
completion(Result.success(null))
}

override suspend fun loadNext(song: Song?) {}

override fun play() {
state = PlaybackState.Playing
callback?.onPlaybackStateChanged(state)
}

override fun pause() {
state = PlaybackState.Paused
callback?.onPlaybackStateChanged(state)
}

override fun release() {
isReleased = true
state = PlaybackState.Paused
}

override fun playBackState(): PlaybackState = state
override fun seek(position: Int) { progress = position }
override fun getProgress(): Int = progress
override fun getDuration(): Int = duration
override fun setVolume(volume: Float) { this.volume = volume }
override fun setRepeatMode(repeatMode: QueueManager.RepeatMode) { this.repeatMode = repeatMode }
override fun setPlaybackSpeed(multiplier: Float) { speed = multiplier }
override fun getPlaybackSpeed(): Float = speed
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.simplecityapps.shuttle.smoke

import android.view.View
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import org.hamcrest.Matcher

/**
* Waits for a view matching [viewMatcher] to become displayed, polling every 100ms.
* Returns as soon as the view is found. Throws after [timeoutMs] if not found.
*/
fun waitForView(viewMatcher: Matcher<View>, timeoutMs: Long = 3000) {
val end = System.currentTimeMillis() + timeoutMs
var lastError: Throwable? = null
while (System.currentTimeMillis() < end) {
try {
onView(viewMatcher).check(matches(isDisplayed()))
return
} catch (e: Throwable) {
lastError = e
Thread.sleep(100)
}
}
throw lastError ?: AssertionError("Timed out waiting for view: $viewMatcher")
}
Loading
Loading