Revision control
Copy as Markdown
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
package org.mozilla.tv.firefox.channels
import android.app.Application
import android.content.Context
import android.content.SharedPreferences
import androidx.annotation.VisibleForTesting
import io.reactivex.Observable
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.rxkotlin.Observables
import io.reactivex.schedulers.Schedulers
import io.reactivex.subjects.BehaviorSubject
import org.mozilla.tv.firefox.channels.content.ChannelContent
import org.mozilla.tv.firefox.channels.content.getMusicChannels
import org.mozilla.tv.firefox.channels.content.getNewsChannels
import org.mozilla.tv.firefox.channels.content.getSportsChannels
import org.mozilla.tv.firefox.channels.pinnedtile.PinnedTileImageUtilWrapper
import org.mozilla.tv.firefox.channels.pinnedtile.PinnedTileRepo
import org.mozilla.tv.firefox.telemetry.TelemetryIntegration
import org.mozilla.tv.firefox.utils.FormattedDomainWrapper
import java.util.Collections
private const val PREF_CHANNEL_REPO = "ChannelRepo"
// BlackList Ids
private const val BUNDLED_PINNED_SITES_ID_BLACKLIST = "blacklist_pinned_tiles"
private const val BUNDLED_NEWS_ID_BLACKLIST = "blacklist_news"
private const val BUNDLED_SPORTS_ID_BLACKLIST = "blacklist_sports"
private const val BUNDLED_MUSIC_ID_BLACKLIST = "blacklist_music"
/**
* ChannelRepo abstracts app logic that requires exposures to other repos (e.g. removing a pinned
* tile channel would require a reference to [PinnedTileRepo].
*
* [TileSource] is used to determine which Repo is responsible to handle requested operations
*/
class ChannelRepo(
application: Application,
imageUtilityWrapper: PinnedTileImageUtilWrapper,
formattedDomainWrapper: FormattedDomainWrapper,
private val pinnedTileRepo: PinnedTileRepo
) {
private val _sharedPreferences: SharedPreferences =
application.getSharedPreferences(PREF_CHANNEL_REPO, Context.MODE_PRIVATE)
fun getPinnedTiles(): Observable<List<ChannelTile>> =
pinnedTiles.filterNotBlacklisted(blacklistedPinnedIds)
fun getNewsTiles(): Observable<List<ChannelTile>> =
bundledNewsTiles.filterNotBlacklisted(blacklistedNewsIds)
fun getSportsTiles(): Observable<List<ChannelTile>> =
bundledSportsTiles.filterNotBlacklisted(blacklistedSportsIds)
fun getMusicTiles(): Observable<List<ChannelTile>> =
bundledMusicTiles.filterNotBlacklisted(blacklistedMusicIds)
fun removeChannelContent(tileData: ChannelTile) {
when (tileData.tileSource) {
TileSource.CUSTOM -> {
TelemetryIntegration.INSTANCE.homeTileRemovedEvent(tileData)
pinnedTileRepo.removePinnedTile(tileData.url)
}
TileSource.BUNDLED -> {
TelemetryIntegration.INSTANCE.homeTileRemovedEvent(tileData) // TODO: verify if we need news, sports and music tiles tracked
addBundleTileToBlackList(tileData.tileSource, tileData.id)
pinnedTileRepo.removePinnedTile(tileData.url)
}
TileSource.NEWS, TileSource.SPORTS, TileSource.MUSIC -> {
addBundleTileToBlackList(tileData.tileSource, tileData.id)
}
}
}
/**
* Used to handle removing bundle tiles by adding to its [BundleType] blacklist in Sha¬redPreferences
*/
private fun addBundleTileToBlackList(source: TileSource, id: String) {
val blackList = loadBlackList(source).toMutableSet()
blackList.add(id)
when (source) {
TileSource.BUNDLED -> blacklistedPinnedIds.onNext(blackList)
TileSource.NEWS -> blacklistedNewsIds.onNext(blackList)
TileSource.SPORTS -> blacklistedSportsIds.onNext(blackList)
TileSource.MUSIC -> blacklistedMusicIds.onNext(blackList)
else -> Unit
}
saveBlackList(source, blackList)
}
private fun loadBlackList(source: TileSource): Set<String> {
val sharedPrefKey = when (source) {
TileSource.BUNDLED -> BUNDLED_PINNED_SITES_ID_BLACKLIST
TileSource.NEWS -> BUNDLED_NEWS_ID_BLACKLIST
TileSource.SPORTS -> BUNDLED_SPORTS_ID_BLACKLIST
TileSource.MUSIC -> BUNDLED_MUSIC_ID_BLACKLIST
else -> throw NotImplementedError("other types shouldn't be able remove tiles")
}
return _sharedPreferences.getStringSet(sharedPrefKey, Collections.emptySet())!!
}
private fun saveBlackList(source: TileSource, blackList: Set<String>) {
val sharedPrefKey = when (source) {
TileSource.BUNDLED -> BUNDLED_PINNED_SITES_ID_BLACKLIST
TileSource.NEWS -> BUNDLED_NEWS_ID_BLACKLIST
TileSource.SPORTS -> BUNDLED_SPORTS_ID_BLACKLIST
TileSource.MUSIC -> BUNDLED_MUSIC_ID_BLACKLIST
else -> throw NotImplementedError("other types shouldn't be able remove tiles")
}
_sharedPreferences.edit().putStringSet(sharedPrefKey, blackList.toSet()).apply()
}
private val pinnedTiles = pinnedTileRepo.pinnedTiles
// This takes place off of the main thread because PinnedTile.toChannelTile needs
// to perform file access, and blocks to do so
.observeOn(Schedulers.io())
.map { it.values.map { it.toChannelTile(imageUtilityWrapper, formattedDomainWrapper) } }
.observeOn(AndroidSchedulers.mainThread())
private val blacklistedPinnedIds = BehaviorSubject.createDefault(loadBlackList(TileSource.BUNDLED))
private val bundledNewsTiles = Observable.just(ChannelContent.getNewsChannels())
.replay(1)
.autoConnect(0)
private val blacklistedNewsIds = BehaviorSubject.createDefault(loadBlackList(TileSource.NEWS))
private val bundledSportsTiles = Observable.just(ChannelContent.getSportsChannels())
.replay(1)
.autoConnect(0)
private val blacklistedSportsIds = BehaviorSubject.createDefault(loadBlackList(TileSource.SPORTS))
private val bundledMusicTiles = Observable.just(ChannelContent.getMusicChannels())
.replay(1)
.autoConnect(0)
private val blacklistedMusicIds = BehaviorSubject.createDefault(loadBlackList(TileSource.MUSIC))
}
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
fun Observable<List<ChannelTile>>.filterNotBlacklisted(
blacklistIds: Observable<Set<String>>
): Observable<List<ChannelTile>> {
return Observables.combineLatest(this, blacklistIds)
.map { (tiles, blacklistIds) -> tiles.filter { !blacklistIds.contains(it.id) } }
}