Add mobile access managment (#344)

This commit is contained in:
Mikołaj Pich 2019-06-03 00:43:54 +02:00 committed by Rafał Borcz
parent 5c70cd8b8c
commit 28f27db2b5
35 changed files with 2455 additions and 8 deletions

View File

@ -7,7 +7,7 @@ references:
container_config: &container_config
docker:
- image: circleci/android:api-28
- image: circleci/android@sha256:5cdc8626cc6f13efe5ed982cdcdb432b0472f8740fed8743a6461e025ad6cdfc
working_directory: *workspace_root
environment:
environment:
@ -93,6 +93,9 @@ jobs:
<<: *container_config
steps:
- *attach_workspace
- run:
name: Accept licenses
command: yes | sdkmanager --licenses && yes | sdkmanager --update
- run:
name: Setup emulator
command: sdkmanager "system-images;android-19;default;armeabi-v7a" && echo "no" | avdmanager create avd -n test -k "system-images;android-19;default;armeabi-v7a"

View File

@ -85,7 +85,7 @@ play {
}
dependencies {
implementation 'com.github.wulkanowy:api:c84356f'
implementation 'com.github.wulkanowy:api:d08b71b'
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
implementation "androidx.legacy:legacy-support-v4:1.0.0"

File diff suppressed because it is too large Load Diff

View File

@ -25,7 +25,8 @@ abstract class AbstractMigrationTest {
.addMigrations(
Migration12(),
Migration13(),
Migration14()
Migration14(),
Migration15()
)
.build()
// close the database and release any stream resources when the test finishes

View File

@ -132,4 +132,8 @@ internal class RepositoryModule {
@Singleton
@Provides
fun provideRecipientDao(database: AppDatabase) = database.recipientDao
@Singleton
@Provides
fun provideMobileDevicesDao(database: AppDatabase) = database.mobileDeviceDao
}

View File

@ -16,6 +16,7 @@ import io.github.wulkanowy.data.db.dao.GradeSummaryDao
import io.github.wulkanowy.data.db.dao.HomeworkDao
import io.github.wulkanowy.data.db.dao.LuckyNumberDao
import io.github.wulkanowy.data.db.dao.MessagesDao
import io.github.wulkanowy.data.db.dao.MobileDeviceDao
import io.github.wulkanowy.data.db.dao.NoteDao
import io.github.wulkanowy.data.db.dao.RecipientDao
import io.github.wulkanowy.data.db.dao.ReportingUnitDao
@ -33,6 +34,7 @@ import io.github.wulkanowy.data.db.entities.GradeSummary
import io.github.wulkanowy.data.db.entities.Homework
import io.github.wulkanowy.data.db.entities.LuckyNumber
import io.github.wulkanowy.data.db.entities.Message
import io.github.wulkanowy.data.db.entities.MobileDevice
import io.github.wulkanowy.data.db.entities.Note
import io.github.wulkanowy.data.db.entities.Recipient
import io.github.wulkanowy.data.db.entities.ReportingUnit
@ -45,6 +47,7 @@ import io.github.wulkanowy.data.db.migrations.Migration11
import io.github.wulkanowy.data.db.migrations.Migration12
import io.github.wulkanowy.data.db.migrations.Migration13
import io.github.wulkanowy.data.db.migrations.Migration14
import io.github.wulkanowy.data.db.migrations.Migration15
import io.github.wulkanowy.data.db.migrations.Migration2
import io.github.wulkanowy.data.db.migrations.Migration3
import io.github.wulkanowy.data.db.migrations.Migration4
@ -74,7 +77,8 @@ import javax.inject.Singleton
LuckyNumber::class,
CompletedLesson::class,
ReportingUnit::class,
Recipient::class
Recipient::class,
MobileDevice::class
],
version = AppDatabase.VERSION_SCHEMA,
exportSchema = true
@ -83,7 +87,7 @@ import javax.inject.Singleton
abstract class AppDatabase : RoomDatabase() {
companion object {
const val VERSION_SCHEMA = 14
const val VERSION_SCHEMA = 15
fun newInstance(context: Context): AppDatabase {
return Room.databaseBuilder(context, AppDatabase::class.java, "wulkanowy_database")
@ -103,7 +107,8 @@ abstract class AppDatabase : RoomDatabase() {
Migration11(),
Migration12(),
Migration13(),
Migration14()
Migration14(),
Migration15()
)
.build()
}
@ -142,4 +147,6 @@ abstract class AppDatabase : RoomDatabase() {
abstract val reportingUnitDao: ReportingUnitDao
abstract val recipientDao: RecipientDao
abstract val mobileDeviceDao: MobileDeviceDao
}

View File

@ -0,0 +1,21 @@
package io.github.wulkanowy.data.db.dao
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.Query
import io.github.wulkanowy.data.db.entities.MobileDevice
import io.reactivex.Maybe
@Dao
interface MobileDeviceDao {
@Insert
fun insertAll(devices: List<MobileDevice>)
@Delete
fun deleteAll(devices: List<MobileDevice>)
@Query("SELECT * FROM MobileDevices WHERE student_id = :studentId ORDER BY date DESC")
fun loadAll(studentId: Int): Maybe<List<MobileDevice>>
}

View File

@ -0,0 +1,25 @@
package io.github.wulkanowy.data.db.entities
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import org.threeten.bp.LocalDateTime
import java.io.Serializable
@Entity(tableName = "MobileDevices")
data class MobileDevice(
@ColumnInfo(name = "student_id")
val studentId: Int,
@ColumnInfo(name = "device_id")
val deviceId: Int,
val name: String,
val date: LocalDateTime
) : Serializable {
@PrimaryKey(autoGenerate = true)
var id: Long = 0
}

View File

@ -0,0 +1,19 @@
package io.github.wulkanowy.data.db.migrations
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
class Migration15 : Migration(14, 15) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("""
CREATE TABLE IF NOT EXISTS MobileDevices (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
student_id INTEGER NOT NULL,
device_id INTEGER NOT NULL,
name TEXT NOT NULL,
date INTEGER NOT NULL
)
""")
}
}

View File

@ -0,0 +1,12 @@
package io.github.wulkanowy.data.pojos
data class MobileDeviceToken(
val token: String,
val symbol: String,
val pin: String,
val qr: String
)

View File

@ -0,0 +1,25 @@
package io.github.wulkanowy.data.repositories.mobiledevice
import io.github.wulkanowy.data.db.dao.MobileDeviceDao
import io.github.wulkanowy.data.db.entities.MobileDevice
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.reactivex.Maybe
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class MobileDeviceLocal @Inject constructor(private val mobileDb: MobileDeviceDao) {
fun saveDevices(devices: List<MobileDevice>) {
mobileDb.insertAll(devices)
}
fun deleteDevices(devices: List<MobileDevice>) {
mobileDb.deleteAll(devices)
}
fun getDevices(semester: Semester): Maybe<List<MobileDevice>> {
return mobileDb.loadAll(semester.studentId).filter { it.isNotEmpty() }
}
}

View File

@ -0,0 +1,47 @@
package io.github.wulkanowy.data.repositories.mobiledevice
import io.github.wulkanowy.api.Api
import io.github.wulkanowy.data.db.entities.MobileDevice
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.pojos.MobileDeviceToken
import io.github.wulkanowy.utils.toLocalDateTime
import io.reactivex.Single
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class MobileDeviceRemote @Inject constructor(private val api: Api) {
fun getDevices(semester: Semester): Single<List<MobileDevice>> {
return Single.just(api.apply { diaryId = semester.diaryId })
.flatMap { api.getRegisteredDevices() }
.map { devices ->
devices.map {
MobileDevice(
studentId = semester.studentId,
date = it.date.toLocalDateTime(),
deviceId = it.id,
name = it.name
)
}
}
}
fun unregisterDevice(semester: Semester, device: MobileDevice): Single<Boolean> {
return Single.just(api.apply { diaryId = semester.diaryId })
.flatMap { api.unregisterDevice(device.deviceId) }
}
fun getToken(semester: Semester): Single<MobileDeviceToken> {
return Single.just(api.apply { diaryId = semester.diaryId })
.flatMap { api.getToken() }
.map {
MobileDeviceToken(
token = it.token,
symbol = it.symbol,
pin = it.pin,
qr = it.qrCodeImage
)
}
}
}

View File

@ -0,0 +1,44 @@
package io.github.wulkanowy.data.repositories.mobiledevice
import com.github.pwittchen.reactivenetwork.library.rx2.ReactiveNetwork
import com.github.pwittchen.reactivenetwork.library.rx2.internet.observing.InternetObservingSettings
import io.github.wulkanowy.data.db.entities.MobileDevice
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.pojos.MobileDeviceToken
import io.github.wulkanowy.utils.uniqueSubtract
import io.reactivex.Single
import java.net.UnknownHostException
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class MobileDeviceRepository @Inject constructor(
private val settings: InternetObservingSettings,
private val local: MobileDeviceLocal,
private val remote: MobileDeviceRemote
) {
fun getDevices(semester: Semester, forceRefresh: Boolean = false): Single<List<MobileDevice>> {
return local.getDevices(semester).filter { !forceRefresh }
.switchIfEmpty(ReactiveNetwork.checkInternetConnectivity(settings)
.flatMap {
if (it) remote.getDevices(semester)
else Single.error(UnknownHostException())
}.flatMap { new ->
local.getDevices(semester).toSingle(emptyList())
.doOnSuccess { old ->
local.deleteDevices(old uniqueSubtract new)
local.saveDevices(new uniqueSubtract old)
}
}
).flatMap { local.getDevices(semester).toSingle(emptyList()) }
}
fun unregisterDevice(semester: Semester, device: MobileDevice): Single<Boolean> {
return remote.unregisterDevice(semester, device)
}
fun getToken(semester: Semester): Single<MobileDeviceToken> {
return remote.getToken(semester)
}
}

View File

@ -19,6 +19,9 @@ import io.github.wulkanowy.ui.modules.luckynumber.LuckyNumberFragment
import io.github.wulkanowy.ui.modules.message.MessageFragment
import io.github.wulkanowy.ui.modules.message.MessageModule
import io.github.wulkanowy.ui.modules.message.preview.MessagePreviewFragment
import io.github.wulkanowy.ui.modules.mobiledevice.token.MobileDeviceTokenDialog
import io.github.wulkanowy.ui.modules.mobiledevice.MobileDeviceFragment
import io.github.wulkanowy.ui.modules.mobiledevice.MobileDeviceModule
import io.github.wulkanowy.ui.modules.more.MoreFragment
import io.github.wulkanowy.ui.modules.note.NoteFragment
import io.github.wulkanowy.ui.modules.settings.SettingsFragment
@ -97,4 +100,12 @@ abstract class MainModule {
@PerFragment
@ContributesAndroidInjector
abstract fun bindAccountDialog(): AccountDialog
@PerFragment
@ContributesAndroidInjector(modules = [MobileDeviceModule::class])
abstract fun bindMobileDevices(): MobileDeviceFragment
@PerFragment
@ContributesAndroidInjector
abstract fun bindMobileDeviceDialog(): MobileDeviceTokenDialog
}

View File

@ -0,0 +1,10 @@
package io.github.wulkanowy.ui.modules.mobiledevice
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.IFlexible
import io.github.wulkanowy.data.db.entities.MobileDevice
class MobileDeviceAdapter<T : IFlexible<*>> : FlexibleAdapter<T>(null, null, true) {
var onDeviceUnregisterListener: (MobileDevice, position: Int) -> Unit = { _, _ -> }
}

View File

@ -0,0 +1,114 @@
package io.github.wulkanowy.ui.modules.mobiledevice
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.View.GONE
import android.view.View.VISIBLE
import android.view.ViewGroup
import eu.davidea.flexibleadapter.common.FlexibleItemDecoration
import eu.davidea.flexibleadapter.common.SmoothScrollLinearLayoutManager
import eu.davidea.flexibleadapter.helpers.EmptyViewHelper
import eu.davidea.flexibleadapter.helpers.UndoHelper
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import io.github.wulkanowy.R
import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.ui.modules.mobiledevice.token.MobileDeviceTokenDialog
import kotlinx.android.synthetic.main.fragment_mobile_device.*
import javax.inject.Inject
class MobileDeviceFragment : BaseFragment(), MobileDeviceView, MainView.TitledView {
@Inject
lateinit var presenter: MobileDevicePresenter
@Inject
lateinit var devicesAdapter: MobileDeviceAdapter<AbstractFlexibleItem<*>>
companion object {
fun newInstance() = MobileDeviceFragment()
}
override val titleStringId: Int
get() = R.string.mobile_devices_title
override val isViewEmpty: Boolean
get() = devicesAdapter.isEmpty
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_mobile_device, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
messageContainer = mobileDevicesRecycler
presenter.onAttachView(this)
}
override fun initView() {
mobileDevicesRecycler.run {
layoutManager = SmoothScrollLinearLayoutManager(context)
adapter = devicesAdapter
addItemDecoration(FlexibleItemDecoration(context)
.withDefaultDivider()
.withDrawDividerOnLastItem(false)
)
}
EmptyViewHelper.create(devicesAdapter, mobileDevicesEmpty)
mobileDevicesSwipe.setOnRefreshListener { presenter.onSwipeRefresh() }
mobileDeviceAddButton.setOnClickListener { presenter.onRegisterDevice() }
devicesAdapter.run {
isPermanentDelete = false
onDeviceUnregisterListener = { device, position ->
val onActionListener = object : UndoHelper.OnActionListener {
override fun onActionConfirmed(action: Int, event: Int) {
presenter.onUnregister(device)
}
override fun onActionCanceled(action: Int, positions: MutableList<Int>?) {
devicesAdapter.restoreDeletedItems()
}
}
UndoHelper(devicesAdapter, onActionListener)
.withConsecutive(false)
.withAction(UndoHelper.Action.REMOVE)
.start(listOf(position), mobileDevicesRecycler, R.string.mobile_device_removed, R.string.all_undo, 3000)
}
}
}
override fun updateData(data: List<MobileDeviceItem>) {
devicesAdapter.updateDataSet(data)
}
override fun clearData() {
devicesAdapter.clear()
}
override fun hideRefresh() {
mobileDevicesSwipe.isRefreshing = false
}
override fun showProgress(show: Boolean) {
mobileDevicesProgress.visibility = if (show) VISIBLE else GONE
}
override fun enableSwipe(enable: Boolean) {
mobileDevicesSwipe.isEnabled = enable
}
override fun showContent(show: Boolean) {
mobileDevicesRecycler.visibility = if (show) VISIBLE else GONE
}
override fun showTokenDialog() {
(activity as? MainActivity)?.showDialogFragment(MobileDeviceTokenDialog.newInstance())
}
override fun onDestroyView() {
presenter.onDetachView()
super.onDestroyView()
}
}

View File

@ -0,0 +1,53 @@
package io.github.wulkanowy.ui.modules.mobiledevice
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.MobileDevice
import io.github.wulkanowy.utils.toFormattedString
import kotlinx.android.extensions.LayoutContainer
import kotlinx.android.synthetic.main.item_mobile_device.*
class MobileDeviceItem(val device: MobileDevice) : AbstractFlexibleItem<MobileDeviceItem.ViewHolder>() {
override fun getLayoutRes() = R.layout.item_mobile_device
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<*>>): ViewHolder {
return ViewHolder(view, adapter)
}
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<*>>, holder: ViewHolder, position: Int, payloads: MutableList<Any>?) {
holder.apply {
mobileDeviceItemDate.text = device.date.toFormattedString("dd.MM.yyyy HH:mm:ss")
mobileDeviceItemName.text = device.name
mobileDeviceItemUnregister.setOnClickListener {
(adapter as MobileDeviceAdapter).onDeviceUnregisterListener(device, holder.flexibleAdapterPosition)
}
}
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as MobileDeviceItem
if (device.id != other.device.id) return false
return true
}
override fun hashCode(): Int {
var result = device.hashCode()
result = 31 * result + device.id.toInt()
return result
}
class ViewHolder(val view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter), LayoutContainer {
override val containerView: View
get() = contentView
}
}

