Add account picker for timetable widget (#314)

Close #281
This commit is contained in:
Rafał Borcz
2019-04-08 00:18:45 +02:00
committed by Mikołaj Pich
parent aa6dcaff94
commit c18877466f
26 changed files with 437 additions and 102 deletions

View File

@ -6,18 +6,16 @@ import javax.inject.Inject
import javax.inject.Singleton
@Singleton
@SuppressLint("ApplySharedPref")
class SharedPrefHelper @Inject constructor(private val sharedPref: SharedPreferences) {
@SuppressLint("ApplySharedPref")
fun putLong(key: String, value: Long, sync: Boolean = false) {
sharedPref.edit().putLong(key, value).apply {
if (sync) commit() else apply()
}
}
fun getLong(key: String, defaultValue: Long): Long {
return sharedPref.getLong(key, defaultValue)
}
fun getLong(key: String, defaultValue: Long) = sharedPref.getLong(key, defaultValue)
fun delete(key: String) {
sharedPref.edit().remove(key).apply()

View File

@ -9,7 +9,8 @@ import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.ui.modules.main.MainModule
import io.github.wulkanowy.ui.modules.message.send.SendMessageActivity
import io.github.wulkanowy.ui.modules.splash.SplashActivity
import io.github.wulkanowy.ui.widgets.timetable.TimetableWidgetProvider
import io.github.wulkanowy.ui.modules.timetablewidget.TimetableWidgetConfigureActivity
import io.github.wulkanowy.ui.modules.timetablewidget.TimetableWidgetProvider
@Module
internal abstract class BuilderModule {
@ -29,6 +30,9 @@ internal abstract class BuilderModule {
@ContributesAndroidInjector
abstract fun bindMessageSendActivity(): SendMessageActivity
@ContributesAndroidInjector
abstract fun bindTimetableWidgetAccountActivity(): TimetableWidgetConfigureActivity
@ContributesAndroidInjector
abstract fun bindTimetableWidgetProvider(): TimetableWidgetProvider
}

View File

@ -7,7 +7,7 @@ import io.github.wulkanowy.data.db.SharedPrefHelper
import io.github.wulkanowy.data.repositories.semester.SemesterRepository
import io.github.wulkanowy.data.repositories.student.StudentRepository
import io.github.wulkanowy.data.repositories.timetable.TimetableRepository
import io.github.wulkanowy.ui.widgets.timetable.TimetableWidgetFactory
import io.github.wulkanowy.ui.modules.timetablewidget.TimetableWidgetFactory
import io.github.wulkanowy.utils.SchedulersProvider
import javax.inject.Inject

View File

@ -0,0 +1,79 @@
package io.github.wulkanowy.ui.modules.timetablewidget
import android.appwidget.AppWidgetManager.ACTION_APPWIDGET_UPDATE
import android.appwidget.AppWidgetManager.EXTRA_APPWIDGET_ID
import android.appwidget.AppWidgetManager.EXTRA_APPWIDGET_IDS
import android.content.Intent
import android.os.Bundle
import android.widget.Toast
import android.widget.Toast.LENGTH_LONG
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.common.SmoothScrollLinearLayoutManager
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import io.github.wulkanowy.R
import io.github.wulkanowy.ui.base.BaseActivity
import io.github.wulkanowy.ui.modules.login.LoginActivity
import io.github.wulkanowy.ui.modules.timetablewidget.TimetableWidgetProvider.Companion.EXTRA_FROM_PROVIDER
import io.github.wulkanowy.utils.setOnItemClickListener
import kotlinx.android.synthetic.main.activity_timetable_widget_configure.*
import javax.inject.Inject
class TimetableWidgetConfigureActivity : BaseActivity(), TimetableWidgetConfigureView {
@Inject
lateinit var configureAdapter: FlexibleAdapter<AbstractFlexibleItem<*>>
@Inject
lateinit var presenter: TimetableWidgetConfigurePresenter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setResult(RESULT_CANCELED)
setContentView(R.layout.activity_timetable_widget_configure)
intent.extras.let {
presenter.onAttachView(this, it?.getInt(EXTRA_APPWIDGET_ID), it?.getBoolean(EXTRA_FROM_PROVIDER))
}
}
override fun initView() {
timetableWidgetConfigureRecycler.apply {
adapter = configureAdapter
layoutManager = SmoothScrollLinearLayoutManager(context)
}
configureAdapter.setOnItemClickListener { presenter.onItemSelect(it) }
}
override fun updateData(data: List<TimetableWidgetConfigureItem>) {
configureAdapter.updateDataSet(data)
}
override fun updateTimetableWidget(widgetId: Int) {
sendBroadcast(Intent(this, TimetableWidgetProvider::class.java)
.apply {
action = ACTION_APPWIDGET_UPDATE
putExtra(EXTRA_APPWIDGET_IDS, intArrayOf(widgetId))
})
}
override fun setSuccessResult(widgetId: Int) {
setResult(RESULT_OK, Intent().apply { putExtra(EXTRA_APPWIDGET_ID, widgetId) })
}
override fun showError(text: String, error: Throwable) {
Toast.makeText(this, text, LENGTH_LONG).show()
}
override fun finishView() {
finish()
}
override fun openLoginView() {
startActivity(LoginActivity.getStartIntent(this))
}
override fun onDestroy() {
super.onDestroy()
presenter.onDetachView()
}
}

View File

@ -0,0 +1,53 @@
package io.github.wulkanowy.ui.modules.timetablewidget
import android.annotation.SuppressLint
import android.view.View
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import eu.davidea.flexibleadapter.items.IFlexible
import eu.davidea.viewholders.FlexibleViewHolder
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Student
import kotlinx.android.extensions.LayoutContainer
import kotlinx.android.synthetic.main.item_account.*
class TimetableWidgetConfigureItem(val student: Student, private val isCurrent: Boolean) :
AbstractFlexibleItem<TimetableWidgetConfigureItem.ViewHolder>() {
override fun getLayoutRes() = R.layout.item_account
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<*>>): ViewHolder {
return ViewHolder(view, adapter)
}
@SuppressLint("SetTextI18n")
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<*>>, holder: ViewHolder, position: Int, payloads: MutableList<Any>) {
holder.apply {
accountItemName.text = "${student.studentName} ${student.className}"
accountItemSchool.text = student.schoolName
accountItemImage.setBackgroundResource(if (isCurrent) R.drawable.ic_account_circular_border else 0)
}
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as TimetableWidgetConfigureItem
if (student != other.student) return false
return true
}
override fun hashCode(): Int {
var result = student.hashCode()
result = 31 * result + student.id.toInt()
return result
}
class ViewHolder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter), LayoutContainer {
override val containerView: View
get() = contentView
}
}

