Skip to content

Commit

Permalink
Add trending api and update news api
Browse files Browse the repository at this point in the history
  • Loading branch information
premnirmal committed Oct 9, 2022
1 parent 6755190 commit 232d065
Show file tree
Hide file tree
Showing 38 changed files with 743 additions and 406 deletions.
5 changes: 3 additions & 2 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,6 @@ android {
applicationId appIdBase
minSdkVersion 21
targetSdkVersion 33
testInstrumentationRunner "com.github.premnirmal.ticker.mock.MockTestRunner"

versionCode = code
versionName = name
Expand Down Expand Up @@ -204,7 +203,7 @@ ext {
JUNIT_VERSION = "4.13.1"
RETROFIT_VERSION = "2.9.0"
OKHTTP_VERSION = "4.9.0"
ROBOLECTRIC_VERSION = "4.3.1"
ROBOLECTRIC_VERSION = "4.4"
COROUTINES_VERSION = "1.6.0"
ROOM_VERSION = "2.4.2"
WORK_VERSION = "2.7.1"
Expand All @@ -225,6 +224,8 @@ dependencies {
implementation 'androidx.core:core-splashscreen:1.0.0'
implementation "androidx.fragment:fragment-ktx:1.5.3"

implementation "io.coil-kt:coil:2.2.2"

implementation "javax.inject:javax.inject:1"
implementation "javax.annotation:javax.annotation-api:1.3.2"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.github.premnirmal.ticker

import android.content.SharedPreferences
import android.os.Build
import androidx.annotation.VisibleForTesting
import androidx.appcompat.app.AppCompatDelegate
import androidx.appcompat.app.AppCompatDelegate.NightMode
import com.github.premnirmal.ticker.components.AppClock
Expand All @@ -21,7 +22,8 @@ import kotlin.random.Random
*/
@Singleton
class AppPreferences @Inject constructor(
private val sharedPreferences: SharedPreferences,
@VisibleForTesting
internal val sharedPreferences: SharedPreferences,
private val clock: AppClock
) {

Expand Down Expand Up @@ -125,8 +127,6 @@ class AppPreferences @Inject constructor(
fun shouldPromptRate(): Boolean = // if the user hasn't rated, ask them again but not too often.
!sharedPreferences.getBoolean(DID_RATE, false) && (Random.nextInt() % 5 == 0)

fun clock(): AppClock = clock

fun backOffAttemptCount(): Int = sharedPreferences.getInt(BACKOFF_ATTEMPTS, 1)

fun setBackOffAttemptCount(count: Int) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import android.content.Context
import android.content.Context.MODE_PRIVATE
import android.content.SharedPreferences
import androidx.room.Room
import androidx.work.WorkManager
import com.github.premnirmal.ticker.AppPreferences
import com.github.premnirmal.ticker.analytics.Analytics
import com.github.premnirmal.ticker.analytics.AnalyticsImpl
Expand Down Expand Up @@ -45,6 +46,9 @@ class AppModule {
@Provides @Singleton fun provideAppWidgetManager(@ApplicationContext context: Context): AppWidgetManager =
AppWidgetManager.getInstance(context)

@Provides @Singleton fun provideWorkManager(@ApplicationContext context: Context): WorkManager =
WorkManager.getInstance(context)

@Provides @Singleton fun provideAnalytics(
@ApplicationContext context: Context,
properties: dagger.Lazy<GeneralProperties>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ import javax.inject.Inject
@HiltViewModel
class DbViewerViewModel @Inject constructor(
application: Application,
private val dao: QuoteDao
private val dao: QuoteDao,
private val workManager: WorkManager
) : AndroidViewModel(application) {

companion object {
Expand Down Expand Up @@ -207,7 +208,7 @@ class DbViewerViewModel @Inject constructor(
</tr>
"""
)
with(WorkManager.getInstance(context)) {
with(workManager) {
pruneWork()
val workInfos = ArrayList<WorkInfo>().apply {
addAll(getWorkInfosByTag(RefreshWorker.TAG).get())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ import javax.inject.Singleton
@Singleton
class AlarmScheduler @Inject constructor(
private val appPreferences: AppPreferences,
private val clock: AppClock
private val clock: AppClock,
private val workManager: WorkManager
) {

/**
Expand Down Expand Up @@ -137,7 +138,7 @@ class AlarmScheduler @Inject constructor(
.addTag(RefreshWorker.TAG)
.setInitialDelay(msToNextAlarm, MILLISECONDS)
.build()
with(WorkManager.getInstance(context)) {
with(workManager) {
this.cancelAllWorkByTag(RefreshWorker.TAG)
this.enqueue(workRequest)
}
Expand All @@ -157,7 +158,7 @@ class AlarmScheduler @Inject constructor(
context: Context,
force: Boolean = true
) {
with(WorkManager.getInstance(context)) {
with(workManager) {
val enqueuedAlready = try {
getWorkInfosByTag(RefreshWorker.TAG_PERIODIC)
.await()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.github.premnirmal.ticker.network

import com.github.premnirmal.ticker.network.data.TrendingResult
import retrofit2.http.GET

interface ApeWisdom {

@GET("filter/all-stocks")
suspend fun getTrendingStocks(): TrendingResult
}
Original file line number Diff line number Diff line change
Expand Up @@ -80,18 +80,44 @@ class NetworkModule {
return yahooFinance
}

@Provides @Singleton internal fun provideNewsApi(
@Provides @Singleton internal fun provideApeWisdom(
@ApplicationContext context: Context,
okHttpClient: OkHttpClient,
converterFactory: GsonConverterFactory
): ApeWisdom {
val retrofit = Retrofit.Builder()
.client(okHttpClient)
.baseUrl(context.getString(R.string.apewisdom_endpoint))
.addConverterFactory(converterFactory)
.build()
val apewisdom = retrofit.create(ApeWisdom::class.java)
return apewisdom
}

@Provides @Singleton internal fun provideGoogleNewsApi(
@ApplicationContext context: Context,
okHttpClient: OkHttpClient
): GoogleNewsApi {
val retrofit =
Retrofit.Builder()
.client(okHttpClient)
.baseUrl(context.getString(R.string.google_news_endpoint))
.addConverterFactory(SimpleXmlConverterFactory.create())
.build()
return retrofit.create(GoogleNewsApi::class.java)
}

@Provides @Singleton internal fun provideYahooFinanceNewsApi(
@ApplicationContext context: Context,
okHttpClient: OkHttpClient
): NewsApi {
): YahooFinanceNewsApi {
val retrofit =
Retrofit.Builder()
.client(okHttpClient)
.baseUrl(context.getString(R.string.news_endpoint))
.baseUrl(context.getString(R.string.yahoo_news_endpoint))
.addConverterFactory(SimpleXmlConverterFactory.create())
.build()
val newsApi = retrofit.create(NewsApi::class.java)
return newsApi
return retrofit.create(YahooFinanceNewsApi::class.java)
}

@Provides @Singleton internal fun provideHistoricalDataApi(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import com.github.premnirmal.ticker.network.data.NewsRssFeed
import retrofit2.http.GET
import retrofit2.http.Query

interface NewsApi {
interface GoogleNewsApi {

/**
* Retrieves the recent news feed given the query.
Expand All @@ -17,4 +17,9 @@ interface NewsApi {

@GET("news/rss/headlines/section/topic/BUSINESS")
suspend fun getBusinessNews(): NewsRssFeed
}

interface YahooFinanceNewsApi {
@GET("rssindex")
suspend fun getNewsFeed(): NewsRssFeed
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.github.premnirmal.ticker.network
import com.github.premnirmal.ticker.model.FetchException
import com.github.premnirmal.ticker.model.FetchResult
import com.github.premnirmal.ticker.network.data.NewsArticle
import com.github.premnirmal.ticker.network.data.Quote
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
Expand All @@ -14,19 +15,26 @@ import javax.inject.Singleton
@Singleton
class NewsProvider @Inject constructor(
private val coroutineScope: CoroutineScope,
private val newsApi: NewsApi
private val googleNewsApi: GoogleNewsApi,
private val yahooNewsApi: YahooFinanceNewsApi,
private val apeWisdom: ApeWisdom,
private val stocksApi: StocksApi
) {

private var cachedBusinessArticles: List<NewsArticle> = emptyList()
private var cachedTrendingStocks: List<Quote> = emptyList()

fun initCache() {
coroutineScope.launch { fetchMarketNews() }
coroutineScope.launch {
fetchMarketNews()
fetchTrendingStocks()
}
}

suspend fun fetchNewsForQuery(query: String): FetchResult<List<NewsArticle>> =
withContext(Dispatchers.IO) {
try {
val newsFeed = newsApi.getNewsFeed(query = query)
val newsFeed = googleNewsApi.getNewsFeed(query = query)
val articles = newsFeed.articleList?.sorted() ?: emptyList()
return@withContext FetchResult.success(articles)
} catch (ex: Exception) {
Expand All @@ -42,8 +50,8 @@ class NewsProvider @Inject constructor(
if (useCache && cachedBusinessArticles.isNotEmpty()) {
return@withContext FetchResult.success(cachedBusinessArticles)
}
val marketNewsArticles = newsApi.getNewsFeed(query = "stock market").articleList.orEmpty()
val businessNewsArticles = newsApi.getBusinessNews().articleList.orEmpty()
val marketNewsArticles = yahooNewsApi.getNewsFeed().articleList.orEmpty()
val businessNewsArticles = googleNewsApi.getBusinessNews().articleList.orEmpty()
val articles: Set<NewsArticle> = HashSet<NewsArticle>().apply {
addAll(marketNewsArticles)
addAll(businessNewsArticles)
Expand All @@ -57,4 +65,24 @@ class NewsProvider @Inject constructor(
FetchException("Error fetching news", ex))
}
}

suspend fun fetchTrendingStocks(useCache: Boolean = false): FetchResult<List<Quote>> =
withContext(Dispatchers.IO) {
try {
if (useCache && cachedTrendingStocks.isNotEmpty()) {
return@withContext FetchResult.success(cachedTrendingStocks)
}
val result = apeWisdom.getTrendingStocks().results
val data = result.map { it.ticker }
val trendingResult = stocksApi.getStocks(data)
if (trendingResult.wasSuccessful) {
cachedTrendingStocks = trendingResult.data
}
return@withContext trendingResult
} catch (ex: Exception) {
Timber.w(ex)
return@withContext FetchResult.failure<List<Quote>>(
FetchException("Error fetching trending", ex))
}
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
package com.github.premnirmal.ticker.network.data

import android.os.Build.VERSION
import android.os.Build.VERSION_CODES
import android.text.Html
import org.simpleframework.xml.Attribute
import org.simpleframework.xml.Element
import org.simpleframework.xml.Root
import org.threeten.bp.Instant
import org.threeten.bp.LocalDateTime
import org.threeten.bp.ZoneId
import org.threeten.bp.format.DateTimeFormatter
import java.net.URL

Expand All @@ -17,20 +22,31 @@ class NewsArticle : Comparable<NewsArticle> {
@get:Element(name = "link")
@set:Element(name = "link")
var url: String = ""
@get:Element(name = "title")
@set:Element(name = "title")
@get:Element(name = "title", required = false)
@set:Element(name = "title", required = false)
var title: String? = null
@get:Element(name = "description")
@set:Element(name = "description")
@get:Element(name = "description", required = false)
@set:Element(name = "description", required = false)
var description: String? = null
@get:Element(name = "pubDate")
@set:Element(name = "pubDate")
@get:Element(name = "pubDate", required = false)
@set:Element(name = "pubDate", required = false)
var publishedAt: String? = null

@get:Element(name = "content", required = false)
@set:Element(name = "content", required = false)
var thumbnail: Thumbnail? = null

val date: LocalDateTime by lazy {
LocalDateTime.parse(publishedAt, DateTimeFormatter.RFC_1123_DATE_TIME)
try {
LocalDateTime.parse(publishedAt, DateTimeFormatter.RFC_1123_DATE_TIME)
} catch (e: Exception) {
Instant.parse(publishedAt).atZone(ZoneId.systemDefault()).toLocalDateTime()
}
}

val imageUrl: String?
get() = thumbnail?.url

fun dateString(): String = OUTPUT_FORMATTER.format(date)

fun sourceName(): String {
Expand All @@ -40,11 +56,19 @@ class NewsArticle : Comparable<NewsArticle> {
}

fun descriptionSanitized(): String {
return Html.fromHtml(description).toString()
return if (VERSION.SDK_INT >= VERSION_CODES.N) {
Html.fromHtml(description.orEmpty(), Html.FROM_HTML_MODE_COMPACT).toString()
} else {
Html.fromHtml(description.orEmpty()).toString()
}
}

fun titleSanitized(): String {
return Html.fromHtml(title).toString()
return if (VERSION.SDK_INT >= VERSION_CODES.N) {
Html.fromHtml(title.orEmpty(), Html.FROM_HTML_MODE_COMPACT).toString()
} else {
Html.fromHtml(title.orEmpty()).toString()
}
}

override fun equals(other: Any?): Boolean {
Expand All @@ -65,3 +89,10 @@ class NewsArticle : Comparable<NewsArticle> {
return other.date.compareTo(this.date)
}
}

@Root(name = "content", strict = false)
data class Thumbnail(
@field:Attribute(name = "url", required = false)
@param:Attribute(name = "url", required = false)
val url: String? = null
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.github.premnirmal.ticker.network.data

import com.google.gson.annotations.SerializedName


data class TrendingResult(
@SerializedName("count") val count: Int,
@SerializedName("pages") val pages: Int,
@SerializedName("current_page") val currentPage: Int,
@SerializedName("results") val results: List<Trending>
)
data class Trending(
@SerializedName("rank") val rank: Int,
@SerializedName("mentions") val mentions: Int,
@SerializedName("mentions_24h_ago") val mentions24hAgo: Int,
@SerializedName("upvotes") val upvotes: Int,
@SerializedName("ticker") val ticker: String,
@SerializedName("name") val name: String?
)
Loading

0 comments on commit 232d065

Please sign in to comment.