View File

@ -0,0 +1,12 @@
package io.github.wulkanowy.ui.modules.mobiledevice
import dagger.Module
import dagger.Provides
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
@Module
class MobileDeviceModule {
@Provides
fun provideMobileDeviceFlexibleAdapter() = MobileDeviceAdapter<AbstractFlexibleItem<*>>()
}

View File

@ -0,0 +1,94 @@
package io.github.wulkanowy.ui.modules.mobiledevice
import io.github.wulkanowy.data.db.entities.MobileDevice
import io.github.wulkanowy.data.repositories.mobiledevice.MobileDeviceRepository
import io.github.wulkanowy.data.repositories.semester.SemesterRepository
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.utils.FirebaseAnalyticsHelper
import io.github.wulkanowy.utils.SchedulersProvider
import timber.log.Timber
import javax.inject.Inject
class MobileDevicePresenter @Inject constructor(
schedulers: SchedulersProvider,
errorHandler: ErrorHandler,
studentRepository: StudentRepository,
private val semesterRepository: SemesterRepository,
private val mobileDeviceRepository: MobileDeviceRepository,
private val analytics: FirebaseAnalyticsHelper
) : BasePresenter<MobileDeviceView>(errorHandler, studentRepository, schedulers) {
override fun onAttachView(view: MobileDeviceView) {
super.onAttachView(view)
view.initView()
Timber.i("Mobile device view was initialized")
loadData()
}
fun onSwipeRefresh() {
loadData(true)
}
private fun loadData(forceRefresh: Boolean = false) {
Timber.i("Loading mobile devices data started")
disposable.add(studentRepository.getCurrentStudent()
.flatMap { semesterRepository.getCurrentSemester(it) }
.flatMap { mobileDeviceRepository.getDevices(it, forceRefresh) }
.map { items -> items.map { MobileDeviceItem(it) } }
.subscribeOn(schedulers.backgroundThread)
.observeOn(schedulers.mainThread)
.doFinally {
view?.run {
hideRefresh()
showProgress(false)
enableSwipe(true)
}
}.subscribe({
Timber.i("Loading mobile devices result: Success")
view?.run {
updateData(it)
showContent(it.isNotEmpty())
}
analytics.logEvent("load_devices", "items" to it.size, "force_refresh" to forceRefresh)
}) {
Timber.i("Loading mobile devices result: An exception occurred")
errorHandler.dispatch(it)
})
}
fun onRegisterDevice() {
view?.showTokenDialog()
}
fun onUnregister(device: MobileDevice) {
Timber.i("Unregister device started")
disposable.add(studentRepository.getCurrentStudent()
.flatMap { semesterRepository.getCurrentSemester(it) }
.flatMap { semester ->
mobileDeviceRepository.unregisterDevice(semester, device)
.flatMap { mobileDeviceRepository.getDevices(semester, it) }
}
.map { items -> items.map { MobileDeviceItem(it) } }
.subscribeOn(schedulers.backgroundThread)
.observeOn(schedulers.mainThread)
.doFinally {
view?.run {
showProgress(false)
enableSwipe(true)
}
}
.subscribe({
Timber.i("Unregister device result: Success")
view?.run {
updateData(it)
showContent(it.isNotEmpty())
}
}) {
Timber.i("Unregister device result: An exception occurred")
errorHandler.dispatch(it)
}
)
}
}