View File

@ -0,0 +1,65 @@
package io.github.wulkanowy.ui.modules.timetablewidget
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import io.github.wulkanowy.data.db.SharedPrefHelper
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.repositories.student.StudentRepository
import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler
import io.github.wulkanowy.ui.modules.timetablewidget.TimetableWidgetProvider.Companion.getStudentWidgetKey
import io.github.wulkanowy.utils.SchedulersProvider
import javax.inject.Inject
class TimetableWidgetConfigurePresenter @Inject constructor(
private val errorHandler: ErrorHandler,
private val schedulers: SchedulersProvider,
private val studentRepository: StudentRepository,
private val sharedPref: SharedPrefHelper
) : BasePresenter<TimetableWidgetConfigureView>(errorHandler) {
private var appWidgetId: Int? = null
private var isFromProvider = false
fun onAttachView(view: TimetableWidgetConfigureView, appWidgetId: Int?, isFromProvider: Boolean?) {
super.onAttachView(view)
this.appWidgetId = appWidgetId
this.isFromProvider = isFromProvider ?: false
view.initView()
loadData()
}
fun onItemSelect(item: AbstractFlexibleItem<*>) {
if (item is TimetableWidgetConfigureItem) {
registerStudent(item.student)
}
}
private fun loadData() {
disposable.add(studentRepository.getSavedStudents(false)
.map { it to appWidgetId?.let { id -> sharedPref.getLong(getStudentWidgetKey(id), 0) } }
.map { (students, currentStudentId) ->
students.map { student -> TimetableWidgetConfigureItem(student, student.id == currentStudentId) }
}
.subscribeOn(schedulers.backgroundThread)
.observeOn(schedulers.mainThread)
.subscribe({
when {
it.isEmpty() -> view?.openLoginView()
it.size == 1 && !isFromProvider -> registerStudent(it.single().student)
else -> view?.updateData(it)
}
}, { errorHandler.dispatch(it) }))
}
private fun registerStudent(student: Student) {
appWidgetId?.also {
sharedPref.putLong(getStudentWidgetKey(it), student.id)
view?.apply {
updateTimetableWidget(it)
setSuccessResult(it)
}
}
view?.finishView()
}
}

