Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix #386: Add support for local caching of audio & image assets #399

Merged
merged 14 commits into from
Nov 20, 2019
Merged
Show file tree
Hide file tree
Changes from 8 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
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import org.oppia.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputMo
import org.oppia.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule
import org.oppia.domain.classify.rules.numericinput.NumericInputRuleModule
import org.oppia.domain.classify.rules.textinput.TextInputRuleModule
import org.oppia.util.caching.CachingModule
import org.oppia.util.gcsresource.GcsResourceModule
import org.oppia.util.logging.LoggerModule
import org.oppia.util.parser.GlideImageLoaderModule
Expand All @@ -29,7 +30,7 @@ import javax.inject.Singleton
ContinueModule::class, FractionInputModule::class, ItemSelectionInputModule::class, MultipleChoiceInputModule::class,
NumberWithUnitsRuleModule::class, NumericInputRuleModule::class, TextInputRuleModule::class,
InteractionsModule::class, GcsResourceModule::class, GlideImageLoaderModule::class, ImageParsingModule::class,
HtmlParserEntityTypeModule::class
HtmlParserEntityTypeModule::class, CachingModule::class
])
interface ApplicationComponent {
@Component.Builder
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ class TopicSummaryViewModel(
private val topicSummaryClickListener: TopicSummaryClickListener
) : HomeItemViewModel() {
val name: String = topicSummary.name
val canonicalStoryCount: Int = topicSummary.canonicalStoryCount
val totalChapterCount: Int = topicSummary.totalChapterCount
@ColorInt
val backgroundColor: Int = retrieveBackgroundColor()
@ColorInt
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,21 @@ import org.oppia.app.R
import org.oppia.app.fragment.FragmentScope
import org.oppia.app.model.Topic
import org.oppia.app.viewmodel.ObservableViewModel
import java.text.DecimalFormat
import javax.inject.Inject

/** [ViewModel] for showing topic overview details. */
@FragmentScope
class TopicOverviewViewModel @Inject constructor() : ObservableViewModel() {
private val decimalFormat: DecimalFormat = DecimalFormat("#.###")

val topic = ObservableField<Topic>(Topic.getDefaultInstance())

var downloadStatusIndicatorDrawableResourceId = ObservableField<Int>(R.drawable.ic_available_offline_primary_24dp)
var downloadStatusIndicatorDrawableResourceId = ObservableField(R.drawable.ic_available_offline_primary_24dp)

/** Returns the number of megabytes of disk space this topic requires, formatted for display. */
fun getTopicSizeMb(): String {
val topicSizeMb: Double = (topic.get()?.diskSizeBytes ?: 0) / (1024.0 * 1024.0)
return decimalFormat.format(topicSizeMb)
}
}
2 changes: 1 addition & 1 deletion app/src/main/res/layout/topic_overview_fragment.xml
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@
android:layout_marginTop="12dp"
android:fontFamily="sans-serif"
android:gravity="top"
android:text="@{String.format(@string/topic_download_text, viewModel.topic.getSerializedSize())}"
android:text="@{String.format(@string/topic_download_text, viewModel.getTopicSizeMb())}"
android:textColor="@color/oppiaPrimaryText"
android:textSize="18sp"
android:textStyle="italic"
Expand Down
2 changes: 1 addition & 1 deletion app/src/main/res/layout/topic_summary_view.xml
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fontFamily="sans-serif-light"
android:text="@{@plurals/lesson_count(viewModel.canonicalStoryCount, viewModel.canonicalStoryCount)}"
android:text="@{@plurals/lesson_count(viewModel.totalChapterCount, viewModel.totalChapterCount)}"
android:textColor="@color/white_80"
android:textSize="14sp"
android:textStyle="italic"
Expand Down
1 change: 1 addition & 0 deletions domain/src/main/assets/about_oppia.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"exploration_id": "1",
"author_notes": "",
"blurb": "",
"category": "Welcome",
Expand Down
1 change: 1 addition & 0 deletions domain/src/main/assets/oppia_exploration.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"exploration_id": "3",
"author_notes": "",
"blurb": "",
"category": "Welcome",
Expand Down
1 change: 1 addition & 0 deletions domain/src/main/assets/prototype_exploration.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"exploration_id": "2",
"language_code": "en",
"param_specs": {},
"param_changes": [],
Expand Down
1 change: 1 addition & 0 deletions domain/src/main/assets/welcome.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"exploration_id": "0",
"author_notes": "",
"blurb": "",
"category": "Welcome",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package org.oppia.domain.audio

import android.media.MediaDataSource
import android.media.MediaPlayer
import android.os.Build
import androidx.annotation.VisibleForTesting
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
Expand All @@ -9,6 +11,8 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.oppia.util.caching.AssetRepository
import org.oppia.util.caching.CacheAssetsLocally
import org.oppia.util.data.AsyncResult
import org.oppia.util.logging.Logger
import org.oppia.util.threading.BackgroundDispatcher
Expand All @@ -28,7 +32,9 @@ import kotlin.concurrent.withLock
@Singleton
class AudioPlayerController @Inject constructor(
private val logger: Logger,
@BackgroundDispatcher private val backgroundDispatcher: CoroutineDispatcher
private val assetRepository: AssetRepository,
@BackgroundDispatcher private val backgroundDispatcher: CoroutineDispatcher,
@CacheAssetsLocally private val cacheAssetsLocally: Boolean
) {

inner class AudioMutableLiveData : MutableLiveData<AsyncResult<PlayProgress>>() {
Expand Down Expand Up @@ -125,7 +131,37 @@ class AudioPlayerController @Inject constructor(

private fun prepareDataSource(url: String) {
try {
mediaPlayer.setDataSource(url)
if (cacheAssetsLocally && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val mediaDataSource: MediaDataSource = object : MediaDataSource() {
private val audioFileBuffer: ByteArray by lazy {
// Ensure that the download occurs off the main thread to avoid strict mode violations for
// cases when we need to stream audio.
assetRepository.loadRemoteBinaryAsset(url)()
}

// https://medium.com/@jacks205/implementing-your-own-android-mediadatasource-e67adb070731.
override fun readAt(position: Long, buffer: ByteArray?, offset: Int, size: Int): Int {
checkNotNull(buffer)
val intPosition = position.toInt()
if (intPosition >= audioFileBuffer.size) {
return -1
}
val availableData = audioFileBuffer.size - intPosition
val adjustedSize = size.coerceIn(0 until availableData)
audioFileBuffer.copyInto(buffer, offset, intPosition, intPosition + adjustedSize)
return adjustedSize
}

override fun getSize(): Long {
return audioFileBuffer.size.toLong()
}

override fun close() {}
}
mediaPlayer.setDataSource(mediaDataSource)
} else {
mediaPlayer.setDataSource(url)
}
mediaPlayer.prepareAsync()
} catch (e: IOException) {
logger.e("AudioPlayerController", "Failed to set data source for media player", e)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@ import org.oppia.domain.util.StateRetriever
import java.io.IOException
import javax.inject.Inject

const val TEST_EXPLORATION_ID_5 = "DIWZiVgs0km-"
const val TEST_EXPLORATION_ID_6 = "test_exp_id_6"
const val TEST_EXPLORATION_ID_30 = "30"
const val TEST_EXPLORATION_ID_7 = "test_exp_id_7"
const val TEST_EXPLORATION_ID_5 = "0"
const val TEST_EXPLORATION_ID_6 = "1"
const val TEST_EXPLORATION_ID_30 = "2"
BenHenning marked this conversation as resolved.
Show resolved Hide resolved
const val TEST_EXPLORATION_ID_7 = "3"

// TODO(#59): Make this class inaccessible outside of the domain package except for tests. UI code should not be allowed
// to depend on this utility.
Expand All @@ -27,9 +27,9 @@ class ExplorationRetriever @Inject constructor(
private val jsonAssetRetriever: JsonAssetRetriever,
private val stateRetriever: StateRetriever
) {
// TODO(#169): Force callers of this method on a background thread.
/** Loads and returns an exploration for the specified exploration ID, or fails. */
@Suppress("RedundantSuspendModifier") // Force callers to call this on a background thread.
internal suspend fun loadExploration(explorationId: String): Exploration {
internal fun loadExploration(explorationId: String): Exploration {
return when (explorationId) {
TEST_EXPLORATION_ID_5 -> loadExplorationFromAsset("welcome.json")
TEST_EXPLORATION_ID_6 -> loadExplorationFromAsset("about_oppia.json")
Expand All @@ -50,6 +50,7 @@ class ExplorationRetriever @Inject constructor(
try {
val explorationObject = jsonAssetRetriever.loadJsonFromAsset(assetName) ?: return Exploration.getDefaultInstance()
return Exploration.newBuilder()
.setId(explorationObject.getString("exploration_id"))
.setTitle(explorationObject.getString("title"))
.setLanguageCode(explorationObject.getString("language_code"))
.setInitStateName(explorationObject.getString("init_state_name"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ const val RATIOS_EXPLORATION_ID_0 = "2mzzFVDLuAj8"
const val RATIOS_EXPLORATION_ID_1 = "5NWuolNcwH6e"
const val RATIOS_EXPLORATION_ID_2 = "k2bQ7z5XHNbK"
const val RATIOS_EXPLORATION_ID_3 = "tIoSb3HZFN6e"
private val FRACTIONS_COMPLETED_CHAPTERS = listOf(FRACTIONS_EXPLORATION_ID_0)
private val RATIOS_COMPLETED_CHAPTERS = listOf<String>()
val COMPLETED_EXPLORATIONS = FRACTIONS_COMPLETED_CHAPTERS + RATIOS_COMPLETED_CHAPTERS

/** Controller that records and provides completion statuses of chapters within the context of a story. */
@Singleton
Expand Down Expand Up @@ -68,6 +71,11 @@ class StoryProgressController @Inject constructor(
}
}

// TODO(#21): Hide this functionality behind a data provider rather than punching a hole in this controller.
internal fun retrieveStoryProgress(storyId: String): StoryProgress {
return createStoryProgressSnapshot(storyId)
}

private fun trackCompletedChapter(storyId: String, explorationId: String) {
check(storyId in trackedStoriesProgress) { "No story found with ID: $storyId" }
trackedStoriesProgress.getValue(storyId).markChapterCompleted(explorationId)
Expand All @@ -91,19 +99,13 @@ class StoryProgressController @Inject constructor(

private fun createStoryProgressForJsonStory(fileName: String, index: Int): TrackedStoryProgress {
val storyData = jsonAssetRetriever.loadJsonFromAsset(fileName)?.getJSONArray("story_list")!!
if (storyData.length() < index) {
return TrackedStoryProgress(
chapterList = listOf(),
completedChapters = setOf()
)
}
val explorationIdList = getExplorationIdsFromStory(
storyData.getJSONObject(index).getJSONObject("story")
.getJSONObject("story_contents").getJSONArray("nodes")
)
return TrackedStoryProgress(
chapterList = explorationIdList,
completedChapters = setOf()
completedChapters = COMPLETED_EXPLORATIONS.filter(explorationIdList::contains).toSet()
)
}

Expand Down
Loading