View File

@ -0,0 +1,24 @@
package io.github.wulkanowy.ui.modules.mobiledevice
import io.github.wulkanowy.ui.base.BaseView
interface MobileDeviceView : BaseView {
val isViewEmpty: Boolean
fun initView()
fun updateData(data: List<MobileDeviceItem>)
fun hideRefresh()
fun clearData()
fun showProgress(show: Boolean)
fun enableSwipe(enable: Boolean)
fun showContent(show: Boolean)
fun showTokenDialog()
}

View File

@ -0,0 +1,89 @@
package io.github.wulkanowy.ui.modules.mobiledevice.token
import android.graphics.BitmapFactory
import android.os.Bundle
import android.util.Base64
import android.view.LayoutInflater
import android.view.View
import android.view.View.GONE
import android.view.View.VISIBLE
import android.view.ViewGroup
import android.widget.Toast
import dagger.android.support.DaggerDialogFragment
import io.github.wulkanowy.R
import io.github.wulkanowy.data.pojos.MobileDeviceToken
import io.github.wulkanowy.data.repositories.mobiledevice.MobileDeviceRemote
import io.github.wulkanowy.ui.base.BaseActivity
import kotlinx.android.synthetic.main.dialog_mobile_device.*
import javax.inject.Inject
class MobileDeviceTokenDialog : DaggerDialogFragment(), MobileDeviceTokenVIew {
@Inject
lateinit var presenter: MobileDeviceTokenPresenter
companion object {
fun newInstance(): MobileDeviceTokenDialog = MobileDeviceTokenDialog()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setStyle(STYLE_NO_TITLE, 0)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.dialog_mobile_device, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
presenter.onAttachView(this)
}
override fun initView() {
mobileDeviceDialogClose.setOnClickListener { dismiss() }
}
override fun updateData(token: MobileDeviceToken) {
mobileDeviceDialogToken.text = token.token
mobileDeviceDialogSymbol.text = token.symbol
mobileDeviceDialogPin.text = token.pin
mobileDeviceQr.setImageBitmap(Base64.decode(token.qr, Base64.DEFAULT).let {
BitmapFactory.decodeByteArray(it, 0, it.size)
})
}
override fun hideLoading() {
mobileDeviceDialogProgress.visibility = GONE
}
override fun showContent() {
mobileDeviceDialogContent.visibility = VISIBLE
}
override fun closeDialog() {
dismiss()
}
override fun showError(text: String, error: Throwable) {
showMessage(text)
}
override fun showMessage(text: String) {
Toast.makeText(context, text, Toast.LENGTH_LONG).show()
}
override fun showExpiredDialog() {
(activity as? BaseActivity<*>)?.showExpiredDialog()
}
override fun openClearLoginView() {
(activity as? BaseActivity<*>)?.openClearLoginView()
}
override fun onDestroyView() {
presenter.onDetachView()
super.onDestroyView()
}
}