View File

@ -0,0 +1,18 @@
package io.github.wulkanowy.ui.modules.timetablewidget
import io.github.wulkanowy.ui.base.BaseView
interface TimetableWidgetConfigureView : BaseView {
fun initView()
fun updateData(data: List<TimetableWidgetConfigureItem>)
fun updateTimetableWidget(widgetId: Int)
fun setSuccessResult(widgetId: Int)
fun finishView()
fun openLoginView()
}

View File

@ -1,5 +1,6 @@
package io.github.wulkanowy.ui.widgets.timetable
package io.github.wulkanowy.ui.modules.timetablewidget
import android.appwidget.AppWidgetManager.EXTRA_APPWIDGET_ID
import android.content.Context
import android.content.Intent
import android.graphics.Paint.ANTI_ALIAS_FLAG
@ -15,9 +16,11 @@ import io.github.wulkanowy.data.db.entities.Timetable
import io.github.wulkanowy.data.repositories.semester.SemesterRepository
import io.github.wulkanowy.data.repositories.student.StudentRepository
import io.github.wulkanowy.data.repositories.timetable.TimetableRepository
import io.github.wulkanowy.ui.modules.timetablewidget.TimetableWidgetProvider.Companion.getDateWidgetKey
import io.github.wulkanowy.ui.modules.timetablewidget.TimetableWidgetProvider.Companion.getStudentWidgetKey
import io.github.wulkanowy.utils.SchedulersProvider
import io.github.wulkanowy.utils.toFormattedString
import io.reactivex.Single
import io.reactivex.Maybe
import org.threeten.bp.LocalDate
import timber.log.Timber
@ -48,28 +51,37 @@ class TimetableWidgetFactory(
override fun onDestroy() {}
override fun onDataSetChanged() {
intent?.action?.let { LocalDate.ofEpochDay(sharedPref.getLong(it, 0)) }
?.let { date ->
try {
lessons = studentRepository.isStudentSaved()
.flatMap { isSaved ->
if (isSaved) {
studentRepository.getCurrentStudent()
.flatMap { semesterRepository.getCurrentSemester(it) }
.flatMap { timetableRepository.getTimetable(it, date, date) }
} else Single.just(emptyList())
}
.map { item -> item.sortedBy { it.number } }
.subscribeOn(schedulers.backgroundThread)
.blockingGet()
} catch (e: Exception) {
Timber.e(e, "An error has occurred while downloading data for the widget")
}
intent?.extras?.getInt(EXTRA_APPWIDGET_ID)?.let { appWidgetId ->
val date = LocalDate.ofEpochDay(sharedPref.getLong(getDateWidgetKey(appWidgetId), 0))
val studentId = sharedPref.getLong(getStudentWidgetKey(appWidgetId), 0)
lessons = try {
studentRepository.isStudentSaved()
.filter { true }
.flatMap { studentRepository.getSavedStudents().toMaybe() }
.flatMap {
if (studentId == 0L) throw IllegalArgumentException("Student id is 0")
it.singleOrNull { student -> student.id == studentId }
.let { student ->
if (student != null) Maybe.just(student)
else Maybe.empty()
}
}
.flatMap { semesterRepository.getCurrentSemester(it).toMaybe() }
.flatMap { timetableRepository.getTimetable(it, date, date).toMaybe() }
.map { item -> item.sortedBy { it.number } }
.subscribeOn(schedulers.backgroundThread)
.blockingGet(emptyList())
} catch (e: Exception) {
Timber.e(e, "An error has occurred in timetable widget factory")
emptyList()
}
}
}
override fun getViewAt(position: Int): RemoteViews? {
if (position == INVALID_POSITION || lessons.getOrNull(position) === null) return null
if (position == INVALID_POSITION || lessons.getOrNull(position) == null) return null
return RemoteViews(context.packageName, R.layout.item_widget_timetable).apply {
lessons[position].let {

View File

@ -1,4 +1,4 @@
package io.github.wulkanowy.ui.widgets.timetable
package io.github.wulkanowy.ui.modules.timetablewidget
import android.app.PendingIntent
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
@ -10,21 +10,28 @@ import android.appwidget.AppWidgetManager.EXTRA_APPWIDGET_IDS
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
import android.widget.RemoteViews
import dagger.android.AndroidInjection
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.SharedPrefHelper
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.repositories.student.StudentRepository
import io.github.wulkanowy.services.widgets.TimetableWidgetService
import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.ui.modules.main.MainActivity.Companion.EXTRA_START_MENU_INDEX
import io.github.wulkanowy.utils.FirebaseAnalyticsHelper
import io.github.wulkanowy.utils.SchedulersProvider
import io.github.wulkanowy.utils.nextOrSameSchoolDay
import io.github.wulkanowy.utils.nextSchoolDay
import io.github.wulkanowy.utils.previousSchoolDay
import io.github.wulkanowy.utils.shortcutWeekDayName
import io.github.wulkanowy.utils.toFormattedString
import io.github.wulkanowy.utils.weekDayName
import io.reactivex.Maybe
import org.threeten.bp.LocalDate
import org.threeten.bp.LocalDate.now
import timber.log.Timber
import javax.inject.Inject
class TimetableWidgetProvider : BroadcastReceiver() {
@ -32,13 +39,21 @@ class TimetableWidgetProvider : BroadcastReceiver() {
@Inject
lateinit var appWidgetManager: AppWidgetManager
@Inject
lateinit var studentRepository: StudentRepository
@Inject
lateinit var sharedPref: SharedPrefHelper
@Inject
lateinit var schedulers: SchedulersProvider
@Inject
lateinit var analytics: FirebaseAnalyticsHelper
companion object {
const val EXTRA_FROM_PROVIDER = "extraFromProvider"
const val EXTRA_TOGGLED_WIDGET_ID = "extraToggledWidget"
const val EXTRA_BUTTON_TYPE = "extraButtonType"
@ -49,7 +64,9 @@ class TimetableWidgetProvider : BroadcastReceiver() {
const val BUTTON_RESET = "buttonReset"
fun createWidgetKey(appWidgetId: Int) = "timetable_widget_$appWidgetId"
fun getDateWidgetKey(appWidgetId: Int) = "timetable_widget_date_$appWidgetId"
fun getStudentWidgetKey(appWidgetId: Int) = "timetable_widget_student_$appWidgetId"
}
override fun onReceive(context: Context, intent: Intent) {
@ -63,12 +80,14 @@ class TimetableWidgetProvider : BroadcastReceiver() {
private fun onUpdate(context: Context, intent: Intent) {
if (intent.getStringExtra(EXTRA_BUTTON_TYPE) === null) {
intent.getIntArrayExtra(EXTRA_APPWIDGET_IDS)?.forEach { appWidgetId ->
updateWidget(context, appWidgetId, now().nextOrSameSchoolDay)
val student = getStudent(sharedPref.getLong(getStudentWidgetKey(appWidgetId), 0), appWidgetId)
updateWidget(context, appWidgetId, now().nextOrSameSchoolDay, student)
}
} else {
val buttonType = intent.getStringExtra(EXTRA_BUTTON_TYPE)
val toggledWidgetId = intent.getIntExtra(EXTRA_TOGGLED_WIDGET_ID, 0)
val savedDate = LocalDate.ofEpochDay(sharedPref.getLong(createWidgetKey(toggledWidgetId), 0))
val student = getStudent(sharedPref.getLong(getStudentWidgetKey(toggledWidgetId), 0), toggledWidgetId)
val savedDate = LocalDate.ofEpochDay(sharedPref.getLong(getDateWidgetKey(toggledWidgetId), 0))
val date = when (buttonType) {
BUTTON_RESET -> now().nextOrSameSchoolDay
BUTTON_NEXT -> savedDate.nextSchoolDay
@ -76,35 +95,46 @@ class TimetableWidgetProvider : BroadcastReceiver() {
else -> now().nextOrSameSchoolDay
}
if (!buttonType.isNullOrBlank()) analytics.logEvent("changed_timetable_widget_day", "button" to buttonType)
updateWidget(context, toggledWidgetId, date)
updateWidget(context, toggledWidgetId, date, student)
}
}
private fun onDelete(intent: Intent) {
intent.getIntExtra(EXTRA_APPWIDGET_ID, 0).let {
if (it != 0) sharedPref.delete(createWidgetKey(it))
if (it != 0) {
sharedPref.apply {
delete(getStudentWidgetKey(it))
delete(getDateWidgetKey(it))
}
}
}
}
private fun updateWidget(context: Context, appWidgetId: Int, date: LocalDate) {
private fun updateWidget(context: Context, appWidgetId: Int, date: LocalDate, student: Student?) {
RemoteViews(context.packageName, R.layout.widget_timetable).apply {
setEmptyView(R.id.timetableWidgetList, R.id.timetableWidgetEmpty)
setTextViewText(R.id.timetableWidgetDay, date.weekDayName.capitalize())
setTextViewText(R.id.timetableWidgetDate, date.toFormattedString())
setTextViewText(R.id.timetableWidgetDate, "${date.shortcutWeekDayName.capitalize()} ${date.toFormattedString()}")
setTextViewText(R.id.timetableWidgetName, student?.studentName ?: context.getString(R.string.all_no_data))
setRemoteAdapter(R.id.timetableWidgetList, Intent(context, TimetableWidgetService::class.java)
.apply { action = createWidgetKey(appWidgetId) })
.apply { putExtra(EXTRA_APPWIDGET_ID, appWidgetId) })
setOnClickPendingIntent(R.id.timetableWidgetNext, createNavIntent(context, appWidgetId, appWidgetId, BUTTON_NEXT))
setOnClickPendingIntent(R.id.timetableWidgetPrev, createNavIntent(context, -appWidgetId, appWidgetId, BUTTON_PREV))
createNavIntent(context, Int.MAX_VALUE - appWidgetId, appWidgetId, BUTTON_RESET).also {
createNavIntent(context, Int.MAX_VALUE - appWidgetId, appWidgetId, BUTTON_RESET).let {
setOnClickPendingIntent(R.id.timetableWidgetDate, it)
setOnClickPendingIntent(R.id.timetableWidgetDay, it)
setOnClickPendingIntent(R.id.timetableWidgetName, it)
}
setOnClickPendingIntent(R.id.timetableWidgetAccount, PendingIntent.getActivity(context, -Int.MAX_VALUE + appWidgetId,
Intent(context, TimetableWidgetConfigureActivity::class.java).apply {
addFlags(FLAG_ACTIVITY_NEW_TASK or FLAG_ACTIVITY_CLEAR_TASK)
putExtra(EXTRA_APPWIDGET_ID, appWidgetId)
putExtra(EXTRA_FROM_PROVIDER, true)
}, FLAG_UPDATE_CURRENT))
setPendingIntentTemplate(R.id.timetableWidgetList,
PendingIntent.getActivity(context, 1, MainActivity.getStartIntent(context).apply {
putExtra(EXTRA_START_MENU_INDEX, 3)
}, FLAG_UPDATE_CURRENT))
}.also {
sharedPref.putLong(createWidgetKey(appWidgetId), date.toEpochDay(), true)
sharedPref.putLong(getDateWidgetKey(appWidgetId), date.toEpochDay(), true)
appWidgetManager.apply {
notifyAppWidgetViewDataChanged(appWidgetId, R.id.timetableWidgetList)
updateAppWidget(appWidgetId, it)
@ -120,4 +150,29 @@ class TimetableWidgetProvider : BroadcastReceiver() {
putExtra(EXTRA_TOGGLED_WIDGET_ID, appWidgetId)
}, FLAG_UPDATE_CURRENT)
}
private fun getStudent(id: Long, appWidgetId: Int): Student? {
return try {
studentRepository.isStudentSaved()
.filter { true }
.flatMap { studentRepository.getSavedStudents(false).toMaybe() }
.flatMap { students ->
students.singleOrNull { student -> student.id == id }
.let { student ->
if (student != null) {
Maybe.just(student)
} else {
studentRepository.getCurrentStudent(false)
.toMaybe()
.doOnSuccess { sharedPref.putLong(getStudentWidgetKey(appWidgetId), it.id) }
}
}
}
.subscribeOn(schedulers.backgroundThread)
.blockingGet()
} catch (e: Exception) {
Timber.e(e, "An error has occurred in timetable widget provider")
null
}
}
}

View File

@ -99,6 +99,9 @@ inline val LocalDate.previousOrSameSchoolDay: LocalDate
inline val LocalDate.weekDayName: String
get() = this.format(ofPattern("EEEE", Locale.getDefault()))
inline val LocalDate.shortcutWeekDayName: String
get() = this.format(ofPattern("EEE", Locale.getDefault()))
inline val LocalDate.monday: LocalDate
get() = this.with(MONDAY)