Quote preview and search enhancement (#104)

* WIP - quote preview

* Adding add-remove functionality from search and from detail

* add missing resources

* update quote detail

* vcode ++

* refresh UI

* vcode++

* nav bar divider

* nav bar color
This commit is contained in:
Prem Nirmal
2019-12-05 10:06:12 -05:00
committed by GitHub
parent ae8ee13b81
commit 8b811792da
38 changed files with 828 additions and 382 deletions
+1 -1
View File
@@ -56,7 +56,7 @@
android:theme="@style/SearchTheme"
android:windowSoftInputMode="adjustResize" />
<activity
android:name="com.github.premnirmal.ticker.news.NewsFeedActivity"
android:name="com.github.premnirmal.ticker.news.QuoteDetailActivity"
android:label="@string/news_feed"
android:theme="@style/NewsFeedActivityTheme" />
<activity
@@ -19,6 +19,9 @@ abstract class BaseGraphActivity : BaseActivity() {
protected var dataPoints: List<DataPoint>? = null
protected lateinit var quote: Quote
protected val isQuoteInitialized: Boolean
get() = this::quote.isInitialized
protected fun setupGraphView() {
val graphView: LineChart = findViewById(R.id.graphView)
graphView.isDoubleTapToZoomEnabled = false
@@ -20,9 +20,8 @@ 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.network.data.Quote
import com.github.premnirmal.ticker.news.GraphActivity
import com.github.premnirmal.ticker.news.NewsFeedActivity
import com.github.premnirmal.ticker.news.QuoteDetailActivity
import com.github.premnirmal.ticker.portfolio.AddPositionActivity
import com.github.premnirmal.ticker.portfolio.PortfolioFragment
import com.github.premnirmal.ticker.portfolio.StocksAdapter
@@ -55,7 +54,7 @@ interface AppComponent {
fun inject(splashActivity: SplashActivity)
fun inject(newsFeedActivity: NewsFeedActivity)
fun inject(newsFeedActivity: QuoteDetailActivity)
fun inject(graphActivity: GraphActivity)
@@ -100,6 +100,7 @@ class HomeFragment : BaseFragment(), ChildFragment, PortfolioFragment.Parent {
private fun updateHeader() {
tabs.visibility = if (widgetDataProvider.hasWidget()) View.VISIBLE else View.INVISIBLE
adapter.notifyDataSetChanged()
subtitle.text = subtitleText
if (!widgetDataProvider.hasWidget()) {
fab_settings.show()
@@ -3,6 +3,7 @@ package com.github.premnirmal.ticker.model
import com.github.premnirmal.ticker.network.data.Holding
import com.github.premnirmal.ticker.network.data.Position
import com.github.premnirmal.ticker.network.data.Quote
import kotlinx.coroutines.Deferred
/**
* Created by premnirmal on 2/28/16.
@@ -29,6 +30,8 @@ interface IStocksProvider {
fun getStock(ticker: String): Quote?
suspend fun fetchStock(ticker: String): Quote?
fun removeStock(ticker: String): Collection<String>
fun removeStocks(tickers: Collection<String>)
@@ -17,7 +17,9 @@ import com.github.premnirmal.ticker.network.data.Position
import com.github.premnirmal.ticker.network.data.Quote
import com.github.premnirmal.ticker.widget.WidgetDataProvider
import com.github.premnirmal.tickerwidget.R
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.threeten.bp.DayOfWeek
@@ -128,8 +130,8 @@ class StocksProvider : IStocksProvider {
get() = alarmScheduler.msToNextAlarm(lastFetched)
private fun scheduleUpdateWithMs(
msToNextAlarm: Long,
refresh: Boolean = false
msToNextAlarm: Long,
refresh: Boolean = false
) {
val updateTime = alarmScheduler.scheduleUpdate(msToNextAlarm, context)
nextFetch = updateTime.toInstant()
@@ -173,7 +175,9 @@ class StocksProvider : IStocksProvider {
override suspend fun fetch(): FetchResult<List<Quote>> = withContext(Dispatchers.IO) {
if (tickerList.isEmpty()) {
bus.send(ErrorEvent(context.getString(R.string.no_symbols_in_portfolio)))
return@withContext FetchResult<List<Quote>>(_error = FetchException("No symbols in portfolio"))
return@withContext FetchResult<List<Quote>>(
_error = FetchException("No symbols in portfolio")
)
} else {
val result = try {
appPreferences.setRefreshing(true)
@@ -227,6 +231,7 @@ class StocksProvider : IStocksProvider {
quote.symbol = ticker
quoteList[ticker] = quote
save()
bus.send(RefreshEvent())
ApplicationScope.launch {
fetch()
}
@@ -240,12 +245,12 @@ class StocksProvider : IStocksProvider {
override fun getPosition(ticker: String): Position? = positionList[ticker]
override fun addPosition(
ticker: String,
shares: Float,
price: Float
ticker: String,
shares: Float,
price: Float
): Holding {
synchronized(quoteList) {
val quote = getStock(ticker)
val quote = quoteList[ticker]
var position = getPosition(ticker)
if (position == null) {
position = Position(ticker, ArrayList())
@@ -263,12 +268,12 @@ class StocksProvider : IStocksProvider {
}
override fun removePosition(
ticker: String,
holding: Holding
ticker: String,
holding: Holding
) {
synchronized(positionList) {
val position = getPosition(ticker)
val quote = getStock(ticker)
val quote = quoteList[ticker]
position?.remove(holding)
quote?.position = position
save()
@@ -295,7 +300,7 @@ class StocksProvider : IStocksProvider {
quoteList.remove(ticker)
positionList.remove(ticker)
save()
scheduleUpdate()
scheduleUpdate(true)
return tickerList
}
}
@@ -308,7 +313,13 @@ class StocksProvider : IStocksProvider {
positionList.remove(it)
}
save()
scheduleUpdate()
scheduleUpdate(true)
}
}
override suspend fun fetchStock(ticker: String): Quote? = withContext(Dispatchers.IO) {
return@withContext quoteList[ticker] ?: run {
api.getStock(ticker)
}
}
@@ -6,7 +6,7 @@ import com.github.premnirmal.ticker.model.FetchException
import com.github.premnirmal.ticker.model.FetchResult
import com.github.premnirmal.ticker.network.data.IQuoteNet
import com.github.premnirmal.ticker.network.data.Quote
import com.github.premnirmal.ticker.network.data.Suggestions.Suggestion
import com.github.premnirmal.ticker.network.data.SuggestionsNet.SuggestionNet
import com.google.gson.Gson
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@@ -36,13 +36,13 @@ class StocksApi {
suggestionApi.getSuggestions(query)
.resultSet?.result
} catch (e: Exception) {
return@withContext FetchResult<List<Suggestion>>(_error = FetchException("Error fetching", e))
return@withContext FetchResult<List<SuggestionNet>>(_error = FetchException("Error fetching", e))
}
val suggestionList = suggestions?.let { ArrayList(it) } ?: ArrayList()
if (suggestionList.isEmpty()) {
suggestionList.add(0, Suggestion(query))
suggestionList.add(0, SuggestionNet(query))
}
return@withContext FetchResult<List<Suggestion>>(_data = suggestionList)
return@withContext FetchResult<List<SuggestionNet>>(_data = suggestionList)
}
/**
@@ -61,6 +61,15 @@ class StocksApi {
.toOrderedQuoteList(tickerList)
}
suspend fun getStock(ticker: String) = withContext(Dispatchers.IO) {
val quoteNets = try {
robindahood.getStocks(ticker)
} catch (ex: HttpException) {
getStocksYahoo(listOf(ticker))
}
return@withContext Quote.fromQuoteNet(quoteNets.first())
}
private suspend fun getStocksYahoo(tickerList: List<String>) = withContext(Dispatchers.IO) {
val query = tickerList.joinToString(",")
val quoteNets = yahooFinance.getStocks(query).quoteResponse!!.result
@@ -1,6 +1,6 @@
package com.github.premnirmal.ticker.network
import com.github.premnirmal.ticker.network.data.Suggestions
import com.github.premnirmal.ticker.network.data.SuggestionsNet
import com.google.gson.Gson
import okhttp3.ResponseBody
import java.io.IOException
@@ -9,14 +9,14 @@ import java.util.regex.Pattern
/**
* Created by premnirmal on 3/3/16.
*/
internal class StupidYahooWrapConverter(gson: Gson) : BaseConverter<Suggestions>(gson) {
internal class StupidYahooWrapConverter(gson: Gson) : BaseConverter<SuggestionsNet>(gson) {
override fun convert(value: ResponseBody?): Suggestions? {
override fun convert(value: ResponseBody?): SuggestionsNet? {
try {
val bodyString = value?.let { getString(it.byteStream()) }
val m = PATTERN_RESPONSE.matcher(bodyString)
if (m.find()) {
return gson.fromJson(m.group(1), Suggestions::class.java)
return gson.fromJson(m.group(1), SuggestionsNet::class.java)
}
throw error("Invalid response")
} catch (e: IOException) {
@@ -1,6 +1,6 @@
package com.github.premnirmal.ticker.network
import com.github.premnirmal.ticker.network.data.Suggestions
import com.github.premnirmal.ticker.network.data.SuggestionsNet
import retrofit2.http.GET
import retrofit2.http.Query
@@ -10,6 +10,6 @@ import retrofit2.http.Query
interface SuggestionApi {
@GET("autoc?callback=YAHOO.Finance.SymbolSuggest.ssCallback&region=US&lang=en-US")
suspend fun getSuggestions(@Query("query") query: String): Suggestions
suspend fun getSuggestions(@Query("query") query: String): SuggestionsNet
}
@@ -1,12 +1,15 @@
package com.github.premnirmal.ticker.network.data
import android.os.Parcel
import android.os.Parcelable
import android.os.Parcelable.Creator
import com.github.premnirmal.ticker.AppPreferences
import java.text.Format
/**
* Created by premnirmal on 3/30/17.
*/
data class Quote(var symbol: String = "") : Comparable<Quote> {
data class Quote(var symbol: String = "") : Parcelable, Comparable<Quote> {
companion object {
fun fromQuoteNet(quoteNet: IQuoteNet): Quote {
@@ -20,6 +23,17 @@ data class Quote(var symbol: String = "") : Comparable<Quote> {
quote.description = quoteNet.description ?: ""
return quote
}
@JvmField
val CREATOR = object: Creator<Quote> {
override fun createFromParcel(parcel: Parcel): Quote {
return Quote(parcel)
}
override fun newArray(size: Int): Array<Quote?> {
return arrayOfNulls(size)
}
}
}
var name: String = ""
@@ -129,4 +143,38 @@ data class Quote(var symbol: String = "") : Comparable<Quote> {
override operator fun compareTo(other: Quote): Int =
other.changeInPercent.compareTo(changeInPercent)
constructor(parcel: Parcel) : this(parcel.readString()!!) {
name = parcel.readString()!!
lastTradePrice = parcel.readFloat()
changeInPercent = parcel.readFloat()
change = parcel.readFloat()
stockExchange = parcel.readString()!!
currency = parcel.readString()!!
description = parcel.readString()!!
isPosition = parcel.readByte() != 0.toByte()
positionPrice = parcel.readFloat()
positionShares = parcel.readFloat()
}
override fun writeToParcel(
parcel: Parcel,
flags: Int
) {
parcel.writeString(symbol)
parcel.writeString(name)
parcel.writeFloat(lastTradePrice)
parcel.writeFloat(changeInPercent)
parcel.writeFloat(change)
parcel.writeString(stockExchange)
parcel.writeString(currency)
parcel.writeString(description)
parcel.writeByte(if (isPosition) 1 else 0)
parcel.writeFloat(positionPrice)
parcel.writeFloat(positionShares)
}
override fun describeContents(): Int {
return 0
}
}
@@ -0,0 +1,26 @@
package com.github.premnirmal.ticker.network.data
import com.github.premnirmal.ticker.network.data.SuggestionsNet.SuggestionNet
data class Suggestion(
var symbol: String = ""
) {
var name: String = ""
var exch: String = ""
var type: String = ""
var exchDisp: String = ""
var typeDisp: String = ""
var exists: Boolean = false
companion object {
fun fromSuggestionNet(suggestionNet: SuggestionNet): Suggestion {
val suggestion = Suggestion(suggestionNet.symbol)
suggestion.name = suggestionNet.name
suggestion.exch = suggestionNet.exch
suggestion.type = suggestionNet.type
suggestion.exchDisp = suggestionNet.exchDisp
suggestion.typeDisp = suggestionNet.typeDisp
return suggestion
}
}
}
@@ -5,14 +5,14 @@ import com.google.gson.annotations.SerializedName
/**
* Created by premnirmal on 3/30/17.
*/
data class Suggestions(@SerializedName("ResultSet") var resultSet: QueryResults? = null) {
data class SuggestionsNet(@SerializedName("ResultSet") var resultSet: QueryResultsNet? = null) {
data class QueryResults(
data class QueryResultsNet(
@SerializedName("Query") var query: String = "") {
@SerializedName("Result") var result: List<Suggestion>? = null
@SerializedName("Result") var result: List<SuggestionNet>? = null
}
data class Suggestion(
data class SuggestionNet(
@SerializedName("symbol") var symbol: String = ""
) {
@SerializedName("name") var name: String = ""
@@ -1,279 +0,0 @@
package com.github.premnirmal.ticker.news
import android.content.Intent
import android.content.res.Configuration
import android.net.Uri
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import android.widget.TextView
import androidx.browser.customtabs.CustomTabsIntent
import androidx.lifecycle.lifecycleScope
import com.github.mikephil.charting.charts.LineChart
import com.github.premnirmal.ticker.BrowserFallback
import com.github.premnirmal.ticker.analytics.ClickEvent
import com.github.premnirmal.ticker.analytics.GeneralEvent
import com.github.premnirmal.ticker.base.BaseGraphActivity
import com.github.premnirmal.ticker.components.InAppMessage
import com.github.premnirmal.ticker.components.Injector
import com.github.premnirmal.ticker.isNetworkOnline
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.data.NewsArticle
import com.github.premnirmal.ticker.network.data.Quote
import com.github.premnirmal.ticker.portfolio.AddPositionActivity
import com.github.premnirmal.ticker.toBitmap
import com.github.premnirmal.tickerwidget.R
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.day_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 kotlinx.coroutines.launch
import saschpe.android.customtabs.CustomTabsHelper
import javax.inject.Inject
class NewsFeedActivity : BaseGraphActivity() {
companion object {
const val TICKER = "TICKER"
private const val DATA_POINTS = "DATA_POINTS"
}
@Inject internal lateinit var stocksProvider: IStocksProvider
@Inject internal lateinit var newsProvider: NewsProvider
@Inject internal lateinit var historyProvider: IHistoryProvider
private lateinit var ticker: String
override val simpleName: String = "NewsFeedActivity"
override fun onCreate(savedInstanceState: Bundle?) {
Injector.appComponent.inject(this)
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_news_feed)
toolbar.setNavigationOnClickListener {
finish()
}
if (resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) {
graph_container.layoutParams.height = (resources.displayMetrics.widthPixels * 0.5625f).toInt()
graph_container.requestLayout()
}
setupGraphView()
val q: Quote?
if (intent.hasExtra(TICKER) && intent.getStringExtra(TICKER) != null) {
ticker = intent.getStringExtra(TICKER)
q = stocksProvider.getStock(ticker)
if (q == null) {
showErrorAndFinish()
return
}
} else {
ticker = ""
showErrorAndFinish()
return
}
quote = q
toolbar.title = ticker
tickerName.text = quote.name
lastTradePrice.text = quote.priceString()
val changeText = "${quote.changeStringWithSign()} ( ${quote.changePercentStringWithSign()})"
change.text = changeText
if (quote.change > 0 || quote.changeInPercent >= 0) {
change.setTextColor(resources.getColor(R.color.positive_green))
lastTradePrice.setTextColor(resources.getColor(R.color.positive_green))
} else {
change.setTextColor(resources.getColor(R.color.negative_red))
lastTradePrice.setTextColor(resources.getColor(R.color.negative_red))
}
exchange.text = quote.stockExchange
numShares.text = quote.numSharesString()
equityValue.text = quote.holdingsString()
description.text = quote.description
edit_positions.setOnClickListener {
analytics.trackClickEvent(ClickEvent("EditPositionClick")
.addProperty("Instrument", ticker))
val intent = Intent(this, AddPositionActivity::class.java)
intent.putExtra(AddPositionActivity.TICKER, quote.symbol)
startActivity(intent)
}
savedInstanceState?.let {
dataPoints = it.getParcelableArrayList(DATA_POINTS)
}
}
private fun fetchData() {
if (isNetworkOnline()) {
lifecycleScope.launch {
val result = historyProvider.getHistoricalDataShort(quote.symbol)
if (result.wasSuccessful) {
dataPoints = result.data
loadGraph()
} else {
progress.visibility = View.GONE
graphView.setNoDataText(getString(R.string.graph_fetch_failed))
InAppMessage.showMessage(this@NewsFeedActivity, R.string.graph_fetch_failed, error = true)
}
}
} else {
progress.visibility = View.GONE
graphView.setNoDataText(getString(R.string.no_network_message))
}
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
dataPoints?.let {
outState.putParcelableArrayList(DATA_POINTS, ArrayList(it))
}
}
private fun setUpArticles(articles: List<NewsArticle>) {
if (articles.isEmpty()) {
news_container.visibility = View.GONE
} else {
news_container.visibility = View.VISIBLE
for (newsArticle in articles) {
val layout = LayoutInflater.from(this)
.inflate(R.layout.item_news, news_container, false)
val sourceView: TextView = layout.findViewById(R.id.news_source)
val titleView: TextView = layout.findViewById(R.id.news_title)
val subTitleView: TextView = layout.findViewById(R.id.news_subtitle)
val dateView: TextView = layout.findViewById(R.id.published_at)
titleView.text = newsArticle.title
subTitleView.text = newsArticle.description
dateView.text = newsArticle.dateString()
sourceView.text = newsArticle.sourceName()
val params = LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
params.bottomMargin = resources.getDimensionPixelSize(R.dimen.activity_vertical_margin)
news_container.addView(layout, params)
layout.tag = newsArticle
layout.setOnClickListener {
val article = it.tag as NewsArticle
val customTabsIntent = CustomTabsIntent.Builder()
.addDefaultShareMenuItem()
.setToolbarColor(this.resources.getColor(R.color.colorPrimary))
.setShowTitle(true)
.setCloseButtonIcon(resources.getDrawable(R.drawable.ic_close).toBitmap())
.build()
analytics.trackClickEvent(ClickEvent("ArticleClick")
.addProperty("Instrument", ticker)
.addProperty("ArticleTitle", newsArticle.title.orEmpty())
.addProperty("ArticleSource", newsArticle.sourceName())
.addProperty("ArticleUrl", newsArticle.url.orEmpty()))
CustomTabsHelper.addKeepAliveExtra(this, customTabsIntent.intent)
CustomTabsHelper.openCustomTab(
this, customTabsIntent, Uri.parse(article.url.orEmpty()),
BrowserFallback()
)
}
}
}
}
override fun onStart() {
super.onStart()
fetch()
}
override fun onResume() {
super.onResume()
numShares.text = quote.numSharesString()
equityValue.text = quote.holdingsString()
if (quote.hasPositions()) {
total_gain_loss.visibility = View.VISIBLE
total_gain_loss.setText("${quote.gainLossString()} (${quote.gainLossPercentString()})")
if (quote.gainLoss() >= 0) {
total_gain_loss.setTextColor(resources.getColor(R.color.positive_green))
} else {
total_gain_loss.setTextColor(resources.getColor(R.color.negative_red))
}
average_price.visibility = View.VISIBLE
average_price.setText(quote.averagePositionPrice())
day_change.visibility = View.VISIBLE
day_change.setText(quote.dayChangeString())
if (quote.change > 0 || quote.changeInPercent >= 0) {
day_change.setTextColor(resources.getColor(R.color.positive_green))
} else {
day_change.setTextColor(resources.getColor(R.color.negative_red))
}
} else {
total_gain_loss.visibility = View.GONE
day_change.visibility = View.GONE
average_price.visibility = View.GONE
}
}
private fun fetch() {
if (!isNetworkOnline()) {
InAppMessage.showMessage(this, R.string.no_network_message, error = true)
}
if (news_container.childCount <= 1) {
fetchNews()
}
if (dataPoints == null) {
fetchData()
} else {
loadGraph()
}
}
private fun fetchNews() {
if (isNetworkOnline()) {
lifecycleScope.launch {
val result = newsProvider.getNews(quote.newsQuery())
if (result.wasSuccessful) {
val articles = result.data
analytics.trackGeneralEvent(GeneralEvent("FetchNews")
.addProperty("Instrument", ticker)
.addProperty("Success", "True"))
setUpArticles(articles)
} else {
news_container.visibility = View.GONE
InAppMessage.showMessage(this@NewsFeedActivity, R.string.news_fetch_failed, error = true)
analytics.trackGeneralEvent(GeneralEvent("FetchNews")
.addProperty("Instrument", ticker)
.addProperty("Success", "False"))
}
}
} else {
news_container.visibility = View.GONE
}
}
override fun onGraphDataAdded(graphView: LineChart) {
progress.visibility = View.GONE
analytics.trackGeneralEvent(GeneralEvent("GraphLoaded"))
}
override fun onNoGraphData(graphView: LineChart) {
progress.visibility = View.GONE
analytics.trackGeneralEvent(GeneralEvent("NoGraphData"))
}
/**
* Called via xml
*/
fun openGraph(v: View) {
analytics.trackClickEvent(ClickEvent("GraphClick")
.addProperty("Instrument", ticker))
val intent = Intent(this, GraphActivity::class.java)
intent.putExtra(GraphActivity.TICKER, ticker)
startActivity(intent)
}
}
@@ -0,0 +1,388 @@
package com.github.premnirmal.ticker.news
import android.app.AlertDialog
import android.content.Intent
import android.content.res.Configuration
import android.net.Uri
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import android.widget.TextView
import androidx.browser.customtabs.CustomTabsIntent
import androidx.lifecycle.lifecycleScope
import com.github.mikephil.charting.charts.LineChart
import com.github.premnirmal.ticker.BrowserFallback
import com.github.premnirmal.ticker.analytics.ClickEvent
import com.github.premnirmal.ticker.analytics.GeneralEvent
import com.github.premnirmal.ticker.base.BaseGraphActivity
import com.github.premnirmal.ticker.components.InAppMessage
import com.github.premnirmal.ticker.components.Injector
import com.github.premnirmal.ticker.isNetworkOnline
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.data.NewsArticle
import com.github.premnirmal.ticker.network.data.Quote
import com.github.premnirmal.ticker.portfolio.AddPositionActivity
import com.github.premnirmal.ticker.showDialog
import com.github.premnirmal.ticker.toBitmap
import com.github.premnirmal.ticker.widget.WidgetDataProvider
import com.github.premnirmal.tickerwidget.R
import com.github.premnirmal.tickerwidget.R.color
import kotlinx.android.synthetic.main.activity_quote_detail.average_price
import kotlinx.android.synthetic.main.activity_quote_detail.change
import kotlinx.android.synthetic.main.activity_quote_detail.day_change
import kotlinx.android.synthetic.main.activity_quote_detail.description
import kotlinx.android.synthetic.main.activity_quote_detail.edit_positions
import kotlinx.android.synthetic.main.activity_quote_detail.equityValue
import kotlinx.android.synthetic.main.activity_quote_detail.exchange
import kotlinx.android.synthetic.main.activity_quote_detail.graphView
import kotlinx.android.synthetic.main.activity_quote_detail.graph_container
import kotlinx.android.synthetic.main.activity_quote_detail.lastTradePrice
import kotlinx.android.synthetic.main.activity_quote_detail.news_container
import kotlinx.android.synthetic.main.activity_quote_detail.numShares
import kotlinx.android.synthetic.main.activity_quote_detail.positions_container
import kotlinx.android.synthetic.main.activity_quote_detail.positions_header
import kotlinx.android.synthetic.main.activity_quote_detail.progress
import kotlinx.android.synthetic.main.activity_quote_detail.tickerName
import kotlinx.android.synthetic.main.activity_quote_detail.toolbar
import kotlinx.android.synthetic.main.activity_quote_detail.total_gain_loss
import kotlinx.coroutines.launch
import saschpe.android.customtabs.CustomTabsHelper
import javax.inject.Inject
class QuoteDetailActivity : BaseGraphActivity() {
companion object {
const val TICKER = "TICKER"
private const val DATA_POINTS = "DATA_POINTS"
private const val QUOTE = "QUOTE"
}
@Inject internal lateinit var stocksProvider: IStocksProvider
@Inject internal lateinit var newsProvider: NewsProvider
@Inject internal lateinit var historyProvider: IHistoryProvider
@Inject internal lateinit var widgetDataProvider: WidgetDataProvider
private lateinit var ticker: String
override val simpleName: String = "NewsFeedActivity"
override fun onCreate(savedInstanceState: Bundle?) {
Injector.appComponent.inject(this)
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_quote_detail)
toolbar.setNavigationOnClickListener {
finish()
}
if (resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) {
graph_container.layoutParams.height = (resources.displayMetrics.widthPixels * 0.5625f).toInt()
graph_container.requestLayout()
}
setupGraphView()
savedInstanceState?.let {
ticker = intent.getStringExtra(TICKER)
dataPoints = it.getParcelableArrayList(DATA_POINTS)
quote = checkNotNull(it.getParcelable(QUOTE))
fetch()
setupUi()
} ?: run {
lifecycleScope.launch {
val q: Quote?
if (intent.hasExtra(TICKER) && intent.getStringExtra(TICKER) != null) {
ticker = intent.getStringExtra(TICKER)
q = stocksProvider.fetchStock(ticker)
if (q == null) {
showErrorAndFinish()
return@launch
}
} else {
ticker = ""
showErrorAndFinish()
return@launch
}
quote = q
fetch()
setupUi()
}
}
}
private fun setupUi() {
toolbar.inflateMenu(R.menu.menu_news_feed)
val isInPortfolio = stocksProvider.hasTicker(ticker)
val addMenuItem = toolbar.menu.findItem(R.id.action_add)
val removeMenuItem = toolbar.menu.findItem(R.id.action_remove)
if (isInPortfolio) {
addMenuItem.isVisible = false
removeMenuItem.isVisible = true
} else {
removeMenuItem.isVisible = false
addMenuItem.isVisible = true
}
toolbar.setOnMenuItemClickListener { menuItem ->
when (menuItem.itemId) {
R.id.action_add -> {
if (widgetDataProvider.hasWidget()) {
val widgetIds = widgetDataProvider.getAppWidgetIds()
if (widgetIds.size > 1) {
val widgets =
widgetIds.map { widgetDataProvider.dataForWidgetId(it) }
.sortedBy { it.widgetName() }
val widgetNames = widgets.map { it.widgetName() }
.toTypedArray()
AlertDialog.Builder(this)
.setTitle(R.string.select_widget)
.setItems(widgetNames) { dialog, which ->
val id = widgets[which].widgetId
addTickerToWidget(ticker, id)
dialog.dismiss()
}
.create()
.show()
} else {
addTickerToWidget(ticker, widgetIds.first())
}
} else {
addTickerToWidget(ticker, WidgetDataProvider.INVALID_WIDGET_ID)
}
updatePositionsUi()
return@setOnMenuItemClickListener true
}
R.id.action_remove -> {
removeMenuItem.isVisible = false
addMenuItem.isVisible = true
stocksProvider.removeStock(ticker)
updatePositionsUi()
return@setOnMenuItemClickListener true
}
}
return@setOnMenuItemClickListener false
}
toolbar.title = ticker
tickerName.text = quote.name
lastTradePrice.text = quote.priceString()
val changeText = "${quote.changeStringWithSign()} ( ${quote.changePercentStringWithSign()})"
change.text = changeText
if (quote.change > 0 || quote.changeInPercent >= 0) {
change.setTextColor(resources.getColor(color.positive_green))
lastTradePrice.setTextColor(resources.getColor(color.positive_green))
} else {
change.setTextColor(resources.getColor(color.negative_red))
lastTradePrice.setTextColor(resources.getColor(color.negative_red))
}
exchange.text = quote.stockExchange
updatePositionsUi()
edit_positions.setOnClickListener {
analytics.trackClickEvent(
ClickEvent("EditPositionClick")
.addProperty("Instrument", ticker)
)
val intent = Intent(this, AddPositionActivity::class.java)
intent.putExtra(AddPositionActivity.TICKER, quote.symbol)
startActivity(intent)
}
}
private fun fetchData() {
if (isNetworkOnline()) {
lifecycleScope.launch {
val result = historyProvider.getHistoricalDataShort(quote.symbol)
if (result.wasSuccessful) {
dataPoints = result.data
loadGraph()
} else {
progress.visibility = View.GONE
graphView.setNoDataText(getString(R.string.graph_fetch_failed))
InAppMessage.showMessage(this@QuoteDetailActivity, R.string.graph_fetch_failed, error = true)
}
}
} else {
progress.visibility = View.GONE
graphView.setNoDataText(getString(R.string.no_network_message))
}
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
dataPoints?.let {
outState.putParcelableArrayList(DATA_POINTS, ArrayList(it))
}
if (isQuoteInitialized) {
outState.putParcelable(QUOTE, quote)
}
}
private fun setUpArticles(articles: List<NewsArticle>) {
if (articles.isEmpty()) {
news_container.visibility = View.GONE
} else {
news_container.visibility = View.VISIBLE
for (newsArticle in articles) {
val layout = LayoutInflater.from(this)
.inflate(R.layout.item_news, news_container, false)
val sourceView: TextView = layout.findViewById(R.id.news_source)
val titleView: TextView = layout.findViewById(R.id.news_title)
val subTitleView: TextView = layout.findViewById(R.id.news_subtitle)
val dateView: TextView = layout.findViewById(R.id.published_at)
titleView.text = newsArticle.title
subTitleView.text = newsArticle.description
dateView.text = newsArticle.dateString()
sourceView.text = newsArticle.sourceName()
val params = LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
params.bottomMargin = resources.getDimensionPixelSize(R.dimen.activity_vertical_margin)
news_container.addView(layout, params)
layout.tag = newsArticle
layout.setOnClickListener {
val article = it.tag as NewsArticle
val customTabsIntent = CustomTabsIntent.Builder()
.addDefaultShareMenuItem()
.setToolbarColor(this.resources.getColor(R.color.colorPrimary))
.setShowTitle(true)
.setCloseButtonIcon(resources.getDrawable(R.drawable.ic_close).toBitmap())
.build()
analytics.trackClickEvent(
ClickEvent("ArticleClick")
.addProperty("Instrument", ticker)
.addProperty("ArticleTitle", newsArticle.title.orEmpty())
.addProperty("ArticleSource", newsArticle.sourceName())
.addProperty("ArticleUrl", newsArticle.url.orEmpty())
)
CustomTabsHelper.addKeepAliveExtra(this, customTabsIntent.intent)
CustomTabsHelper.openCustomTab(
this, customTabsIntent, Uri.parse(article.url.orEmpty()),
BrowserFallback()
)
}
}
}
}
override fun onResume() {
super.onResume()
if (isQuoteInitialized) {
updatePositionsUi()
}
}
private fun updatePositionsUi() {
val isInPortfolio = stocksProvider.hasTicker(ticker)
if (isInPortfolio) {
positions_container.visibility = View.VISIBLE
positions_header.visibility = View.VISIBLE
numShares.text = quote.numSharesString()
equityValue.text = quote.holdingsString()
description.text = quote.description
if (quote.hasPositions()) {
total_gain_loss.visibility = View.VISIBLE
total_gain_loss.setText("${quote.gainLossString()} (${quote.gainLossPercentString()})")
if (quote.gainLoss() >= 0) {
total_gain_loss.setTextColor(resources.getColor(color.positive_green))
} else {
total_gain_loss.setTextColor(resources.getColor(color.negative_red))
}
average_price.visibility = View.VISIBLE
average_price.setText(quote.averagePositionPrice())
day_change.visibility = View.VISIBLE
day_change.setText(quote.dayChangeString())
if (quote.change > 0 || quote.changeInPercent >= 0) {
day_change.setTextColor(resources.getColor(color.positive_green))
} else {
day_change.setTextColor(resources.getColor(color.negative_red))
}
} else {
total_gain_loss.visibility = View.GONE
day_change.visibility = View.GONE
average_price.visibility = View.GONE
}
} else {
positions_container.visibility = View.GONE
positions_header.visibility = View.GONE
}
}
private fun fetch() {
if (!isNetworkOnline()) {
InAppMessage.showMessage(this, R.string.no_network_message, error = true)
}
if (news_container.childCount <= 1) {
fetchNews()
}
if (dataPoints == null) {
fetchData()
} else {
loadGraph()
}
}
private fun fetchNews() {
if (isNetworkOnline()) {
lifecycleScope.launch {
val result = newsProvider.getNews(quote.newsQuery())
if (result.wasSuccessful) {
val articles = result.data
analytics.trackGeneralEvent(
GeneralEvent("FetchNews")
.addProperty("Instrument", ticker)
.addProperty("Success", "True")
)
setUpArticles(articles)
} else {
news_container.visibility = View.GONE
InAppMessage.showMessage(this@QuoteDetailActivity, R.string.news_fetch_failed, error = true)
analytics.trackGeneralEvent(
GeneralEvent("FetchNews")
.addProperty("Instrument", ticker)
.addProperty("Success", "False")
)
}
}
} else {
news_container.visibility = View.GONE
}
}
override fun onGraphDataAdded(graphView: LineChart) {
progress.visibility = View.GONE
analytics.trackGeneralEvent(GeneralEvent("GraphLoaded"))
}
override fun onNoGraphData(graphView: LineChart) {
progress.visibility = View.GONE
analytics.trackGeneralEvent(GeneralEvent("NoGraphData"))
}
/**
* Called via xml
*/
fun openGraph(v: View) {
analytics.trackClickEvent(
ClickEvent("GraphClick")
.addProperty("Instrument", ticker)
)
val intent = Intent(this, GraphActivity::class.java)
intent.putExtra(GraphActivity.TICKER, ticker)
startActivity(intent)
}
private fun addTickerToWidget(
ticker: String,
widgetId: Int
) {
val widgetData = widgetDataProvider.dataForWidgetId(widgetId)
if (!widgetData.hasTicker(ticker)) {
widgetData.addTicker(ticker)
widgetDataProvider.broadcastUpdateWidget(widgetId)
val addMenuItem = toolbar.menu.findItem(R.id.action_add)
val removeMenuItem = toolbar.menu.findItem(R.id.action_remove)
addMenuItem.isVisible = false
removeMenuItem.isVisible = true
InAppMessage.showMessage(this, getString(R.string.added_to_list, ticker))
} else {
showDialog(getString(R.string.already_in_portfolio, ticker))
}
}
}
@@ -18,7 +18,7 @@ import com.github.premnirmal.ticker.components.InAppMessage
import com.github.premnirmal.ticker.components.Injector
import com.github.premnirmal.ticker.events.RefreshEvent
import com.github.premnirmal.ticker.network.data.Quote
import com.github.premnirmal.ticker.news.NewsFeedActivity
import com.github.premnirmal.ticker.news.QuoteDetailActivity
import com.github.premnirmal.ticker.portfolio.StocksAdapter.QuoteClickListener
import com.github.premnirmal.ticker.portfolio.drag_drop.OnStartDragListener
import com.github.premnirmal.ticker.portfolio.drag_drop.SimpleItemTouchHelperCallback
@@ -29,7 +29,6 @@ import kotlinx.android.synthetic.main.portfolio_fragment.stockList
import kotlinx.android.synthetic.main.portfolio_fragment.view_flipper
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
/**
@@ -96,8 +95,8 @@ class PortfolioFragment : BaseFragment(), QuoteClickListener, OnStartDragListene
) {
analytics.trackClickEvent(ClickEvent("InstrumentClick")
.addProperty("Instrument", quote.symbol))
val intent = Intent(view.context, NewsFeedActivity::class.java)
intent.putExtra(NewsFeedActivity.TICKER, quote.symbol)
val intent = Intent(view.context, QuoteDetailActivity::class.java)
intent.putExtra(QuoteDetailActivity.TICKER, quote.symbol)
startActivity(intent)
}
@@ -107,7 +106,7 @@ class PortfolioFragment : BaseFragment(), QuoteClickListener, OnStartDragListene
position: Int
) {
val popupWindow = PopupMenu(view.context, view)
popupWindow.menuInflater.inflate(R.menu.stock_menu, popupWindow.menu)
popupWindow.menuInflater.inflate(R.menu.menu_portfolio, popupWindow.menu)
val moveToWidgetItem = popupWindow.menu.findItem(R.id.move_to_widget)
moveToWidgetItem.isEnabled = widgetId !=
AppWidgetManager.INVALID_APPWIDGET_ID && holder.widgetDataProvider.getAppWidgetIds().size >
@@ -2,6 +2,7 @@ package com.github.premnirmal.ticker.portfolio.search
import android.app.AlertDialog
import android.appwidget.AppWidgetManager
import android.content.Intent
import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
@@ -19,7 +20,8 @@ import com.github.premnirmal.ticker.getStatusBarHeight
import com.github.premnirmal.ticker.home.ChildFragment
import com.github.premnirmal.ticker.model.IStocksProvider
import com.github.premnirmal.ticker.network.StocksApi
import com.github.premnirmal.ticker.network.data.Suggestions.Suggestion
import com.github.premnirmal.ticker.network.data.Suggestion
import com.github.premnirmal.ticker.news.QuoteDetailActivity
import com.github.premnirmal.ticker.portfolio.search.SuggestionsAdapter.SuggestionClickListener
import com.github.premnirmal.ticker.showDialog
import com.github.premnirmal.ticker.widget.WidgetDataProvider
@@ -27,7 +29,9 @@ import com.github.premnirmal.tickerwidget.R
import kotlinx.android.synthetic.main.fragment_search.fake_status_bar
import kotlinx.android.synthetic.main.fragment_search.recycler_view
import kotlinx.android.synthetic.main.fragment_search.search_view
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
import javax.inject.Inject
@@ -90,6 +94,19 @@ class SearchFragment : BaseFragment(), ChildFragment, SuggestionClickListener, T
super.onSaveInstanceState(outState)
}
override fun onResume() {
super.onResume()
lifecycleScope.launch {
withContext(Dispatchers.Default) {
adapter.getData()
.forEach {
it.exists = stocksProvider.hasTicker(it.symbol)
}
}
adapter.notifyDataSetChanged()
}
}
private fun addTickerToWidget(
ticker: String,
widgetId: Int
@@ -131,7 +148,14 @@ class SearchFragment : BaseFragment(), ChildFragment, SuggestionClickListener, T
lifecycleScope.launch {
val suggestions = stocksApi.getSuggestions(query)
if (suggestions.wasSuccessful) {
adapter.setData(suggestions.data)
val data = withContext(Dispatchers.Default) {
suggestions.data.map {
val sug = Suggestion.fromSuggestionNet(it)
sug.exists = stocksProvider.hasTicker(sug.symbol)
sug
}
}
adapter.setData(data)
} else {
Timber.w(suggestions.error)
InAppMessage.showMessage(requireActivity(), R.string.error_fetching_suggestions, error = true)
@@ -143,34 +167,63 @@ class SearchFragment : BaseFragment(), ChildFragment, SuggestionClickListener, T
}
}
override fun onSuggestionClick(suggestion: Suggestion) {
override fun onSuggestionClick(suggestion: Suggestion): Boolean {
val ticker = suggestion.symbol
if (selectedWidgetId > 0) {
addTickerToWidget(ticker, selectedWidgetId)
return
return true
}
if (widgetDataProvider.hasWidget()) {
val widgetIds = widgetDataProvider.getAppWidgetIds()
if (widgetIds.size > 1) {
val widgets =
widgetIds.map { widgetDataProvider.dataForWidgetId(it) }
.sortedBy { it.widgetName() }
val widgetNames = widgets.map { it.widgetName() }
.toTypedArray()
AlertDialog.Builder(context!!)
.setTitle(R.string.select_widget)
.setItems(widgetNames) { dialog, which ->
val id = widgets[which].widgetId
addTickerToWidget(ticker, id)
dialog.dismiss()
}
.create()
.show()
val intent = Intent(requireContext(), QuoteDetailActivity::class.java)
intent.putExtra(QuoteDetailActivity.TICKER, ticker)
startActivity(intent)
return false
}
override fun onAddRemoveClick(suggestion: Suggestion): Boolean {
val ticker = suggestion.symbol
if (!suggestion.exists) {
if (selectedWidgetId > 0) {
addTickerToWidget(ticker, selectedWidgetId)
return true
} else {
addTickerToWidget(ticker, widgetIds.first())
if (widgetDataProvider.hasWidget()) {
val widgetIds = widgetDataProvider.getAppWidgetIds()
if (widgetIds.size > 1) {
val widgets =
widgetIds.map { widgetDataProvider.dataForWidgetId(it) }
.sortedBy { it.widgetName() }
val widgetNames = widgets.map { it.widgetName() }
.toTypedArray()
AlertDialog.Builder(requireActivity())
.setTitle(R.string.select_widget)
.setItems(widgetNames) { dialog, which ->
val id = widgets[which].widgetId
addTickerToWidget(ticker, id)
suggestion.exists = !suggestion.exists
adapter.notifyDataSetChanged()
dialog.dismiss()
}
.create()
.show()
return false
} else {
addTickerToWidget(ticker, widgetIds.first())
return true
}
} else {
addTickerToWidget(ticker, WidgetDataProvider.INVALID_WIDGET_ID)
return true
}
}
} else {
addTickerToWidget(ticker, WidgetDataProvider.INVALID_WIDGET_ID)
if (selectedWidgetId > 0) {
val widgetData = widgetDataProvider.dataForWidgetId(selectedWidgetId)
widgetData.removeStock(ticker)
} else {
val widgetData = widgetDataProvider.widgetDataWithStock(ticker)
widgetData?.removeStock(ticker)
}
return true
}
}
@@ -4,8 +4,10 @@ import android.text.Html
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import com.github.premnirmal.ticker.network.data.Suggestions.Suggestion
import androidx.recyclerview.widget.RecyclerView
import com.github.premnirmal.ticker.network.data.Suggestion
import com.github.premnirmal.ticker.portfolio.search.SuggestionsAdapter.SuggestionVH
import com.github.premnirmal.tickerwidget.R
import java.util.ArrayList
@@ -14,12 +16,13 @@ import java.util.ArrayList
* Created by premnirmal on 2/26/16.
*/
internal class SuggestionsAdapter(private val suggestionClickListener: SuggestionClickListener) :
androidx.recyclerview.widget.RecyclerView.Adapter<SuggestionVH>() {
RecyclerView.Adapter<SuggestionVH>() {
private val suggestions: MutableList<Suggestion> = ArrayList()
interface SuggestionClickListener {
fun onSuggestionClick(suggestion: Suggestion)
fun onSuggestionClick(suggestion: Suggestion): Boolean
fun onAddRemoveClick(suggestion: Suggestion): Boolean
}
override fun getItemCount(): Int {
@@ -36,6 +39,8 @@ internal class SuggestionsAdapter(private val suggestionClickListener: Suggestio
notifyDataSetChanged()
}
fun getData() = suggestions as List<Suggestion>
override fun getItemId(position: Int): Long {
return position.toLong()
}
@@ -61,15 +66,28 @@ internal class SuggestionsAdapter(private val suggestionClickListener: Suggestio
class SuggestionVH(
itemView: View,
private val suggestionClickListener: SuggestionClickListener
) :
androidx.recyclerview.widget.RecyclerView.ViewHolder(itemView) {
) : RecyclerView.ViewHolder(itemView) {
private val textView: TextView = itemView as TextView
private val textView: TextView = itemView.findViewById(R.id.suggestion_text)
private val addRemoveImage: ImageView = itemView.findViewById(R.id.add_remove_image)
private var suggestion: Suggestion? = null
init {
itemView.setOnClickListener { _ ->
suggestion?.let { suggestionClickListener.onSuggestionClick(it) }
textView.setOnClickListener { _ ->
suggestion?.let {
if (suggestionClickListener.onSuggestionClick(it)) {
it.exists = !it.exists
update(it)
}
}
}
addRemoveImage.setOnClickListener {
suggestion?.let {
if(suggestionClickListener.onAddRemoveClick(it)) {
it.exists = !it.exists
update(it)
}
}
}
}
@@ -87,6 +105,9 @@ internal class SuggestionsAdapter(private val suggestionClickListener: Suggestio
}
val name = builder.toString()
textView.text = name
addRemoveImage.setImageResource(
if (item.exists) R.drawable.ic_remove_circle else R.drawable.ic_add_circle
)
}
}
}
@@ -101,7 +101,7 @@ class SettingsFragment : PreferenceFragmentCompat(), ChildFragment,
savedInstanceState: Bundle?
) {
super.onViewCreated(view, savedInstanceState)
(toolbar.layoutParams as MarginLayoutParams).topMargin = context!!.getStatusBarHeight()
(toolbar.layoutParams as MarginLayoutParams).topMargin = requireContext().getStatusBarHeight()
listView.addItemDecoration(DividerItemDecoration(activity, DividerItemDecoration.VERTICAL))
listView.isVerticalScrollBarEnabled = false
setupSimplePreferencesScreen()
@@ -175,9 +175,9 @@ class SettingsFragment : PreferenceFragmentCompat(), ChildFragment,
.setShowTitle(true)
.setCloseButtonIcon(resources.getDrawable(R.drawable.ic_close).toBitmap())
.build()
CustomTabsHelper.addKeepAliveExtra(context!!, customTabsIntent.intent)
CustomTabsHelper.addKeepAliveExtra(requireContext(), customTabsIntent.intent)
CustomTabsHelper.openCustomTab(
context!!, customTabsIntent,
requireContext(), customTabsIntent,
Uri.parse(resources.getString(R.string.privacy_policy_url)),
BrowserFallback()
)
@@ -207,7 +207,7 @@ class SettingsFragment : PreferenceFragmentCompat(), ChildFragment,
preferences.edit()
.clear()
.apply()
val filesDir = context!!.filesDir
val filesDir = requireContext().filesDir
filesDir.listFiles()
.forEach { file ->
file?.delete()
@@ -482,7 +482,7 @@ class SettingsFragment : PreferenceFragmentCompat(), ChildFragment,
return
}
val uri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
FileProvider.getUriForFile(context!!, BuildConfig.APPLICATION_ID + ".provider", file)
FileProvider.getUriForFile(requireContext(), BuildConfig.APPLICATION_ID + ".provider", file)
} else {
Uri.fromFile(file)
}
@@ -32,7 +32,7 @@ class WidgetSettingsActivity : BaseActivity(), WidgetSettingsFragment.Parent {
}
if (savedInstanceState == null) {
supportFragmentManager.beginTransaction()
.add(R.id.fragment_container, WidgetSettingsFragment.newInstance(widgetId))
.add(R.id.fragment_container, WidgetSettingsFragment.newInstance(widgetId, true))
.commit()
}
}
@@ -10,8 +10,10 @@ import android.view.View.OnClickListener
import android.view.ViewGroup
import androidx.annotation.ArrayRes
import com.github.premnirmal.ticker.base.BaseFragment
import com.github.premnirmal.ticker.components.AsyncBus
import com.github.premnirmal.ticker.components.InAppMessage
import com.github.premnirmal.ticker.components.Injector
import com.github.premnirmal.ticker.events.RefreshEvent
import com.github.premnirmal.ticker.showDialog
import com.github.premnirmal.ticker.ui.SettingsTextView
import com.github.premnirmal.ticker.widget.WidgetData
@@ -33,12 +35,14 @@ import javax.inject.Inject
class WidgetSettingsFragment : BaseFragment(), OnClickListener {
companion object {
const val ARG_WIDGET_ID = AppWidgetManager.EXTRA_APPWIDGET_ID
private const val ARG_WIDGET_ID = AppWidgetManager.EXTRA_APPWIDGET_ID
private const val ARG_SHOW_ADD_STOCKS = "show_add_stocks"
fun newInstance(widgetId: Int): WidgetSettingsFragment {
fun newInstance(widgetId: Int, showAddStocks: Boolean = true): WidgetSettingsFragment {
val fragment = WidgetSettingsFragment()
val args = Bundle()
args.putInt(ARG_WIDGET_ID, widgetId)
args.putBoolean(ARG_SHOW_ADD_STOCKS, showAddStocks)
fragment.arguments = args
return fragment
}
@@ -49,6 +53,8 @@ class WidgetSettingsFragment : BaseFragment(), OnClickListener {
}
@Inject internal lateinit var widgetDataProvider: WidgetDataProvider
@Inject internal lateinit var bus: AsyncBus
private var showAddStocks = true
private val parent: Parent
get() = activity as Parent
internal var widgetId = 0
@@ -58,6 +64,7 @@ class WidgetSettingsFragment : BaseFragment(), OnClickListener {
super.onCreate(savedInstanceState)
Injector.appComponent.inject(this)
widgetId = arguments!!.getInt(ARG_WIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID)
showAddStocks = arguments!!.getBoolean(ARG_SHOW_ADD_STOCKS, true)
}
override fun onCreateView(
@@ -73,6 +80,7 @@ class WidgetSettingsFragment : BaseFragment(), OnClickListener {
savedInstanceState: Bundle?
) {
super.onViewCreated(view, savedInstanceState)
setting_add_stock.visibility = if (showAddStocks) View.VISIBLE else View.GONE
val widgetData = widgetDataProvider.dataForWidgetId(widgetId)
setWidgetNameSetting(widgetData)
setLayoutTypeSetting(widgetData)
@@ -103,6 +111,7 @@ class WidgetSettingsFragment : BaseFragment(), OnClickListener {
setWidgetNameSetting(widgetData)
v.setIsEditable(false)
v.setOnClickListener(this)
bus.send(RefreshEvent())
InAppMessage.showMessage(activity!!, R.string.widget_name_updated)
}
}
@@ -6,7 +6,6 @@ import android.appwidget.AppWidgetProvider
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Build.VERSION
import android.os.Build.VERSION_CODES
import android.os.Bundle
@@ -18,6 +17,7 @@ import com.github.premnirmal.ticker.home.ParanormalActivity
import com.github.premnirmal.ticker.model.IStocksProvider
import com.github.premnirmal.tickerwidget.R
import com.github.premnirmal.tickerwidget.R.layout
import timber.log.Timber
import javax.inject.Inject
/**
@@ -97,9 +97,14 @@ class StockWidget : AppWidgetProvider() {
appWidgetIds: IntArray?
) {
super.onDeleted(context, appWidgetIds)
appWidgetIds?.let {
it.forEach { widgetId ->
widgetDataProvider.widgetRemoved(widgetId)
appWidgetIds?.let { id ->
id.forEach { widgetId ->
val removed = widgetDataProvider.removeWidget(widgetId)
removed?.getTickers()?.forEach { ticker ->
if (!widgetDataProvider.containsTicker(ticker)) {
stocksProvider.removeStock(ticker)
}
}
}
}
}
@@ -5,6 +5,7 @@ import android.content.ComponentName
import android.content.Context
import android.content.Intent
import com.github.premnirmal.ticker.components.Injector
import com.github.premnirmal.ticker.model.IStocksProvider
import com.github.premnirmal.ticker.network.data.Quote
import javax.inject.Inject
import javax.inject.Singleton
@@ -56,10 +57,11 @@ class WidgetDataProvider {
}
}
fun widgetRemoved(widgetId: Int) {
synchronized(widgets) {
fun removeWidget(widgetId: Int): WidgetData? {
return synchronized(widgets) {
val removed = widgets.remove(widgetId)
removed?.onWidgetRemoved()
return@synchronized removed
}
}
@@ -83,6 +85,9 @@ class WidgetDataProvider {
fun containsTicker(ticker: String): Boolean = widgets.any { it.value.hasTicker(ticker) }
fun widgetDataWithStock(ticker: String) =
widgets.filter { it.value.hasTicker(ticker) }.values.firstOrNull()
fun moveQuoteToDifferentWidget(
oldWidgetId: Int,
quote: Quote,
@@ -81,7 +81,7 @@ class WidgetsFragment : BaseFragment(), ChildFragment, OnItemSelectedListener {
}
private fun setWidgetFragment(widgetId: Int) {
val fragment = WidgetSettingsFragment.newInstance(widgetId)
val fragment = WidgetSettingsFragment.newInstance(widgetId, false)
childFragmentManager.beginTransaction()
.replace(R.id.child_fragment_container, fragment)
.commit()
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFF"
android:pathData="M13,7h-2v4L7,11v2h4v4h2v-4h4v-2h-4L13,7zM12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8z"/>
</vector>
+9
View File
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFF"
android:pathData="M19,13H5v-2h14v2z"/>
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFF"
android:pathData="M7,11v2h10v-2L7,11zM12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8z"/>
</vector>
@@ -17,11 +17,11 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:background="@color/card_bg"
android:background="@color/bottom_bar_bg"
app:elevation="4dp"
app:itemIconTint="@drawable/bottom_nav_icon_tint"
app:itemTextColor="@drawable/bottom_nav_icon_tint"
app:menu="@menu/bottom_nav_items"
app:menu="@menu/menu_bottom_nav"
/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
@@ -125,6 +125,7 @@
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/positions_header"
android:layout_marginBottom="8dp"
android:layout_marginEnd="16dp"
android:layout_marginStart="16dp"
@@ -154,6 +155,7 @@
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/positions_container"
android:layout_margin="8dp"
android:background="@drawable/card_bg"
android:orientation="vertical"
+40 -15
View File
@@ -1,19 +1,44 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView
xmlns:android="http://schemas.android.com/apk/res/android"
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/suggestion_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:background="?attr/selectableItemBackground"
android:gravity="center_vertical"
android:lines="1"
android:maxLines="1"
android:paddingBottom="10dp"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="10dp"
android:textSize="@dimen/text_size"
tools:text="GOOG - Alphabet Inc (NASDAQ)"
/>
android:orientation="horizontal"
android:paddingLeft="6dp"
android:paddingStart="6dp"
android:paddingRight="6dp"
android:paddingEnd="6dp"
>
<TextView
android:id="@+id/suggestion_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:paddingLeft="6dp"
android:paddingStart="6dp"
android:paddingRight="6dp"
android:paddingEnd="6dp"
android:paddingTop="10dp"
android:paddingBottom="10dp"
android:layout_weight="1"
android:background="?attr/selectableItemBackground"
android:gravity="center_vertical"
android:lines="1"
android:maxLines="1"
android:textSize="@dimen/text_size"
tools:text="GOOG - Alphabet Inc (NASDAQ)"
/>
<ImageView
android:id="@+id/add_remove_image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingLeft="6dp"
android:paddingStart="6dp"
android:paddingRight="6dp"
android:paddingEnd="6dp"
android:paddingTop="10dp"
android:paddingBottom="10dp"
android:layout_gravity="center"
tools:src="@drawable/ic_add_circle"
/>
</LinearLayout>
+15
View File
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_add"
android:icon="@drawable/ic_add_circle"
android:title="@string/add"
app:showAsAction="always" />
<item
android:id="@+id/action_remove"
android:icon="@drawable/ic_remove_circle"
android:title="@string/remove"
app:showAsAction="always" />
</menu>
+5 -1
View File
@@ -21,7 +21,7 @@
<!-- v21 specific -->
<item name="android:statusBarColor">@color/transparent</item>
<item name="android:navigationBarColor">@color/default_dark_bg</item>
<item name="android:navigationBarColor">@color/nav_bar_bg</item>
</style>
<!-- Custom Preference Theme -->
@@ -35,6 +35,10 @@
<item name="layout">@layout/fragment_settings</item>
</style>
<style name="ParanormalActivityTheme" parent="AppTheme">
<item name="android:navigationBarColor">@color/nav_bar_bg_2</item>
</style>
<style name="WidgetSettingsTheme" parent="AppTheme">
<item name="android:statusBarColor">@color/color_primary</item>
</style>
+5 -1
View File
@@ -24,7 +24,7 @@
<!-- v21 specific -->
<item name="android:statusBarColor">@color/transparent</item>
<item name="android:navigationBarColor">@color/default_dark_bg</item>
<item name="android:navigationBarColor">@color/nav_bar_bg</item>
</style>
<!-- Custom Preference Theme -->
<style name="AppPreferenceTheme" parent="@style/PreferenceThemeOverlay">
@@ -37,6 +37,10 @@
<item name="layout">@layout/fragment_settings</item>
</style>
<style name="ParanormalActivityTheme" parent="AppTheme">
<item name="android:navigationBarColor">@color/nav_bar_bg_2</item>
</style>
<style name="WidgetSettingsTheme" parent="AppTheme">
<item name="android:statusBarColor">@color/color_primary</item>
</style>
+65
View File
@@ -0,0 +1,65 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="AppTheme" parent="Theme.AppCompat.NoActionBar">
<item name="colorPrimary">@color/color_primary</item>
<item name="colorPrimaryDark">@color/color_primary_dark</item>
<item name="colorAccent">@color/color_accent</item>
<item name="android:colorPrimary">@color/color_primary</item>
<item name="android:colorPrimaryDark">@color/color_primary_dark</item>
<item name="android:windowBackground">@color/default_dark_bg</item>
<item name="android:colorAccent">@color/color_accent</item>
<item name="android:colorSecondary">@color/color_accent</item>
<item name="android:alertDialogTheme">@style/AlertDialog</item>
<item name="android:dialogTheme">@style/AlertDialog</item>
<item name="android:actionBarStyle">@style/ActionBarStyle</item>
<item name="android:textViewStyle">@style/TextViewStyle</item>
<item name="android:buttonStyle">@style/ButtonStyle</item>
<!-- For AppCompat -->
<item name="editTextStyle">@style/EditTextStyle</item>
<item name="buttonStyle">@style/ButtonStyle</item>
<item name="preferenceTheme">@style/AppPreferenceTheme</item>
<!-- v21 specific -->
<item name="android:statusBarColor">@color/transparent</item>
<item name="android:navigationBarColor">@color/nav_bar_bg</item>
<!-- v27 specific -->
<item name="android:navigationBarDividerColor">@color/transparent</item>
</style>
<!-- Custom Preference Theme -->
<style name="AppPreferenceTheme" parent="@style/PreferenceThemeOverlay">
<item name="preferenceFragmentCompatStyle">@style/AppPreferenceFragmentCompatStyle</item>
<item name="preferenceFragmentStyle">@style/AppPreferenceFragmentCompatStyle</item>
</style>
<!-- Custom Style for PreferenceFragmentCompat -->
<style name="AppPreferenceFragmentCompatStyle" parent="@style/PreferenceFragment.Material">
<item name="android:layout">@layout/fragment_settings</item>
<item name="layout">@layout/fragment_settings</item>
</style>
<style name="ParanormalActivityTheme" parent="AppTheme">
<item name="android:navigationBarColor">@color/nav_bar_bg_2</item>
</style>
<style name="WidgetSettingsTheme" parent="AppTheme">
<item name="android:statusBarColor">@color/color_primary</item>
</style>
<style name="SearchTheme" parent="AppTheme">
<item name="android:statusBarColor">@color/color_primary</item>
</style>
<style name="NewsFeedActivityTheme" parent="AppTheme">
<item name="android:statusBarColor">@color/color_primary</item>
</style>
<style name="GraphActivityTheme" parent="AppTheme">
<item name="android:navigationBarColor">@color/black</item>
</style>
<style name="AddPositionTheme" parent="AppTheme">
<item name="android:statusBarColor">@color/color_primary</item>
</style>
</resources>
+3
View File
@@ -18,6 +18,9 @@
<color name="translucent">#98000000</color>
<color name="bg_gradient_center">#752a2a2a</color>
<color name="black_gradient_center">#3f3f3f</color>
<color name="nav_bar_bg">@color/default_dark_bg</color>
<color name="nav_bar_bg_2">@color/card_bg</color>
<color name="bottom_bar_bg">@color/card_bg</color>
<color name="white">#ffffff</color>
<color name="black">#000000</color>
+2 -2
View File
@@ -1,2 +1,2 @@
versionName=3.4.3
versionCode=343
versionName=3.4.8
versionCode=348