View File

@ -0,0 +1,51 @@
package io.github.wulkanowy.ui.modules.mobiledevice.token
import io.github.wulkanowy.data.repositories.mobiledevice.MobileDeviceRepository
import io.github.wulkanowy.data.repositories.semester.SemesterRepository
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.utils.FirebaseAnalyticsHelper
import io.github.wulkanowy.utils.SchedulersProvider
import timber.log.Timber
import javax.inject.Inject
class MobileDeviceTokenPresenter @Inject constructor(
schedulers: SchedulersProvider,
errorHandler: ErrorHandler,
studentRepository: StudentRepository,
private val semesterRepository: SemesterRepository,
private val mobileDeviceRepository: MobileDeviceRepository,
private val analytics: FirebaseAnalyticsHelper
) : BasePresenter<MobileDeviceTokenVIew>(errorHandler, studentRepository, schedulers) {
override fun onAttachView(view: MobileDeviceTokenVIew) {
super.onAttachView(view)
view.initView()
Timber.i("Mobile device view was initialized")
loadData()
}
private fun loadData() {
Timber.i("Mobile device registration data started")
disposable.add(studentRepository.getCurrentStudent()
.flatMap { semesterRepository.getCurrentSemester(it) }
.flatMap { mobileDeviceRepository.getToken(it) }
.subscribeOn(schedulers.backgroundThread)
.observeOn(schedulers.mainThread)
.doFinally { view?.hideLoading() }
.subscribe({
Timber.i("Mobile device registration result: Success")
view?.run {
updateData(it)
showContent()
}
analytics.logEvent("device_register", "symbol" to it.token.substring(0, 3))
}) {
Timber.i("Mobile device registration result: An exception occurred")
view?.closeDialog()
errorHandler.dispatch(it)
}
)
}
}

