Historical data (#57)
* wip - historical data graphs * finished implementing graphs using alpha vantage api
This commit is contained in:
@@ -208,6 +208,7 @@ dependencies {
|
||||
implementation "uk.co.chrisjenx:calligraphy:2.1.0"
|
||||
implementation "com.jakewharton.timber:timber:4.3.1"
|
||||
implementation "saschpe.android:customtabs:1.1.1"
|
||||
implementation 'com.github.PhilJay:MPAndroidChart:v3.0.3'
|
||||
|
||||
prodImplementation("com.crashlytics.sdk.android:crashlytics:2.6.7@aar") {
|
||||
transitive = true
|
||||
|
||||
Vendored
+4
-1
@@ -71,4 +71,7 @@
|
||||
-keepattributes Signature
|
||||
-keepattributes Exceptions
|
||||
-dontwarn okio.**
|
||||
-keepattributes EnclosingMethod
|
||||
-keepattributes EnclosingMethod
|
||||
|
||||
# MPAndroidChart
|
||||
-keep class com.github.mikephil.charting.** { *; }
|
||||
@@ -21,6 +21,7 @@
|
||||
<activity
|
||||
android:name="com.github.premnirmal.ticker.home.SplashActivity"
|
||||
android:configChanges="orientation|keyboardHidden|screenSize"
|
||||
android:screenOrientation="portrait"
|
||||
android:theme="@style/SplashTheme">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
@@ -31,25 +32,31 @@
|
||||
android:name="com.github.premnirmal.ticker.home.ParanormalActivity"
|
||||
android:label="@string/app_name"
|
||||
android:launchMode="singleTask"
|
||||
android:screenOrientation="portrait"
|
||||
android:theme="@style/ParanormalActivityTheme"/>
|
||||
<activity
|
||||
android:name="com.github.premnirmal.ticker.portfolio.search.TickerSelectorActivity"
|
||||
android:label="@string/add_ticker"
|
||||
android:screenOrientation="portrait"
|
||||
android:theme="@style/TickerSelectorActivityTheme"
|
||||
android:windowSoftInputMode="adjustResize"/>
|
||||
<activity
|
||||
android:name="com.github.premnirmal.ticker.portfolio.AddPositionActivity"
|
||||
android:label="@string/add_position"/>
|
||||
android:label="@string/add_position"
|
||||
android:screenOrientation="portrait"/>
|
||||
<activity
|
||||
android:name="com.github.premnirmal.ticker.portfolio.EditPositionActivity"
|
||||
android:label="@string/edit_position"/>
|
||||
android:label="@string/edit_position"
|
||||
android:screenOrientation="portrait"/>
|
||||
<activity
|
||||
android:name="com.github.premnirmal.ticker.settings.SettingsActivity"
|
||||
android:label="@string/app_settings"
|
||||
android:screenOrientation="portrait"
|
||||
android:theme="@style/SettingsActivityTheme"/>
|
||||
<activity
|
||||
android:name="com.github.premnirmal.ticker.settings.WidgetSettingsActivity"
|
||||
android:label="@string/action_Settings"
|
||||
android:screenOrientation="portrait"
|
||||
android:theme="@style/WidgetSettingsActivityTheme">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE"/>
|
||||
@@ -58,7 +65,12 @@
|
||||
<activity
|
||||
android:name="com.github.premnirmal.ticker.news.NewsFeedActivity"
|
||||
android:label="@string/news_feed"
|
||||
android:screenOrientation="portrait"
|
||||
android:theme="@style/NewsFeedActivityTheme"/>
|
||||
<activity
|
||||
android:name="com.github.premnirmal.ticker.news.GraphActivity"
|
||||
android:screenOrientation="landscape"
|
||||
android:theme="@style/GraphActivityTheme"/>
|
||||
|
||||
<receiver
|
||||
android:name="com.github.premnirmal.ticker.UpdateReceiver"
|
||||
|
||||
@@ -133,7 +133,9 @@ class AppPreferences @Inject constructor() {
|
||||
const val DARK = 2
|
||||
const val LIGHT = 3
|
||||
|
||||
val TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm")!!
|
||||
val TIME_FORMATTER: DateTimeFormatter = DateTimeFormatter.ofPattern("HH:mm")
|
||||
val DATE_FORMATTER: DateTimeFormatter = DateTimeFormatter.ofPattern("MM/dd/yy")
|
||||
val AXIS_DATE_FORMATTER: DateTimeFormatter = DateTimeFormatter.ofPattern("MM/dd-yyyy")
|
||||
|
||||
val DECIMAL_FORMAT: Format = DecimalFormat("0.00")
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.github.premnirmal.ticker
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.ConnectivityManager
|
||||
import android.util.Base64
|
||||
import com.github.premnirmal.ticker.components.Analytics
|
||||
import com.github.premnirmal.ticker.components.AppComponent
|
||||
|
||||
@@ -6,15 +6,16 @@ import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.content.DialogInterface.OnClickListener
|
||||
import android.graphics.Rect
|
||||
import android.net.ConnectivityManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.PersistableBundle
|
||||
import android.support.v7.app.AppCompatActivity
|
||||
import android.view.View
|
||||
import com.github.premnirmal.ticker.components.InAppMessage
|
||||
import com.github.premnirmal.ticker.components.RxBus
|
||||
import com.github.premnirmal.ticker.events.ErrorEvent
|
||||
import com.github.premnirmal.ticker.portfolio.search.TickerSelectorActivity
|
||||
import com.github.premnirmal.tickerwidget.R.string
|
||||
import com.trello.rxlifecycle2.android.ActivityEvent
|
||||
import com.trello.rxlifecycle2.android.RxLifecycleAndroid
|
||||
import io.reactivex.Observable
|
||||
@@ -33,20 +34,6 @@ abstract class BaseActivity : AppCompatActivity() {
|
||||
|
||||
// Extension functions.
|
||||
|
||||
fun Activity.isNetworkOnline(): Boolean {
|
||||
try {
|
||||
val connectivityManager = this.getSystemService(
|
||||
Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||
val i = connectivityManager.activeNetworkInfo ?: return false
|
||||
if (!i.isConnected) return false
|
||||
if (!i.isAvailable) return false
|
||||
return true
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
fun Activity.getStatusBarHeight(): Int {
|
||||
val result: Int
|
||||
val resourceId: Int = this.resources.getIdentifier("status_bar_height", "dimen", "android")
|
||||
@@ -94,7 +81,8 @@ abstract class BaseActivity : AppCompatActivity() {
|
||||
|
||||
private val lifecycleSubject = BehaviorSubject.create<ActivityEvent>()
|
||||
|
||||
@Inject internal lateinit var bus: RxBus
|
||||
@Inject
|
||||
internal lateinit var bus: RxBus
|
||||
|
||||
private fun lifecycle(): Observable<ActivityEvent> = lifecycleSubject
|
||||
|
||||
@@ -157,4 +145,9 @@ abstract class BaseActivity : AppCompatActivity() {
|
||||
toolbar.paddingRight, toolbar.paddingBottom)
|
||||
}
|
||||
}
|
||||
|
||||
protected fun showErrorAndFinish() {
|
||||
InAppMessage.showToast(this, string.error_symbol)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
package com.github.premnirmal.ticker.base
|
||||
|
||||
import android.graphics.Color
|
||||
import com.github.mikephil.charting.charts.LineChart
|
||||
import com.github.mikephil.charting.components.XAxis
|
||||
import com.github.mikephil.charting.components.YAxis
|
||||
import com.github.mikephil.charting.data.LineData
|
||||
import com.github.mikephil.charting.data.LineDataSet
|
||||
import com.github.premnirmal.ticker.network.data.DataPoint
|
||||
import com.github.premnirmal.ticker.network.data.Quote
|
||||
import com.github.premnirmal.ticker.ui.DateAxisFormatter
|
||||
import com.github.premnirmal.ticker.ui.MultilineXAxisRenderer
|
||||
import com.github.premnirmal.ticker.ui.TextMarkerView
|
||||
import com.github.premnirmal.ticker.ui.ValueAxisFormatter
|
||||
import com.github.premnirmal.tickerwidget.R
|
||||
|
||||
abstract class BaseGraphActivity : BaseActivity() {
|
||||
|
||||
protected var dataPoints: List<DataPoint>? = null
|
||||
protected lateinit var quote: Quote
|
||||
|
||||
protected fun setupGraphView(graphView: LineChart) {
|
||||
graphView.isDoubleTapToZoomEnabled = false
|
||||
graphView.axisLeft.setDrawGridLines(false)
|
||||
graphView.axisLeft.setDrawAxisLine(false)
|
||||
graphView.axisLeft.isEnabled = false
|
||||
graphView.axisRight.setDrawGridLines(false)
|
||||
graphView.axisRight.setDrawAxisLine(true)
|
||||
graphView.axisRight.isEnabled = true
|
||||
graphView.xAxis.setDrawGridLines(false)
|
||||
graphView.setXAxisRenderer(MultilineXAxisRenderer(graphView.viewPortHandler,
|
||||
graphView.xAxis,
|
||||
graphView.getTransformer(YAxis.AxisDependency.RIGHT)))
|
||||
graphView.extraBottomOffset = resources.getDimension(R.dimen.graph_bottom_offset)
|
||||
graphView.legend.isEnabled = false
|
||||
graphView.description = null
|
||||
graphView.setNoDataTextColor(resources.getColor(R.color.colorAccent))
|
||||
graphView.setNoDataText("")
|
||||
graphView.marker = TextMarkerView(this)
|
||||
}
|
||||
|
||||
protected fun loadGraph(graphView: LineChart) {
|
||||
if (dataPoints == null || dataPoints!!.isEmpty()) {
|
||||
onNoGraphData(graphView)
|
||||
graphView.setNoDataText(getString(R.string.no_data))
|
||||
graphView.invalidate()
|
||||
return
|
||||
}
|
||||
graphView.setNoDataText("")
|
||||
graphView.lineData?.clearValues()
|
||||
val series = LineDataSet(dataPoints, quote.symbol)
|
||||
series.setDrawHorizontalHighlightIndicator(false)
|
||||
series.setDrawValues(false)
|
||||
val colorAccent = resources.getColor(R.color.color_accent)
|
||||
series.setDrawFilled(true)
|
||||
series.color = colorAccent
|
||||
series.fillColor = colorAccent
|
||||
series.fillAlpha = 150
|
||||
series.setDrawCircles(true)
|
||||
series.mode = LineDataSet.Mode.CUBIC_BEZIER
|
||||
series.cubicIntensity = 0.07f
|
||||
series.lineWidth = 2f
|
||||
series.setDrawCircles(false)
|
||||
series.highLightColor = Color.GRAY
|
||||
val lineData = LineData(series)
|
||||
graphView.data = lineData
|
||||
val xAxis: XAxis = graphView.xAxis
|
||||
val yAxis: YAxis = graphView.axisRight
|
||||
xAxis.valueFormatter = DateAxisFormatter()
|
||||
yAxis.valueFormatter = ValueAxisFormatter()
|
||||
xAxis.position = XAxis.XAxisPosition.BOTTOM
|
||||
xAxis.textSize = 10f
|
||||
yAxis.textSize = 10f
|
||||
xAxis.textColor = Color.GRAY
|
||||
yAxis.textColor = Color.GRAY
|
||||
xAxis.setLabelCount(5, true)
|
||||
yAxis.setLabelCount(5, true)
|
||||
yAxis.setPosition(YAxis.YAxisLabelPosition.OUTSIDE_CHART)
|
||||
xAxis.setDrawAxisLine(true)
|
||||
yAxis.setDrawAxisLine(true)
|
||||
xAxis.setDrawGridLines(false)
|
||||
yAxis.setDrawGridLines(false)
|
||||
graphView.invalidate()
|
||||
onGraphDataAdded(graphView)
|
||||
}
|
||||
|
||||
protected abstract fun onGraphDataAdded(graphView: LineChart)
|
||||
|
||||
protected abstract fun onNoGraphData(graphView: LineChart)
|
||||
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import com.github.premnirmal.ticker.home.HomePagerAdapter
|
||||
import com.github.premnirmal.ticker.home.ParanormalActivity
|
||||
import com.github.premnirmal.ticker.home.SplashActivity
|
||||
import com.github.premnirmal.ticker.model.AlarmScheduler
|
||||
import com.github.premnirmal.ticker.model.HistoryProvider
|
||||
import com.github.premnirmal.ticker.model.RefreshService
|
||||
import com.github.premnirmal.ticker.model.StocksProvider
|
||||
import com.github.premnirmal.ticker.model.StocksStorage
|
||||
@@ -14,6 +15,7 @@ import com.github.premnirmal.ticker.network.NewsProvider
|
||||
import com.github.premnirmal.ticker.network.RequestInterceptor
|
||||
import com.github.premnirmal.ticker.network.StocksApi
|
||||
import com.github.premnirmal.ticker.network.UserAgentInterceptor
|
||||
import com.github.premnirmal.ticker.news.GraphActivity
|
||||
import com.github.premnirmal.ticker.news.NewsFeedActivity
|
||||
import com.github.premnirmal.ticker.portfolio.AddPositionActivity
|
||||
import com.github.premnirmal.ticker.portfolio.EditPositionActivity
|
||||
@@ -56,6 +58,8 @@ interface AppComponent {
|
||||
|
||||
fun inject(newsFeedActivity: NewsFeedActivity)
|
||||
|
||||
fun inject(graphActivity: GraphActivity)
|
||||
|
||||
// Components
|
||||
|
||||
fun inject(stocksStorage: StocksStorage)
|
||||
@@ -64,6 +68,8 @@ interface AppComponent {
|
||||
|
||||
fun inject(stocksProvider: StocksProvider)
|
||||
|
||||
fun inject(historicalDataProvider: HistoryProvider)
|
||||
|
||||
fun inject(alarmScheduler: AlarmScheduler)
|
||||
|
||||
fun inject(updateReceiver: UpdateReceiver)
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.github.premnirmal.ticker.components
|
||||
|
||||
import android.content.Context
|
||||
import android.net.ConnectivityManager
|
||||
|
||||
|
||||
fun Context.isNetworkOnline(): Boolean {
|
||||
try {
|
||||
val connectivityManager = this.getSystemService(
|
||||
Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||
val i = connectivityManager.activeNetworkInfo ?: return false
|
||||
if (!i.isConnected) return false
|
||||
if (!i.isAvailable) return false
|
||||
return true
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
fun Long.minutesInMs(): Long {
|
||||
return this * 60 * 1000
|
||||
}
|
||||
@@ -22,6 +22,7 @@ import com.github.premnirmal.ticker.AppPreferences
|
||||
import com.github.premnirmal.ticker.base.BaseActivity
|
||||
import com.github.premnirmal.ticker.components.InAppMessage
|
||||
import com.github.premnirmal.ticker.components.Injector
|
||||
import com.github.premnirmal.ticker.components.isNetworkOnline
|
||||
import com.github.premnirmal.ticker.model.IStocksProvider
|
||||
import com.github.premnirmal.ticker.network.SimpleSubscriber
|
||||
import com.github.premnirmal.ticker.network.data.Quote
|
||||
@@ -269,7 +270,7 @@ class ParanormalActivity : BaseActivity(), PortfolioFragment.Callback {
|
||||
override fun onError(e: Throwable) {
|
||||
attemptingFetch = false
|
||||
swipe_container?.isRefreshing = false
|
||||
InAppMessage.showMessage(this@ParanormalActivity, getString(R.string.refresh_failed))
|
||||
InAppMessage.showMessage(this@ParanormalActivity, getString(string.refresh_failed))
|
||||
}
|
||||
|
||||
override fun onNext(result: List<Quote>) {
|
||||
@@ -280,12 +281,12 @@ class ParanormalActivity : BaseActivity(), PortfolioFragment.Callback {
|
||||
})
|
||||
} else {
|
||||
attemptingFetch = false
|
||||
InAppMessage.showMessage(this, getString(R.string.refresh_failed))
|
||||
InAppMessage.showMessage(this, getString(string.refresh_failed))
|
||||
swipe_container?.isRefreshing = false
|
||||
}
|
||||
} else {
|
||||
attemptingFetch = false
|
||||
InAppMessage.showMessage(this, getString(R.string.no_network_message))
|
||||
InAppMessage.showMessage(this, getString(string.no_network_message))
|
||||
swipe_container?.isRefreshing = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
package com.github.premnirmal.ticker.model
|
||||
|
||||
import com.github.premnirmal.ticker.components.Injector
|
||||
import com.github.premnirmal.ticker.model.IHistoryProvider.Range
|
||||
import com.github.premnirmal.ticker.network.HistoricalDataApi
|
||||
import com.github.premnirmal.ticker.network.data.DataPoint
|
||||
import com.github.premnirmal.tickerwidget.R
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import org.threeten.bp.LocalDate
|
||||
import org.threeten.bp.format.DateTimeFormatter
|
||||
import javax.inject.Inject
|
||||
|
||||
class HistoryProvider : IHistoryProvider {
|
||||
|
||||
@Inject
|
||||
internal lateinit var historicalDataApi: HistoricalDataApi
|
||||
private val apiKey = Injector.appComponent.appContext().getString(R.string.alpha_vantage_api_key)
|
||||
|
||||
private var cachedData: Pair<String, List<DataPoint>>? = null
|
||||
|
||||
init {
|
||||
Injector.appComponent.inject(this)
|
||||
}
|
||||
|
||||
override fun getHistoricalDataShort(symbol: String): Observable<List<DataPoint>> {
|
||||
return historicalDataApi.getHistoricalData(apiKey = apiKey, symbol = symbol).map {
|
||||
val points = ArrayList<DataPoint>()
|
||||
it.timeSeries.forEach { k, v ->
|
||||
val epochDate = LocalDate.parse(k, DateTimeFormatter.ISO_LOCAL_DATE).toEpochDay()
|
||||
points.add(DataPoint(epochDate.toFloat(), v.close.toFloat()))
|
||||
}
|
||||
points.sort()
|
||||
points as List<DataPoint>
|
||||
}.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
}
|
||||
|
||||
override fun getHistoricalDataByRange(symbol: String, range: Range): Observable<List<DataPoint>> {
|
||||
val observable: Observable<List<DataPoint>>
|
||||
if (symbol == cachedData?.first) {
|
||||
observable = Observable.fromCallable {
|
||||
val filtered = cachedData!!.second.filter {
|
||||
it.getDate().isAfter(range.end)
|
||||
}.toMutableList()
|
||||
filtered.sort()
|
||||
filtered
|
||||
}
|
||||
} else {
|
||||
cachedData = null
|
||||
observable = historicalDataApi.getHistoricalDataFull(apiKey = apiKey, symbol = symbol).map {
|
||||
val points = ArrayList<DataPoint>()
|
||||
it.timeSeries.forEach { k, v ->
|
||||
val epochDate = LocalDate.parse(k, DateTimeFormatter.ISO_LOCAL_DATE).toEpochDay()
|
||||
points.add(DataPoint(epochDate.toFloat(),
|
||||
v.close.toFloat()))
|
||||
}
|
||||
cachedData = Pair(symbol, points)
|
||||
val filtered = points.filter { it.getDate().isAfter(range.end) }.toMutableList()
|
||||
filtered.sort()
|
||||
filtered
|
||||
}
|
||||
}
|
||||
return observable.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.github.premnirmal.ticker.model
|
||||
|
||||
import com.github.premnirmal.ticker.network.data.DataPoint
|
||||
import io.reactivex.Observable
|
||||
import org.threeten.bp.LocalDate
|
||||
import java.io.Serializable
|
||||
|
||||
interface IHistoryProvider {
|
||||
|
||||
fun getHistoricalDataShort(symbol: String): Observable<List<DataPoint>>
|
||||
|
||||
fun getHistoricalDataByRange(symbol: String, range: Range): Observable<List<DataPoint>>
|
||||
|
||||
sealed class Range(val end: LocalDate) : Serializable {
|
||||
class DateRange(end: LocalDate): Range(end)
|
||||
companion object {
|
||||
val ONE_MONTH = DateRange(LocalDate.now().minusMonths(1))
|
||||
val THREE_MONTH = DateRange(LocalDate.now().minusMonths(3))
|
||||
val ONE_YEAR = DateRange(LocalDate.now().minusYears(1))
|
||||
val MAX = DateRange(LocalDate.now().minusYears(20))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,8 @@ interface IStocksProvider {
|
||||
|
||||
fun schedule()
|
||||
|
||||
fun scheduleSoon()
|
||||
|
||||
fun getTickers(): List<String>
|
||||
|
||||
fun getStock(ticker: String): Quote?
|
||||
|
||||
@@ -5,6 +5,7 @@ import android.app.job.JobService
|
||||
import android.os.Build.VERSION_CODES
|
||||
import android.support.annotation.RequiresApi
|
||||
import com.github.premnirmal.ticker.components.Injector
|
||||
import com.github.premnirmal.ticker.components.isNetworkOnline
|
||||
import com.github.premnirmal.ticker.network.SimpleSubscriber
|
||||
import com.github.premnirmal.ticker.network.data.Quote
|
||||
import timber.log.Timber
|
||||
@@ -23,22 +24,27 @@ class RefreshService : JobService() {
|
||||
|
||||
override fun onStartJob(params: JobParameters): Boolean {
|
||||
Timber.i("onStartJob " + params.jobId)
|
||||
stocksProvider.fetch().subscribe(
|
||||
object : SimpleSubscriber<List<Quote>>() {
|
||||
override fun onError(e: Throwable) {
|
||||
// StocksProvider will handle rescheduling the job
|
||||
val needsReschedule = false
|
||||
jobFinished(params, needsReschedule)
|
||||
}
|
||||
if (isNetworkOnline()) {
|
||||
stocksProvider.fetch().subscribe(
|
||||
object : SimpleSubscriber<List<Quote>>() {
|
||||
override fun onError(e: Throwable) {
|
||||
// StocksProvider will handle rescheduling the job
|
||||
val needsReschedule = false
|
||||
jobFinished(params, needsReschedule)
|
||||
}
|
||||
|
||||
override fun onComplete() {
|
||||
// doesn't need reschedule
|
||||
val needsReschedule = false
|
||||
jobFinished(params, needsReschedule)
|
||||
}
|
||||
})
|
||||
// additional work is being performed
|
||||
return true
|
||||
override fun onComplete() {
|
||||
// doesn't need reschedule
|
||||
val needsReschedule = false
|
||||
jobFinished(params, needsReschedule)
|
||||
}
|
||||
})
|
||||
// additional work is being performed
|
||||
return true
|
||||
} else {
|
||||
stocksProvider.scheduleSoon()
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStopJob(params: JobParameters): Boolean {
|
||||
|
||||
@@ -9,6 +9,7 @@ import com.github.premnirmal.ticker.components.AppClock
|
||||
import com.github.premnirmal.ticker.components.InAppMessage
|
||||
import com.github.premnirmal.ticker.components.Injector
|
||||
import com.github.premnirmal.ticker.components.RxBus
|
||||
import com.github.premnirmal.ticker.components.minutesInMs
|
||||
import com.github.premnirmal.ticker.events.ErrorEvent
|
||||
import com.github.premnirmal.ticker.events.RefreshEvent
|
||||
import com.github.premnirmal.ticker.network.RobindahoodException
|
||||
@@ -287,6 +288,10 @@ class StocksProvider @Inject constructor() : IStocksProvider {
|
||||
scheduleUpdate()
|
||||
}
|
||||
|
||||
override fun scheduleSoon() {
|
||||
scheduleUpdateWithMs(5L.minutesInMs(), true)
|
||||
}
|
||||
|
||||
override fun addStock(ticker: String): Collection<String> {
|
||||
synchronized(quoteList, {
|
||||
if (!tickerList.contains(ticker)) {
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.github.premnirmal.ticker.network
|
||||
|
||||
import com.github.premnirmal.ticker.network.data.HistoricalData
|
||||
import io.reactivex.Observable
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Query
|
||||
|
||||
const val TIME_SERIES_DAILY = "TIME_SERIES_DAILY"
|
||||
|
||||
interface HistoricalDataApi {
|
||||
|
||||
@GET("query")
|
||||
fun getHistoricalData(@Query(value = "function") function: String = TIME_SERIES_DAILY,
|
||||
@Query(value = "apikey") apiKey: String,
|
||||
@Query(value = "symbol") symbol: String): Observable<HistoricalData>
|
||||
|
||||
@GET("query")
|
||||
fun getHistoricalDataFull(@Query(value = "function") function: String = TIME_SERIES_DAILY,
|
||||
@Query(value = "outputsize") outputSize: String = "full",
|
||||
@Query(value = "apikey") apiKey: String,
|
||||
@Query(value = "symbol") symbol: String): Observable<HistoricalData>
|
||||
}
|
||||
@@ -3,6 +3,8 @@ package com.github.premnirmal.ticker.network
|
||||
import android.content.Context
|
||||
import com.github.premnirmal.ticker.components.RxBus
|
||||
import com.github.premnirmal.ticker.model.AlarmScheduler
|
||||
import com.github.premnirmal.ticker.model.HistoryProvider
|
||||
import com.github.premnirmal.ticker.model.IHistoryProvider
|
||||
import com.github.premnirmal.ticker.model.IStocksProvider
|
||||
import com.github.premnirmal.ticker.model.StocksProvider
|
||||
import com.github.premnirmal.ticker.widget.WidgetDataProvider
|
||||
@@ -36,7 +38,7 @@ class NetworkModule {
|
||||
internal const val READ_TIMEOUT: Long = 20000
|
||||
}
|
||||
|
||||
@Provides @Singleton @Named("yahooClient")
|
||||
@Provides @Singleton @Named("client")
|
||||
internal fun provideHttpClientForYahoo(): OkHttpClient {
|
||||
val logger = HttpLoggingInterceptor()
|
||||
logger.level = if (BuildConfig.DEBUG) HttpLoggingInterceptor.Level.BODY else HttpLoggingInterceptor.Level.NONE
|
||||
@@ -49,19 +51,6 @@ class NetworkModule {
|
||||
return okHttpClient
|
||||
}
|
||||
|
||||
@Provides @Singleton @Named("newsClient")
|
||||
internal fun provideHttpClientForNews(): OkHttpClient {
|
||||
val logger = HttpLoggingInterceptor()
|
||||
logger.level = if (BuildConfig.DEBUG) HttpLoggingInterceptor.Level.BODY else HttpLoggingInterceptor.Level.NONE
|
||||
val okHttpClient = OkHttpClient.Builder()
|
||||
.addInterceptor(UserAgentInterceptor())
|
||||
.addInterceptor(logger)
|
||||
.readTimeout(READ_TIMEOUT, TimeUnit.MILLISECONDS)
|
||||
.connectTimeout(CONNECTION_TIMEOUT, TimeUnit.MILLISECONDS)
|
||||
.build()
|
||||
return okHttpClient
|
||||
}
|
||||
|
||||
@Provides @Singleton @Named("robindahoodClient")
|
||||
internal fun provideHttpClientForRobindahood(context: Context, bus: RxBus): OkHttpClient {
|
||||
val logger = HttpLoggingInterceptor()
|
||||
@@ -98,7 +87,7 @@ class NetworkModule {
|
||||
|
||||
@Provides @Singleton
|
||||
internal fun provideSuggestionsApi(context: Context,
|
||||
@Named("yahooClient") okHttpClient: OkHttpClient,
|
||||
@Named("client") okHttpClient: OkHttpClient,
|
||||
gson: Gson, converterFactory: GsonConverterFactory,
|
||||
rxJavaFactory: RxJava2CallAdapterFactory): SuggestionApi {
|
||||
val Retrofit = Retrofit.Builder()
|
||||
@@ -146,7 +135,7 @@ class NetworkModule {
|
||||
|
||||
@Provides @Singleton
|
||||
internal fun provideNewsApi(context: Context,
|
||||
@Named("newsClient") okHttpClient: OkHttpClient,
|
||||
@Named("client") okHttpClient: OkHttpClient,
|
||||
gson: Gson, converterFactory: GsonConverterFactory,
|
||||
rxJavaFactory: RxJava2CallAdapterFactory): NewsApi {
|
||||
val Retrofit = Retrofit.Builder()
|
||||
@@ -159,12 +148,30 @@ class NetworkModule {
|
||||
return newsApi
|
||||
}
|
||||
|
||||
@Provides @Singleton
|
||||
internal fun provideHistoricalDataApi(context: Context,
|
||||
@Named("client") okHttpClient: OkHttpClient,
|
||||
gson: Gson, converterFactory: GsonConverterFactory,
|
||||
rxJavaFactory: RxJava2CallAdapterFactory): HistoricalDataApi {
|
||||
val Retrofit = Retrofit.Builder()
|
||||
.client(okHttpClient)
|
||||
.baseUrl(context.getString(R.string.alpha_vantage_endpoint))
|
||||
.addCallAdapterFactory(rxJavaFactory)
|
||||
.addConverterFactory(converterFactory)
|
||||
.build()
|
||||
val api = Retrofit.create(HistoricalDataApi::class.java)
|
||||
return api
|
||||
}
|
||||
|
||||
@Provides @Singleton
|
||||
internal fun provideNewsProvider(): NewsProvider = NewsProvider()
|
||||
|
||||
@Provides @Singleton
|
||||
internal fun provideStocksProvider(): IStocksProvider = StocksProvider()
|
||||
|
||||
@Provides @Singleton
|
||||
internal fun provideHistoricalDataProvider(): IHistoryProvider = HistoryProvider()
|
||||
|
||||
@Provides @Singleton
|
||||
internal fun provideAlarmScheduler(): AlarmScheduler = AlarmScheduler()
|
||||
|
||||
|
||||
@@ -17,10 +17,12 @@ import javax.inject.Singleton
|
||||
@Singleton
|
||||
class StocksApi @Inject constructor() {
|
||||
|
||||
@Inject internal lateinit var gson: Gson
|
||||
@Inject internal lateinit var financeApi: Robindahood
|
||||
@Inject internal lateinit var clock: AppClock
|
||||
|
||||
@Inject
|
||||
internal lateinit var gson: Gson
|
||||
@Inject
|
||||
internal lateinit var financeApi: Robindahood
|
||||
@Inject
|
||||
internal lateinit var clock: AppClock
|
||||
var lastFetched: Long = 0
|
||||
|
||||
init {
|
||||
@@ -66,5 +68,4 @@ class StocksApi @Inject constructor() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.github.premnirmal.ticker.network.data
|
||||
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import com.github.mikephil.charting.data.Entry
|
||||
import com.github.premnirmal.ticker.network.data.HistoricalData.HistoricalValue
|
||||
import org.threeten.bp.LocalDate
|
||||
import org.threeten.bp.format.DateTimeFormatter
|
||||
import java.io.Serializable
|
||||
|
||||
class DataPoint : Entry, Serializable, Comparable<DataPoint> {
|
||||
|
||||
constructor(x: Float, y: Float) : super(x, y)
|
||||
|
||||
constructor(x: Float, y: Float, data: HistoricalValue) : super(x, y, data)
|
||||
|
||||
constructor(source: Parcel) : super(source)
|
||||
|
||||
fun getDate(): LocalDate = LocalDate.ofEpochDay(x.toLong())
|
||||
|
||||
override fun compareTo(other: DataPoint): Int = x.compareTo(other.x)
|
||||
|
||||
companion object {
|
||||
|
||||
private val FORMATTER: DateTimeFormatter by lazy { DateTimeFormatter.ofPattern("MMMM d") }
|
||||
private const val serialVersionUID = 42L
|
||||
|
||||
val CREATOR: Parcelable.Creator<DataPoint> = object : Parcelable.Creator<DataPoint> {
|
||||
override fun createFromParcel(source: Parcel): DataPoint {
|
||||
return DataPoint(source)
|
||||
}
|
||||
|
||||
override fun newArray(size: Int): Array<DataPoint?> {
|
||||
return arrayOfNulls(size)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.github.premnirmal.ticker.network.data
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
data class HistoricalData(
|
||||
@field:SerializedName("Time Series (Daily)")
|
||||
var timeSeries: Map<String, HistoricalValue> = LinkedHashMap()) {
|
||||
|
||||
data class HistoricalValue(
|
||||
@field:SerializedName("1. open")
|
||||
var open: String = "",
|
||||
@field:SerializedName("4. close")
|
||||
var close: String = "",
|
||||
@field:SerializedName("3. low")
|
||||
var low: String = "",
|
||||
@field:SerializedName("2. high")
|
||||
var high: String = "",
|
||||
@field:SerializedName("5. volume")
|
||||
var volume: String = "")
|
||||
|
||||
}
|
||||
@@ -94,6 +94,6 @@ data class Quote(var symbol: String = "",
|
||||
}
|
||||
}
|
||||
|
||||
override operator fun compareTo(other: Quote): Int = java.lang.Float.compare(
|
||||
other.changeInPercent, changeInPercent)
|
||||
override operator fun compareTo(other: Quote): Int =
|
||||
other.changeInPercent.compareTo(changeInPercent)
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
package com.github.premnirmal.ticker.news
|
||||
|
||||
import android.content.DialogInterface
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.WindowManager
|
||||
import com.github.mikephil.charting.animation.Easing
|
||||
import com.github.mikephil.charting.charts.LineChart
|
||||
import com.github.premnirmal.ticker.base.BaseGraphActivity
|
||||
import com.github.premnirmal.ticker.components.Injector
|
||||
import com.github.premnirmal.ticker.components.isNetworkOnline
|
||||
import com.github.premnirmal.ticker.model.IHistoryProvider
|
||||
import com.github.premnirmal.ticker.model.IHistoryProvider.Range
|
||||
import com.github.premnirmal.ticker.model.IStocksProvider
|
||||
import com.github.premnirmal.ticker.network.SimpleSubscriber
|
||||
import com.github.premnirmal.ticker.network.data.DataPoint
|
||||
import com.github.premnirmal.ticker.network.data.Quote
|
||||
import com.github.premnirmal.tickerwidget.R
|
||||
import kotlinx.android.synthetic.main.activity_graph.desc
|
||||
import kotlinx.android.synthetic.main.activity_graph.graphActivityRoot
|
||||
import kotlinx.android.synthetic.main.activity_graph.graphView
|
||||
import kotlinx.android.synthetic.main.activity_graph.graph_holder
|
||||
import kotlinx.android.synthetic.main.activity_graph.max
|
||||
import kotlinx.android.synthetic.main.activity_graph.one_month
|
||||
import kotlinx.android.synthetic.main.activity_graph.one_year
|
||||
import kotlinx.android.synthetic.main.activity_graph.progress
|
||||
import kotlinx.android.synthetic.main.activity_graph.three_month
|
||||
import kotlinx.android.synthetic.main.activity_graph.tickerName
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
class GraphActivity : BaseGraphActivity() {
|
||||
|
||||
companion object {
|
||||
const val TICKER = "TICKER"
|
||||
private const val DATA_POINTS = "DATA_POINTS"
|
||||
private const val RANGE = "RANGE"
|
||||
private const val DURATION = 2000
|
||||
}
|
||||
|
||||
private var range = Range.THREE_MONTH
|
||||
private lateinit var ticker: String
|
||||
@Inject
|
||||
internal lateinit var historyProvider: IHistoryProvider
|
||||
@Inject
|
||||
internal lateinit var stocksProvider: IStocksProvider
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
Injector.appComponent.inject(this)
|
||||
setContentView(R.layout.activity_graph)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
graphActivityRoot.setPadding(graphActivityRoot.paddingLeft, getStatusBarHeight(),
|
||||
graphActivityRoot.paddingRight, graphActivityRoot.paddingBottom)
|
||||
}
|
||||
if (Build.VERSION.SDK_INT < 16) {
|
||||
window.setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
|
||||
WindowManager.LayoutParams.FLAG_FULLSCREEN)
|
||||
} else {
|
||||
val decorView = window.decorView
|
||||
decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_FULLSCREEN
|
||||
}
|
||||
setupGraphView(graphView)
|
||||
val q: Quote?
|
||||
if (intent.hasExtra(TICKER) && intent.getStringExtra(TICKER) != null) {
|
||||
ticker = intent.getStringExtra(TICKER)
|
||||
q = stocksProvider.getStock(ticker)
|
||||
if (q == null) {
|
||||
showErrorAndFinish()
|
||||
}
|
||||
} else {
|
||||
ticker = ""
|
||||
showErrorAndFinish()
|
||||
return
|
||||
}
|
||||
quote = q!!
|
||||
tickerName.text = ticker
|
||||
desc.text = quote.name
|
||||
|
||||
savedInstanceState?.let {
|
||||
dataPoints = it.getParcelableArrayList(DATA_POINTS)
|
||||
}
|
||||
|
||||
var view: View? = null
|
||||
when (range) {
|
||||
Range.ONE_MONTH -> view = one_month
|
||||
Range.THREE_MONTH -> view = three_month
|
||||
Range.ONE_YEAR -> view = one_year
|
||||
Range.MAX -> view = max
|
||||
}
|
||||
view?.isEnabled = false
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
if (dataPoints == null) {
|
||||
getData()
|
||||
} else {
|
||||
loadGraph(graphView)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
dataPoints?.let {
|
||||
outState.putParcelableArrayList(DATA_POINTS, ArrayList(it))
|
||||
}
|
||||
outState.putSerializable(RANGE, range)
|
||||
}
|
||||
|
||||
private fun getData() {
|
||||
if (isNetworkOnline()) {
|
||||
graph_holder.visibility = View.GONE
|
||||
progress.visibility = View.VISIBLE
|
||||
val observable = historyProvider.getHistoricalDataByRange(ticker, range)
|
||||
bind(observable).subscribe(object : SimpleSubscriber<List<DataPoint>>() {
|
||||
override fun onError(e: Throwable) {
|
||||
Timber.w(e)
|
||||
showDialog(getString(R.string.error_loading_graph),
|
||||
DialogInterface.OnClickListener { _, _ -> finish() })
|
||||
}
|
||||
|
||||
override fun onNext(result: List<DataPoint>) {
|
||||
dataPoints = result
|
||||
loadGraph(graphView)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
showDialog(getString(R.string.no_network_message),
|
||||
DialogInterface.OnClickListener { _, _ -> finish() })
|
||||
}
|
||||
}
|
||||
|
||||
override fun onGraphDataAdded(graphView: LineChart) {
|
||||
progress.visibility = View.GONE
|
||||
graph_holder.visibility = View.VISIBLE
|
||||
graphView.animateX(DURATION, Easing.EasingOption.EaseInOutCubic)
|
||||
}
|
||||
|
||||
override fun onNoGraphData(graphView: LineChart) {
|
||||
progress.visibility = View.GONE
|
||||
graph_holder.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
/**
|
||||
* xml OnClick
|
||||
* @param v
|
||||
*/
|
||||
fun updateRange(v: View) {
|
||||
when (v.id) {
|
||||
R.id.one_month -> range = Range.ONE_MONTH
|
||||
R.id.three_month -> range = Range.THREE_MONTH
|
||||
R.id.one_year -> range = Range.ONE_YEAR
|
||||
R.id.max -> range = Range.MAX
|
||||
}
|
||||
val parent = v.parent as ViewGroup
|
||||
(0 until parent.childCount)
|
||||
.map { parent.getChildAt(it) }
|
||||
.forEach { it.isEnabled = it != v }
|
||||
getData()
|
||||
}
|
||||
}
|
||||
@@ -13,9 +13,11 @@ import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import com.github.premnirmal.ticker.base.BaseActivity
|
||||
import com.github.premnirmal.ticker.components.InAppMessage
|
||||
import com.github.mikephil.charting.charts.LineChart
|
||||
import com.github.premnirmal.ticker.base.BaseGraphActivity
|
||||
import com.github.premnirmal.ticker.components.Injector
|
||||
import com.github.premnirmal.ticker.network.data.DataPoint
|
||||
import com.github.premnirmal.ticker.model.IHistoryProvider
|
||||
import com.github.premnirmal.ticker.model.IStocksProvider
|
||||
import com.github.premnirmal.ticker.network.NewsProvider
|
||||
import com.github.premnirmal.ticker.network.SimpleSubscriber
|
||||
@@ -23,37 +25,39 @@ import com.github.premnirmal.ticker.network.data.NewsArticle
|
||||
import com.github.premnirmal.ticker.network.data.Quote
|
||||
import com.github.premnirmal.ticker.portfolio.EditPositionActivity
|
||||
import com.github.premnirmal.tickerwidget.R
|
||||
import com.github.premnirmal.tickerwidget.R.string
|
||||
import kotlinx.android.synthetic.main.activity_news_feed.average_price
|
||||
import kotlinx.android.synthetic.main.activity_news_feed.change
|
||||
import kotlinx.android.synthetic.main.activity_news_feed.description
|
||||
import kotlinx.android.synthetic.main.activity_news_feed.edit_positions
|
||||
import kotlinx.android.synthetic.main.activity_news_feed.equityValue
|
||||
import kotlinx.android.synthetic.main.activity_news_feed.exchange
|
||||
import kotlinx.android.synthetic.main.activity_news_feed.graphView
|
||||
import kotlinx.android.synthetic.main.activity_news_feed.graph_container
|
||||
import kotlinx.android.synthetic.main.activity_news_feed.lastTradePrice
|
||||
import kotlinx.android.synthetic.main.activity_news_feed.news_container
|
||||
import kotlinx.android.synthetic.main.activity_news_feed.numShares
|
||||
import kotlinx.android.synthetic.main.activity_news_feed.progress
|
||||
import kotlinx.android.synthetic.main.activity_news_feed.tickerName
|
||||
import kotlinx.android.synthetic.main.activity_news_feed.toolbar
|
||||
import kotlinx.android.synthetic.main.activity_news_feed.total_gain_loss
|
||||
import saschpe.android.customtabs.CustomTabsHelper
|
||||
import saschpe.android.customtabs.WebViewFallback
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
|
||||
class NewsFeedActivity : BaseActivity() {
|
||||
class NewsFeedActivity : BaseGraphActivity() {
|
||||
|
||||
companion object {
|
||||
const val TICKER = "TICKER"
|
||||
private const val DATA_POINTS = "DATA_POINTS"
|
||||
}
|
||||
|
||||
@Inject
|
||||
internal lateinit var stocksProvider: IStocksProvider
|
||||
@Inject
|
||||
lateinit var newsProvider: NewsProvider
|
||||
internal lateinit var newsProvider: NewsProvider
|
||||
@Inject
|
||||
internal lateinit var historyProvider: IHistoryProvider
|
||||
private lateinit var ticker: String
|
||||
private lateinit var quote: Quote
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
@@ -63,6 +67,9 @@ class NewsFeedActivity : BaseActivity() {
|
||||
toolbar.setNavigationOnClickListener {
|
||||
finish()
|
||||
}
|
||||
graph_container.layoutParams.height = (resources.displayMetrics.widthPixels * 0.5625f).toInt()
|
||||
graph_container.requestLayout()
|
||||
setupGraphView(graphView)
|
||||
val q: Quote?
|
||||
if (intent.hasExtra(TICKER) && intent.getStringExtra(TICKER) != null) {
|
||||
ticker = intent.getStringExtra(TICKER)
|
||||
@@ -96,18 +103,30 @@ class NewsFeedActivity : BaseActivity() {
|
||||
intent.putExtra(EditPositionActivity.TICKER, quote.symbol)
|
||||
startActivity(intent)
|
||||
}
|
||||
savedInstanceState?.let {
|
||||
dataPoints = it.getParcelableArrayList(DATA_POINTS)
|
||||
}
|
||||
}
|
||||
|
||||
if (news_container.childCount <= 1) {
|
||||
bind(newsProvider.getNews(quote.newsQuery())).subscribe(
|
||||
object : SimpleSubscriber<List<NewsArticle>>() {
|
||||
override fun onNext(result: List<NewsArticle>) {
|
||||
setUpArticles(result)
|
||||
}
|
||||
private fun fetchData() {
|
||||
bind(historyProvider.getHistoricalDataShort(quote.symbol)).subscribe(
|
||||
object : SimpleSubscriber<List<DataPoint>>() {
|
||||
override fun onNext(result: List<DataPoint>) {
|
||||
dataPoints = result
|
||||
loadGraph(graphView)
|
||||
}
|
||||
|
||||
override fun onError(e: Throwable) {
|
||||
Timber.w(e)
|
||||
}
|
||||
})
|
||||
override fun onError(e: Throwable) {
|
||||
progress.visibility = View.GONE
|
||||
graphView.setNoDataText(getString(R.string.no_data))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
dataPoints?.let {
|
||||
outState.putParcelableArrayList(DATA_POINTS, ArrayList(it))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,6 +169,18 @@ class NewsFeedActivity : BaseActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
if (news_container.childCount <= 1) {
|
||||
fetchNews()
|
||||
}
|
||||
if (dataPoints == null) {
|
||||
fetchData()
|
||||
} else {
|
||||
loadGraph(graphView)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
numShares.text = quote.numSharesString()
|
||||
@@ -170,9 +201,34 @@ class NewsFeedActivity : BaseActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun showErrorAndFinish() {
|
||||
InAppMessage.showToast(this, string.error_symbol)
|
||||
finish()
|
||||
private fun fetchNews() {
|
||||
bind(newsProvider.getNews(quote.newsQuery())).subscribe(
|
||||
object : SimpleSubscriber<List<NewsArticle>>() {
|
||||
override fun onNext(result: List<NewsArticle>) {
|
||||
setUpArticles(result)
|
||||
}
|
||||
|
||||
override fun onError(e: Throwable) {
|
||||
news_container.visibility = View.GONE
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun onGraphDataAdded(graphView: LineChart) {
|
||||
progress.visibility = View.GONE
|
||||
}
|
||||
|
||||
override fun onNoGraphData(graphView: LineChart) {
|
||||
progress.visibility = View.GONE
|
||||
}
|
||||
|
||||
/**
|
||||
* Called via xml
|
||||
*/
|
||||
fun openGraph(v: View) {
|
||||
val intent = Intent(this, GraphActivity::class.java)
|
||||
intent.putExtra(GraphActivity.TICKER, ticker)
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
private fun Drawable.toBitmap(): Bitmap {
|
||||
|
||||
+1
@@ -19,6 +19,7 @@ import android.view.ViewTreeObserver
|
||||
import com.github.premnirmal.ticker.base.BaseActivity
|
||||
import com.github.premnirmal.ticker.components.InAppMessage
|
||||
import com.github.premnirmal.ticker.components.Injector
|
||||
import com.github.premnirmal.ticker.components.isNetworkOnline
|
||||
import com.github.premnirmal.ticker.network.SimpleSubscriber
|
||||
import com.github.premnirmal.ticker.network.SuggestionApi
|
||||
import com.github.premnirmal.ticker.network.data.Suggestions.Suggestion
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.github.premnirmal.ticker.ui
|
||||
|
||||
import android.graphics.Canvas
|
||||
import com.github.mikephil.charting.components.AxisBase
|
||||
import com.github.mikephil.charting.components.XAxis
|
||||
import com.github.mikephil.charting.formatter.IAxisValueFormatter
|
||||
import com.github.mikephil.charting.renderer.XAxisRenderer
|
||||
import com.github.mikephil.charting.utils.MPPointF
|
||||
import com.github.mikephil.charting.utils.Transformer
|
||||
import com.github.mikephil.charting.utils.Utils
|
||||
import com.github.mikephil.charting.utils.ViewPortHandler
|
||||
import com.github.premnirmal.ticker.AppPreferences
|
||||
import org.threeten.bp.LocalDate
|
||||
|
||||
class DateAxisFormatter : IAxisValueFormatter {
|
||||
|
||||
override fun getFormattedValue(value: Float, axis: AxisBase): String {
|
||||
val date = LocalDate.ofEpochDay(value.toLong())
|
||||
return date.format(AppPreferences.AXIS_DATE_FORMATTER)
|
||||
}
|
||||
}
|
||||
|
||||
class ValueAxisFormatter : IAxisValueFormatter {
|
||||
|
||||
override fun getFormattedValue(value: Float,
|
||||
axis: AxisBase): String = "$${AppPreferences.DECIMAL_FORMAT.format(value)}"
|
||||
}
|
||||
|
||||
class MultilineXAxisRenderer(viewPortHandler: ViewPortHandler?, xAxis: XAxis?,
|
||||
trans: Transformer?) : XAxisRenderer(viewPortHandler, xAxis, trans) {
|
||||
|
||||
override fun drawLabel(c: Canvas, formattedLabel: String, x: Float, y: Float, anchor: MPPointF,
|
||||
angleDegrees: Float) {
|
||||
val lines = formattedLabel.split("-")
|
||||
for (i in 0 until lines.size) {
|
||||
val vOffset = i * mAxisLabelPaint.textSize
|
||||
Utils.drawXAxisValue(c, lines[i], x, y + vOffset, mAxisLabelPaint, anchor, angleDegrees)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.github.premnirmal.ticker.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.widget.TextView
|
||||
import com.github.mikephil.charting.components.MarkerView
|
||||
import com.github.mikephil.charting.data.Entry
|
||||
import com.github.mikephil.charting.highlight.Highlight
|
||||
import com.github.mikephil.charting.utils.MPPointF
|
||||
import com.github.premnirmal.ticker.AppPreferences
|
||||
import com.github.premnirmal.ticker.AppPreferences.Companion.DATE_FORMATTER
|
||||
import com.github.premnirmal.ticker.network.data.DataPoint
|
||||
import com.github.premnirmal.tickerwidget.R
|
||||
|
||||
class TextMarkerView(context: Context) : MarkerView(context, R.layout.text_marker_layout) {
|
||||
|
||||
private var tvContent: TextView = findViewById(R.id.tvContent)
|
||||
private val offsetPoint by lazy {
|
||||
MPPointF((-(width / 2)).toFloat(), (-height).toFloat())
|
||||
}
|
||||
|
||||
override fun refreshContent(e: Entry?, highlight: Highlight?) {
|
||||
val dataPoint = e as DataPoint
|
||||
val price = AppPreferences.DECIMAL_FORMAT.format(dataPoint.y)
|
||||
val date = dataPoint.getDate().format(DATE_FORMATTER)
|
||||
tvContent.text = "$${price}\n$date"
|
||||
super.refreshContent(e, highlight)
|
||||
}
|
||||
|
||||
override fun getOffset(): MPPointF = offsetPoint
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:height="24dp"
|
||||
android:viewportHeight="24"
|
||||
android:viewportWidth="24"
|
||||
android:width="24dp">
|
||||
|
||||
<path
|
||||
android:pathData="M0 0h24v24H0z"/>
|
||||
<path
|
||||
android:fillColor="#ffffff"
|
||||
android:pathData="M19 19H5V5h7V3H5c-1.11 0-2 0.9-2 2v14c0 1.1 0.89 2 2 2h14c1.1 0 2-0.9 2-2v-7h-2v7zM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,119 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/graphActivityRoot"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@android:color/black"
|
||||
android:orientation="vertical"
|
||||
>
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="0.88"
|
||||
android:padding="15dp"
|
||||
>
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progress"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
/>
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/graph_holder"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:visibility="gone"
|
||||
>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tickerName"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:layout_margin="3dp"
|
||||
android:textSize="20dp"
|
||||
tools:text="GOOG"
|
||||
style="@style/BoldTextView"
|
||||
/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/desc"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/tickerName"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:layout_margin="3dp"
|
||||
android:textSize="14dp"
|
||||
tools:text="GOOG"
|
||||
style="@style/BoldTextView"
|
||||
/>
|
||||
|
||||
<com.github.mikephil.charting.charts.LineChart
|
||||
android:id="@+id/graphView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
/>
|
||||
|
||||
</RelativeLayout>
|
||||
</FrameLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="0dp"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_weight="0.12"
|
||||
android:orientation="horizontal"
|
||||
>
|
||||
|
||||
<Button
|
||||
android:id="@+id/one_month"
|
||||
android:layout_width="100dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_margin="5dp"
|
||||
android:gravity="center"
|
||||
android:onClick="updateRange"
|
||||
android:text="@string/one_month"
|
||||
style="@style/ButtonStyle"
|
||||
/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/three_month"
|
||||
android:layout_width="100dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_margin="5dp"
|
||||
android:gravity="center"
|
||||
android:onClick="updateRange"
|
||||
android:text="@string/three_month"
|
||||
style="@style/ButtonStyle"
|
||||
/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/one_year"
|
||||
android:layout_width="100dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_margin="5dp"
|
||||
android:gravity="center"
|
||||
android:onClick="updateRange"
|
||||
android:text="@string/one_year"
|
||||
style="@style/ButtonStyle"
|
||||
/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/max"
|
||||
android:layout_width="100dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_margin="5dp"
|
||||
android:gravity="center"
|
||||
android:onClick="updateRange"
|
||||
android:text="@string/max"
|
||||
style="@style/ButtonStyle"
|
||||
/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
@@ -41,6 +41,39 @@
|
||||
style="@style/BoldTextView"
|
||||
/>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/expand_graph"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="top|end"
|
||||
android:contentDescription="@string/expand"
|
||||
android:onClick="openGraph"
|
||||
android:padding="8dp"
|
||||
android:src="@drawable/ic_expand"
|
||||
/>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/graph_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="200dp"
|
||||
android:padding="8dp"
|
||||
>
|
||||
<com.github.mikephil.charting.charts.LineChart
|
||||
android:id="@+id/graphView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
/>
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progress"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginBottom="20dp"
|
||||
/>
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
@@ -192,10 +225,10 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
android:layout_weight="1"
|
||||
app:center_text="true"
|
||||
app:name="@string/average_price"
|
||||
app:or="horizontal"
|
||||
app:size="@dimen/larger_text"
|
||||
app:center_text="true"
|
||||
/>
|
||||
|
||||
<com.github.premnirmal.ticker.ui.StockFieldView
|
||||
@@ -204,10 +237,10 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
android:layout_weight="1"
|
||||
app:center_text="true"
|
||||
app:name="@string/gain_loss"
|
||||
app:or="horizontal"
|
||||
app:size="@dimen/larger_text"
|
||||
app:center_text="true"
|
||||
/>
|
||||
|
||||
</LinearLayout>
|
||||
@@ -227,12 +260,12 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_margin="8dp"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center_vertical"
|
||||
android:text="@string/recent_news"
|
||||
android:textSize="@dimen/thin_title_text"
|
||||
style="@style/ThinTitleTextView"
|
||||
android:layout_margin="8dp"
|
||||
/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="40dp"
|
||||
>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvContent"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:background="@drawable/translucent_widget_bg"
|
||||
android:gravity="center"
|
||||
android:padding="5dp"
|
||||
android:text=""
|
||||
android:textSize="@dimen/small_text"
|
||||
style="@style/BoldTextView"
|
||||
/>
|
||||
|
||||
</RelativeLayout>
|
||||
@@ -103,6 +103,13 @@
|
||||
<string name="set">Fijado</string>
|
||||
<string name="positions">Posiciones</string>
|
||||
<string name="recent_news">Noticias recientes</string>
|
||||
<string name="no_data">No datos</string>
|
||||
<string name="expand">Expande</string>
|
||||
<string name="one_month">1 Mes</string>
|
||||
<string name="three_month">3 Meses</string>
|
||||
<string name="one_year">1 Año</string>
|
||||
<string name="max">Max</string>
|
||||
<string name="error_loading_graph">Error al cargar los datos del gráfico</string>
|
||||
|
||||
<string name="tutorial">Tutorial</string>
|
||||
<string name="how_to_title">Cómo agregar un widget</string>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<resources>
|
||||
<string-array name="whats_new">
|
||||
<item>Correcciones visuales</item>
|
||||
<item>¡Los gráficos están de vuelta!</item>
|
||||
</string-array>
|
||||
</resources>
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
<dimen name="text_size">15sp</dimen>
|
||||
<dimen name="button_text">13sp</dimen>
|
||||
|
||||
<dimen name="graph_bottom_offset">8dp</dimen>
|
||||
|
||||
<integer name="text_size_small">12</integer>
|
||||
<integer name="text_size_medium">14</integer>
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
<string name="suggestions_endpoint" translatable="false">https://s.yimg.com/aq/</string>
|
||||
<string name="news_endpoint" translatable="false">https://newsapi.org/</string>
|
||||
<string name="news_api_key" translatable="false">05bb573fdcc943f0a34bb0517f05e09b</string>
|
||||
<string name="alpha_vantage_endpoint" translatable="false">https://www.alphavantage.co/</string>
|
||||
<string name="alpha_vantage_api_key" translatable="false">9VQGQDBGD0L1B3IH</string>
|
||||
|
||||
<string name="portfolio">Portfolio</string>
|
||||
<string name="widget_label">StocksWidget</string>
|
||||
@@ -110,6 +112,13 @@
|
||||
<string name="set">Set</string>
|
||||
<string name="positions">Positions</string>
|
||||
<string name="recent_news">Recent News</string>
|
||||
<string name="no_data">No data</string>
|
||||
<string name="expand">Expand</string>
|
||||
<string name="one_month">1 Month</string>
|
||||
<string name="three_month">3 Months</string>
|
||||
<string name="one_year">1 Year</string>
|
||||
<string name="max">Max</string>
|
||||
<string name="error_loading_graph">Error loading graph</string>
|
||||
|
||||
<string name="tutorial">Tutorial</string>
|
||||
<string name="how_to_title">How to add a widget</string>
|
||||
|
||||
@@ -36,6 +36,8 @@
|
||||
|
||||
<style name="NewsFeedActivityTheme" parent="AppTheme"/>
|
||||
|
||||
<style name="GraphActivityTheme" parent="AppTheme"/>
|
||||
|
||||
<style name="TextViewStyle" parent="android:Widget.TextView">
|
||||
<item name="android:textSize">@dimen/text_size</item>
|
||||
<item name="fontPath">fonts/Ubuntu-Regular.ttf</item>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<resources>
|
||||
<string-array name="whats_new">
|
||||
<item>UI fixes</item>
|
||||
<item>Graphs are back! Check out the news feed.</item>
|
||||
</string-array>
|
||||
</resources>
|
||||
|
||||
@@ -2,7 +2,9 @@ package com.github.premnirmal.ticker.mock
|
||||
|
||||
import android.content.Context
|
||||
import com.github.premnirmal.ticker.components.RxBus
|
||||
import com.github.premnirmal.ticker.model.IHistoryProvider
|
||||
import com.github.premnirmal.ticker.model.IStocksProvider
|
||||
import com.github.premnirmal.ticker.network.HistoricalDataApi
|
||||
import com.github.premnirmal.ticker.network.NewsApi
|
||||
import com.github.premnirmal.ticker.network.NewsProvider
|
||||
import com.github.premnirmal.ticker.network.Robindahood
|
||||
@@ -71,4 +73,13 @@ class MockNetworkModule {
|
||||
|
||||
@Provides @Singleton
|
||||
internal fun provideNewsProvider(): NewsProvider = Mocker.provide(NewsProvider::class)
|
||||
|
||||
@Provides @Singleton
|
||||
internal fun provideHistoricalDataApi(context: Context, okHttpClient: OkHttpClient,
|
||||
gson: Gson, converterFactory: GsonConverterFactory,
|
||||
rxJavaFactory: RxJava2CallAdapterFactory): HistoricalDataApi = Mocker.provide(HistoricalDataApi::class)
|
||||
|
||||
@Provides @Singleton
|
||||
internal fun provideHistoricalDataProvider(): IHistoryProvider =
|
||||
Mocker.provide(IHistoryProvider::class)
|
||||
}
|
||||
@@ -1,2 +1,2 @@
|
||||
versionName=2.4.06
|
||||
versionCode=236
|
||||
versionName=2.5.02
|
||||
versionCode=239
|
||||
@@ -14,5 +14,4 @@
|
||||
# org.gradle.parallel=true
|
||||
org.gradle.jvmargs=-Xmx2560m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
|
||||
org.gradle.caching=true
|
||||
android.enableAapt2=false
|
||||
android.enableBuildCache=true
|
||||
Reference in New Issue
Block a user