View File

@ -0,0 +1,17 @@
package io.github.wulkanowy.ui.modules.mobiledevice.token
import io.github.wulkanowy.data.pojos.MobileDeviceToken
import io.github.wulkanowy.ui.base.BaseView
interface MobileDeviceTokenVIew : BaseView {
fun initView()
fun hideLoading()
fun showContent()
fun closeDialog()
fun updateData(token: MobileDeviceToken)
}

View File

@ -17,6 +17,7 @@ import io.github.wulkanowy.ui.modules.luckynumber.LuckyNumberFragment
import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.ui.modules.message.MessageFragment
import io.github.wulkanowy.ui.modules.mobiledevice.MobileDeviceFragment
import io.github.wulkanowy.ui.modules.note.NoteFragment
import io.github.wulkanowy.ui.modules.settings.SettingsFragment
import io.github.wulkanowy.utils.setOnItemClickListener
@ -69,6 +70,14 @@ class MoreFragment : BaseFragment(), MoreView, MainView.TitledView, MainView.Mai
}
}
override val mobileDevicesRes: Pair<String, Drawable?>?
get() {
return context?.run {
getString(R.string.mobile_devices_title) to
ContextCompat.getDrawable(this, R.drawable.ic_menu_main_mobile_devices_24dp)
}
}
override val settingsRes: Pair<String, Drawable?>?
get() {
return context?.run {
@ -127,6 +136,10 @@ class MoreFragment : BaseFragment(), MoreView, MainView.TitledView, MainView.Mai
(activity as? MainActivity)?.pushView(LuckyNumberFragment.newInstance())
}
override fun openMobileDevicesView() {
(activity as? MainActivity)?.pushView(MobileDeviceFragment.newInstance())
}
override fun openSettingsView() {
(activity as? MainActivity)?.pushView(SettingsFragment.newInstance())
}

View File

@ -30,6 +30,7 @@ class MorePresenter @Inject constructor(
homeworkRes?.first -> openHomeworkView()
noteRes?.first -> openNoteView()
luckyNumberRes?.first -> openLuckyNumberView()
mobileDevicesRes?.first -> openMobileDevicesView()
settingsRes?.first -> openSettingsView()
aboutRes?.first -> openAboutView()
}
@ -50,6 +51,7 @@ class MorePresenter @Inject constructor(
homeworkRes?.let { MoreItem(it.first, it.second) },
noteRes?.let { MoreItem(it.first, it.second) },
luckyNumberRes?.let { MoreItem(it.first, it.second) },
mobileDevicesRes?.let { MoreItem(it.first, it.second) },
settingsRes?.let { MoreItem(it.first, it.second) },
aboutRes?.let { MoreItem(it.first, it.second) })
)

View File

@ -13,6 +13,8 @@ interface MoreView : BaseView {
val luckyNumberRes: Pair<String, Drawable?>?
val mobileDevicesRes: Pair<String, Drawable?>?
val settingsRes: Pair<String, Drawable?>?
val aboutRes: Pair<String, Drawable?>?
@ -34,4 +36,6 @@ interface MoreView : BaseView {
fun openNoteView()
fun openLuckyNumberView()
fun openMobileDevicesView()
}

View File

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
</vector>

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.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M4,6h18L22,4L4,4c-1.1,0 -2,0.9 -2,2v11L0,17v3h14v-3L4,17L4,6zM23,8h-6c-0.55,0 -1,0.45 -1,1v10c0,0.55 0.45,1 1,1h6c0.55,0 1,-0.45 1,-1L24,9c0,-0.55 -0.45,-1 -1,-1zM22,17h-4v-7h4v7z"/>
</vector>

View File

@ -1,10 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="#FFFFFF"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:fillColor="#000"
android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,1H5v2h14V4z" />
</vector>

View File

@ -0,0 +1,98 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.modules.mobiledevice.token.MobileDeviceTokenDialog">
<FrameLayout
android:layout_width="300dp"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="20dp">
<LinearLayout
android:id="@+id/mobileDeviceDialogContent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:visibility="invisible"
tools:visibility="visible">
<ImageView
android:id="@+id/mobileDeviceQr"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:adjustViewBounds="true"
android:layout_gravity="center"
android:contentDescription="@string/mobile_device_qr"
android:layout_marginBottom="12dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/mobile_device_token"
android:textSize="17sp" />
<TextView
android:id="@+id/mobileDeviceDialogToken"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="3dp"
android:text="@string/all_no_data"
android:textIsSelectable="true"
android:textSize="12sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:text="@string/mobile_device_symbol"
android:textSize="17sp" />
<TextView
android:id="@+id/mobileDeviceDialogSymbol"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="3dp"
android:text="@string/all_no_data"
android:textIsSelectable="true"
android:textSize="12sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:text="@string/mobile_device_pin"
android:textSize="17sp" />
<TextView
android:id="@+id/mobileDeviceDialogPin"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="3dp"
android:text="@string/all_no_data"
android:textIsSelectable="true"
android:textSize="12sp" />
<Button
android:id="@+id/mobileDeviceDialogClose"
style="@style/Widget.AppCompat.Button.Borderless"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:layout_marginTop="15dp"
android:text="@string/all_close"
android:textAllCaps="true"
android:textSize="15sp" />
</LinearLayout>
<ProgressBar
android:id="@+id/mobileDeviceDialogProgress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:indeterminate="true"
tools:visibility="invisible" />
</FrameLayout>
</ScrollView>

View File

@ -0,0 +1,65 @@
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.modules.mobiledevice.MobileDeviceFragment">
<ProgressBar
android:id="@+id/mobileDevicesProgress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:indeterminate="true"
tools:visibility="invisible" />
<LinearLayout
android:id="@+id/mobileDevicesEmpty"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
android:padding="10dp"
android:alpha="0.0">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:gravity="center"
android:text="@string/mobile_devices_no_items"
android:textSize="20sp" />
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minWidth="100dp"
android:minHeight="100dp"
app:srcCompat="@drawable/ic_menu_main_mobile_devices_24dp"
app:tint="?android:attr/textColorPrimary"
tools:ignore="contentDescription" />
</LinearLayout>
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/mobileDevicesSwipe"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/mobileDevicesRecycler"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:listitem="@layout/item_mobile_device" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/mobileDeviceAddButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="16dp"
android:clickable="true"
android:focusable="true"
android:tint="#FFFFFF"
app:srcCompat="@drawable/ic_all_add_24dp" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -0,0 +1,52 @@
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/mobileDevice_subitem_container"
android:layout_width="match_parent"
android:layout_height="64dp"
tools:context=".ui.modules.mobiledevice.MobileDeviceItem">
<TextView
android:id="@+id/mobileDeviceItemName"
android:layout_width="match_parent"
android:layout_height="32dp"
android:layout_marginStart="16dp"
android:layout_marginLeft="16dp"
android:layout_toStartOf="@id/mobileDeviceItemUnregister"
android:layout_toLeftOf="@id/mobileDeviceItemUnregister"
android:ellipsize="end"
android:gravity="bottom"
android:maxLines="1"
android:textSize="16sp"
tools:text="@tools:sample/lorem/random" />
<TextView
android:id="@+id/mobileDeviceItemDate"
android:layout_width="match_parent"
android:layout_height="20dp"
android:layout_below="@id/mobileDeviceItemName"
android:layout_marginStart="16dp"
android:layout_marginLeft="16dp"
android:layout_toStartOf="@id/mobileDeviceItemUnregister"
android:layout_toLeftOf="@id/mobileDeviceItemUnregister"
android:gravity="bottom"
android:textColor="?android:textColorSecondary"
android:textSize="14sp"
tools:text="@tools:sample/date/ddmmyy" />
<ImageButton
android:id="@+id/mobileDeviceItemUnregister"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:layout_marginStart="4dp"
android:layout_marginLeft="4dp"
android:layout_marginEnd="4dp"
android:layout_marginRight="4dp"
android:background="?selectableItemBackgroundBorderless"
android:contentDescription="@string/mobile_devices_unregister"
app:srcCompat="@drawable/ic_message_delete_24dp"
app:tint="?android:textColorSecondary" />
</RelativeLayout>

View File

@ -204,6 +204,17 @@
<string name="lucky_number_notify_new_item">Dziś szczęśliwym numerkiem jest: %d</string>
<!--Mobile devices-->
<string name="mobile_devices_title">Dostęp mobilny</string>
<string name="mobile_devices_no_items">Brak urządzeń</string>
<string name="mobile_devices_unregister">Wyrejestruj</string>
<string name="mobile_device_removed">Urządzenie usunięte</string>
<string name="mobile_device_qr">Kod QR</string>
<string name="mobile_device_token">Token</string>
<string name="mobile_device_symbol">Symbol</string>
<string name="mobile_device_pin">PIN</string>
<!--Account-->
<string name="account_add_new">Dodaj konto</string>
<string name="account_logout">Wyloguj</string>
@ -279,6 +290,7 @@
<!--Others-->
<string name="all_copied">Skopiowano</string>
<string name="all_undo">Cofnij</string>
<!--Errors-->

View File

@ -188,6 +188,16 @@
<string name="lucky_number_notify_new_item_title">Lucky number for today</string>
<string name="lucky_number_notify_new_item">Today\'s lucky number is: %d</string>
<!--Mobile devices-->
<string name="mobile_devices_title">Mobile devices</string>
<string name="mobile_devices_no_items">No devices</string>
<string name="mobile_devices_unregister">Unregister</string>
<string name="mobile_device_removed">Device removed</string>
<string name="mobile_device_qr">QR code</string>
<string name="mobile_device_token">Token</string>
<string name="mobile_device_symbol">Symbol</string>
<string name="mobile_device_pin">PIN</string>
<!--Account-->
<string name="account_add_new">Add account</string>
@ -264,6 +274,7 @@
<!--Others-->
<string name="all_copied">Copied</string>
<string name="all_undo">Undo</string>
<!--Errors-->

View File

@ -0,0 +1,64 @@
package io.github.wulkanowy.data.repositories.mobiledevice
import com.github.pwittchen.reactivenetwork.library.rx2.internet.observing.InternetObservingSettings
import io.github.wulkanowy.data.db.entities.MobileDevice
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.repositories.UnitTestInternetObservingStrategy
import io.reactivex.Maybe
import io.reactivex.Single
import org.junit.Before
import org.junit.Test
import org.mockito.Mock
import org.mockito.Mockito.doReturn
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
import org.threeten.bp.LocalDateTime.of
class MobileDeviceRepositoryTest {
@Mock
private lateinit var semester: Semester
@Mock
private lateinit var mobileDeviceRemote: MobileDeviceRemote
@Mock
private lateinit var mobileDeviceLocal: MobileDeviceLocal
private lateinit var mobileDeviceRepository: MobileDeviceRepository
private val settings = InternetObservingSettings.builder()
.strategy(UnitTestInternetObservingStrategy())
.build()
@Before
fun initTest() {
MockitoAnnotations.initMocks(this)
mobileDeviceRepository = MobileDeviceRepository(settings, mobileDeviceLocal, mobileDeviceRemote)
}
@Test
fun getDevices() {
val devices = listOf(
getDeviceEntity(1),
getDeviceEntity(2)
)
doReturn(Maybe.empty<MobileDevice>()).`when`(mobileDeviceLocal).getDevices(semester)
doReturn(Single.just(devices)).`when`(mobileDeviceRemote).getDevices(semester)
mobileDeviceRepository.getDevices(semester).blockingGet()
verify(mobileDeviceLocal).deleteDevices(emptyList())
verify(mobileDeviceLocal).saveDevices(devices)
}
private fun getDeviceEntity(day: Int): MobileDevice {
return MobileDevice(
studentId = 1,
deviceId = 1,
name = "",
date = of(2019, 5, day, 0, 0, 0)
)
}
}