Add workaround password decryption error (#189)

This commit is contained in:
Rafał Borcz 2018-12-01 15:59:03 +01:00 committed by Mikołaj Pich
parent 11cc85e37c
commit 9a298833f5
33 changed files with 220 additions and 110 deletions

View File

@ -42,7 +42,7 @@ class StudentLocalTest {
.blockingGet()
assert(studentLocal.isStudentSaved)
val student = studentLocal.getCurrentStudent().blockingGet()
val student = studentLocal.getCurrentStudent(true).blockingGet()
assertEquals("23", student.schoolSymbol)
}
}

View File

@ -38,7 +38,7 @@ class ScramblerTest {
val keyStore = KeyStore.getInstance("AndroidKeyStore")
keyStore.load(null)
keyStore.deleteEntry("USER_PASSWORD")
keyStore.deleteEntry("wulkanowy_password")
assertFailsWith<ScramblerException> {
decrypt(text)

View File

@ -13,14 +13,17 @@ open class ErrorHandler @Inject constructor(protected val resources: Resources)
var showErrorMessage: (String, Throwable) -> Unit = { _, _ -> }
open fun proceed(error: Throwable) {
fun dispatch(error: Throwable) {
Timber.e(error, "An exception occurred while the Wulkanowy was running")
proceed(error)
}
protected open fun proceed(error: Throwable) {
showErrorMessage((when (error) {
is UnknownHostException -> resources.getString(R.string.error_no_internet)
is SocketTimeoutException -> resources.getString(R.string.error_timeout)
is NotLoggedInException -> resources.getString(R.string.error_login_failed)
is ServiceUnavailableException -> resources.getString(R.string.error_service_unavaible)
is ServiceUnavailableException -> resources.getString(R.string.error_service_unavailable)
else -> resources.getString(R.string.error_unknown)
}), error)
}

View File

@ -2,6 +2,7 @@ package io.github.wulkanowy.data
import android.content.Context
import android.content.SharedPreferences
import android.content.res.Resources
import androidx.preference.PreferenceManager
import com.github.pwittchen.reactivenetwork.library.rx2.internet.observing.InternetObservingSettings
import com.github.pwittchen.reactivenetwork.library.rx2.internet.observing.strategy.SocketInternetObservingStrategy
@ -40,8 +41,9 @@ internal class RepositoryModule {
@Provides
fun provideDatabase(context: Context) = AppDatabase.newInstance(context)
@Singleton
@Provides
fun provideErrorHandler(context: Context) = ErrorHandler(context.resources)
fun provideResources(context: Context): Resources = context.resources
@Singleton
@Provides

View File

@ -36,12 +36,12 @@ class StudentRepository @Inject constructor(
return cachedStudents
}
fun getSavedStudents(): Single<List<Student>> {
return local.getStudents().toSingle(emptyList())
fun getSavedStudents(decryptPass: Boolean = true): Single<List<Student>> {
return local.getStudents(decryptPass).toSingle(emptyList())
}
fun getCurrentStudent(): Single<Student> {
return local.getCurrentStudent().toSingle()
fun getCurrentStudent(decryptPass: Boolean = true): Single<Student> {
return local.getCurrentStudent(decryptPass).toSingle()
}
fun saveStudent(student: Student): Single<Long> {

View File

@ -31,12 +31,13 @@ class StudentLocal @Inject constructor(
.doOnSuccess { sharedPref.putBoolean(STUDENT_SAVED_KEY, true) }
}
fun getStudents(): Maybe<List<Student>> {
fun getStudents(decryptPass: Boolean): Maybe<List<Student>> {
return studentDb.loadAll()
.map { list -> list.map { it.apply { if (decryptPass) password = decrypt(password) } } }
}
fun getCurrentStudent(): Maybe<Student> {
return studentDb.loadCurrent().map { it.apply { password = decrypt(password) } }
fun getCurrentStudent(decryptPass: Boolean): Maybe<Student> {
return studentDb.loadCurrent().map { it.apply { if (decryptPass) password = decrypt(password) } }
}
fun setCurrentStudent(student: Student): Completable {

View File

@ -27,7 +27,5 @@ internal class AppModule {
@Singleton
@Provides
fun provideJobDispatcher(context: Context): FirebaseJobDispatcher {
return FirebaseJobDispatcher(GooglePlayDriver(context))
}
fun provideJobDispatcher(context: Context) = FirebaseJobDispatcher(GooglePlayDriver(context))
}

View File

@ -1,15 +1,15 @@
package io.github.wulkanowy.ui.modules.account
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import io.github.wulkanowy.data.ErrorHandler
import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.modules.main.MainErrorHandler
import io.github.wulkanowy.utils.SchedulersProvider
import io.reactivex.Single
import javax.inject.Inject
class AccountPresenter @Inject constructor(
private val errorHandler: ErrorHandler,
private val errorHandler: MainErrorHandler,
private val studentRepository: StudentRepository,
private val schedulers: SchedulersProvider
) : BasePresenter<AccountView>(errorHandler) {
@ -31,7 +31,7 @@ class AccountPresenter @Inject constructor(
fun onLogoutConfirm() {
disposable.add(studentRepository.getCurrentStudent()
.flatMapCompletable { studentRepository.logoutStudent(it) }
.andThen(studentRepository.getSavedStudents())
.andThen(studentRepository.getSavedStudents(false))
.flatMap {
if (it.isNotEmpty()) studentRepository.switchStudent(it[0]).toSingle { it }
else Single.just(it)
@ -44,7 +44,7 @@ class AccountPresenter @Inject constructor(
if (it.isEmpty()) openClearLoginView()
else recreateView()
}
}, { errorHandler.proceed(it) }))
}, { errorHandler.dispatch(it) }))
}
fun onItemSelected(item: AbstractFlexibleItem<*>) {
@ -55,17 +55,17 @@ class AccountPresenter @Inject constructor(
disposable.add(studentRepository.switchStudent(item.student)
.subscribeOn(schedulers.backgroundThread)
.observeOn(schedulers.mainThread)
.subscribe({ view?.recreateView() }, { errorHandler.proceed(it) }))
.subscribe({ view?.recreateView() }, { errorHandler.dispatch(it) }))
}
}
}
private fun loadData() {
disposable.add(studentRepository.getSavedStudents()
disposable.add(studentRepository.getSavedStudents(false)
.map { it.map { item -> AccountItem(item) } }
.subscribeOn(schedulers.backgroundThread)
.observeOn(schedulers.mainThread)
.subscribe({ view?.updateData(it) }, { errorHandler.proceed(it) }))
.subscribe({ view?.updateData(it) }, { errorHandler.dispatch(it) }))
}
}

View File

@ -1,12 +1,12 @@
package io.github.wulkanowy.ui.modules.attendance
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import io.github.wulkanowy.data.ErrorHandler
import io.github.wulkanowy.data.repositories.AttendanceRepository
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.data.repositories.SemesterRepository
import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.modules.main.MainErrorHandler
import io.github.wulkanowy.utils.SchedulersProvider
import io.github.wulkanowy.utils.isHolidays
import io.github.wulkanowy.utils.logEvent
@ -21,7 +21,7 @@ import java.util.concurrent.TimeUnit.MILLISECONDS
import javax.inject.Inject
class AttendancePresenter @Inject constructor(
private val errorHandler: ErrorHandler,
private val errorHandler: MainErrorHandler,
private val schedulers: SchedulersProvider,
private val attendanceRepository: AttendanceRepository,
private val studentRepository: StudentRepository,
@ -95,7 +95,7 @@ class AttendancePresenter @Inject constructor(
logEvent("Attendance load", mapOf("items" to it.size, "forceRefresh" to forceRefresh, "date" to currentDate.toFormattedString()))
}) {
view?.run { showEmpty(isViewEmpty) }
errorHandler.proceed(it)
errorHandler.dispatch(it)
}
)
}

View File

@ -1,12 +1,12 @@
package io.github.wulkanowy.ui.modules.exam
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import io.github.wulkanowy.data.ErrorHandler
import io.github.wulkanowy.data.db.entities.Exam
import io.github.wulkanowy.data.repositories.ExamRepository
import io.github.wulkanowy.data.repositories.SemesterRepository
import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.modules.main.MainErrorHandler
import io.github.wulkanowy.utils.SchedulersProvider
import io.github.wulkanowy.utils.friday
import io.github.wulkanowy.utils.isHolidays
@ -21,7 +21,7 @@ import java.util.concurrent.TimeUnit.MILLISECONDS
import javax.inject.Inject
class ExamPresenter @Inject constructor(
private val errorHandler: ErrorHandler,
private val errorHandler: MainErrorHandler,
private val schedulers: SchedulersProvider,
private val examRepository: ExamRepository,
private val studentRepository: StudentRepository,
@ -91,7 +91,7 @@ class ExamPresenter @Inject constructor(
logEvent("Exam load", mapOf("items" to it.size, "forceRefresh" to forceRefresh, "date" to currentDate.toFormattedString()))
}) {
view?.run { showEmpty(isViewEmpty) }
errorHandler.proceed(it)
errorHandler.dispatch(it)
})
}
}

View File

@ -88,6 +88,10 @@ class GradeFragment : BaseFragment(), GradeView, MainView.MainChildView, MainVie
gradeProgress.visibility = if (show) VISIBLE else INVISIBLE
}
override fun showEmpty() {
gradeEmpty.visibility = VISIBLE
}
override fun showSemesterDialog(selectedIndex: Int) {
arrayOf(getString(R.string.grade_semester, 1),
getString(R.string.grade_semester, 2)).also { array ->

View File

@ -1,10 +1,10 @@
package io.github.wulkanowy.ui.modules.grade
import io.github.wulkanowy.data.ErrorHandler
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.repositories.SemesterRepository
import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.modules.main.MainErrorHandler
import io.github.wulkanowy.utils.SchedulersProvider
import io.github.wulkanowy.utils.logEvent
import io.reactivex.Completable
@ -12,7 +12,7 @@ import java.util.concurrent.TimeUnit.MILLISECONDS
import javax.inject.Inject
class GradePresenter @Inject constructor(
private val errorHandler: ErrorHandler,
private val errorHandler: MainErrorHandler,
private val schedulers: SchedulersProvider,
private val studentRepository: StudentRepository,
private val semesterRepository: SemesterRepository
@ -83,7 +83,13 @@ class GradePresenter @Inject constructor(
}
.subscribeOn(schedulers.backgroundThread)
.observeOn(schedulers.mainThread)
.subscribe({ view?.run { loadChild(currentPageIndex) } }) { errorHandler.proceed(it) })
.subscribe({ view?.run { loadChild(currentPageIndex) } }) {
errorHandler.dispatch(it)
view?.run {
showProgress(false)
showEmpty()
}
})
}
private fun loadChild(index: Int, forceRefresh: Boolean = false) {

View File

@ -12,6 +12,8 @@ interface GradeView : BaseView {
fun showProgress(show: Boolean)
fun showEmpty()
fun showSemesterDialog(selectedIndex: Int)
fun notifyChildLoadData(index: Int, semesterId: Int, forceRefresh: Boolean)

View File

@ -1,13 +1,13 @@
package io.github.wulkanowy.ui.modules.grade.details
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import io.github.wulkanowy.data.ErrorHandler
import io.github.wulkanowy.data.db.entities.Grade
import io.github.wulkanowy.data.repositories.GradeRepository
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.data.repositories.SemesterRepository
import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.modules.main.MainErrorHandler
import io.github.wulkanowy.utils.SchedulersProvider
import io.github.wulkanowy.utils.calcAverage
import io.github.wulkanowy.utils.changeModifier
@ -17,7 +17,7 @@ import timber.log.Timber
import javax.inject.Inject
class GradeDetailsPresenter @Inject constructor(
private val errorHandler: ErrorHandler,
private val errorHandler: MainErrorHandler,
private val schedulers: SchedulersProvider,
private val gradeRepository: GradeRepository,
private val studentRepository: StudentRepository,
@ -54,7 +54,7 @@ class GradeDetailsPresenter @Inject constructor(
logEvent("Grade details load", mapOf("items" to it.size, "forceRefresh" to forceRefresh))
}) {
view?.run { showEmpty(isViewEmpty) }
errorHandler.proceed(it)
errorHandler.dispatch(it)
})
}
@ -134,7 +134,7 @@ class GradeDetailsPresenter @Inject constructor(
disposable.add(gradeRepository.updateGrade(grade)
.subscribeOn(schedulers.backgroundThread)
.observeOn(schedulers.mainThread)
.subscribe({}) { error -> errorHandler.proceed(error) })
.subscribe({}) { error -> errorHandler.dispatch(error) })
Timber.d("Grade ${grade.id} updated")
}
}

View File

@ -1,6 +1,5 @@
package io.github.wulkanowy.ui.modules.grade.summary
import io.github.wulkanowy.data.ErrorHandler
import io.github.wulkanowy.data.db.entities.GradeSummary
import io.github.wulkanowy.data.repositories.GradeRepository
import io.github.wulkanowy.data.repositories.GradeSummaryRepository
@ -8,6 +7,7 @@ import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.data.repositories.SemesterRepository
import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.modules.main.MainErrorHandler
import io.github.wulkanowy.utils.SchedulersProvider
import io.github.wulkanowy.utils.calcAverage
import io.github.wulkanowy.utils.changeModifier
@ -17,7 +17,7 @@ import java.util.Locale.FRANCE
import javax.inject.Inject
class GradeSummaryPresenter @Inject constructor(
private val errorHandler: ErrorHandler,
private val errorHandler: MainErrorHandler,
private val gradeSummaryRepository: GradeSummaryRepository,
private val gradeRepository: GradeRepository,
private val studentRepository: StudentRepository,
@ -71,7 +71,7 @@ class GradeSummaryPresenter @Inject constructor(
logEvent("Grade summary load", mapOf("items" to it.first.size, "forceRefresh" to forceRefresh))
}) {
view?.run { showEmpty(isViewEmpty) }
errorHandler.proceed(it)
errorHandler.dispatch(it)
})
}

View File

@ -1,11 +1,11 @@
package io.github.wulkanowy.ui.modules.homework
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import io.github.wulkanowy.data.ErrorHandler
import io.github.wulkanowy.data.repositories.HomeworkRepository
import io.github.wulkanowy.data.repositories.SemesterRepository
import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.modules.main.MainErrorHandler
import io.github.wulkanowy.utils.SchedulersProvider
import io.github.wulkanowy.utils.isHolidays
import io.github.wulkanowy.utils.logEvent
@ -18,7 +18,7 @@ import java.util.concurrent.TimeUnit
import javax.inject.Inject
class HomeworkPresenter @Inject constructor(
private val errorHandler: ErrorHandler,
private val errorHandler: MainErrorHandler,
private val schedulers: SchedulersProvider,
private val homeworkRepository: HomeworkRepository,
private val studentRepository: StudentRepository,
@ -81,7 +81,7 @@ class HomeworkPresenter @Inject constructor(
logEvent("Homework load", mapOf("items" to it.size, "forceRefresh" to forceRefresh, "date" to currentDate.toFormattedString()))
}) {
view?.run { showEmpty(isViewEmpty()) }
errorHandler.proceed(it)
errorHandler.dispatch(it)
})
}
}

View File

@ -5,8 +5,9 @@ import android.database.sqlite.SQLiteConstraintException
import io.github.wulkanowy.R
import io.github.wulkanowy.api.login.BadCredentialsException
import io.github.wulkanowy.data.ErrorHandler
import javax.inject.Inject
class LoginErrorHandler(resources: Resources) : ErrorHandler(resources) {
class LoginErrorHandler @Inject constructor(resources: Resources) : ErrorHandler(resources) {
var onBadCredentials: () -> Unit = {}

View File

@ -1,6 +1,5 @@
package io.github.wulkanowy.ui.modules.login
import android.content.Context
import dagger.Module
import dagger.Provides
import dagger.android.ContributesAndroidInjector
@ -20,11 +19,6 @@ internal abstract class LoginModule {
@PerActivity
@Provides
fun provideLoginAdapter(activity: LoginActivity) = BasePagerAdapter(activity.supportFragmentManager)
@JvmStatic
@PerActivity
@Provides
fun provideLoginErrorHandler(context: Context) = LoginErrorHandler(context.resources)
}
@PerFragment

View File

@ -64,7 +64,7 @@ class LoginFormPresenter @Inject constructor(
}
}
}, {
errorHandler.proceed(it)
errorHandler.dispatch(it)
logRegister(it.localizedMessage, false, if (symbol.isEmpty()) "nil" else symbol, endpoint)
}))
}

View File

@ -31,7 +31,7 @@ class LoginOptionsPresenter @Inject constructor(
.observeOn(schedulers.mainThread)
.subscribeOn(schedulers.backgroundThread)
.doOnSubscribe { view?.showActionBar(true) }
.subscribe({ view?.updateData(it.map { student -> LoginOptionsItem(student) }) }, { errorHandler.proceed(it) }))
.subscribe({ view?.updateData(it.map { student -> LoginOptionsItem(student) }) }, { errorHandler.dispatch(it) }))
}
fun onItemSelected(item: AbstractFlexibleItem<*>?) {
@ -59,7 +59,7 @@ class LoginOptionsPresenter @Inject constructor(
logRegister("Success", true, student.symbol, student.endpoint)
view?.openMainView()
}, {
errorHandler.proceed(it)
errorHandler.dispatch(it)
view?.apply {
showProgress(false)
showContent(true)

View File

@ -5,6 +5,7 @@ import android.content.Intent
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import com.aurelhubert.ahbottomnavigation.AHBottomNavigation.TitleState.ALWAYS_SHOW
@ -18,6 +19,7 @@ import io.github.wulkanowy.ui.modules.account.AccountDialog
import io.github.wulkanowy.ui.modules.attendance.AttendanceFragment
import io.github.wulkanowy.ui.modules.exam.ExamFragment
import io.github.wulkanowy.ui.modules.grade.GradeFragment
import io.github.wulkanowy.ui.modules.login.LoginActivity
import io.github.wulkanowy.ui.modules.more.MoreFragment
import io.github.wulkanowy.ui.modules.timetable.TimetableFragment
import io.github.wulkanowy.utils.getThemeAttrColor
@ -132,6 +134,15 @@ class MainActivity : BaseActivity(), MainView {
navController.showDialogFragment(AccountDialog.newInstance())
}
override fun showExpiredDialog() {
AlertDialog.Builder(this)
.setTitle(R.string.main_session_expired)
.setMessage(R.string.main_session_relogin)
.setPositiveButton(R.string.main_log_in) { _, _ -> presenter.onLoginSelected() }
.setNegativeButton(android.R.string.cancel) { _, _ -> }
.show()
}
override fun notifyMenuViewReselected() {
(navController.currentStack?.get(0) as? MainView.MainChildView)?.onFragmentReselected()
}
@ -152,6 +163,11 @@ class MainActivity : BaseActivity(), MainView {
GradeNotification(applicationContext).cancelAll()
}
override fun openLoginView() {
startActivity(LoginActivity.getStartIntent(this)
.apply { addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK) })
}
override fun onSaveInstanceState(outState: Bundle?) {
super.onSaveInstanceState(outState)
navController.onSaveInstanceState(outState)

View File

@ -0,0 +1,25 @@
package io.github.wulkanowy.ui.modules.main
import android.content.res.Resources
import io.github.wulkanowy.data.ErrorHandler
import io.github.wulkanowy.di.scopes.PerActivity
import io.github.wulkanowy.utils.security.ScramblerException
import javax.inject.Inject
@PerActivity
class MainErrorHandler @Inject constructor(resources: Resources) : ErrorHandler(resources) {
var onDecryptionFail: () -> Unit = {}
override fun proceed(error: Throwable) {
when (error) {
is ScramblerException -> onDecryptionFail()
else -> super.proceed(error)
}
}
override fun clear() {
super.clear()
onDecryptionFail = {}
}
}

View File

@ -1,34 +1,37 @@
package io.github.wulkanowy.ui.modules.main
import io.github.wulkanowy.data.ErrorHandler
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.services.job.ServiceHelper
import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.utils.SchedulersProvider
import io.github.wulkanowy.utils.logLogin
import io.reactivex.Completable
import javax.inject.Inject
class MainPresenter @Inject constructor(
errorHandler: ErrorHandler,
private val errorHandler: MainErrorHandler,
private val studentRepository: StudentRepository,
private val prefRepository: PreferencesRepository,
private val schedulers: SchedulersProvider,
private val serviceHelper: ServiceHelper
) : BasePresenter<MainView>(errorHandler) {
fun onAttachView(view: MainView, initMenuIndex: Int) {
super.onAttachView(view)
view.run {
cancelNotifications()
errorHandler.onDecryptionFail = { showExpiredDialog() }
startMenuIndex = if (initMenuIndex != -1) initMenuIndex else prefRepository.startMenuIndex
initView()
}
serviceHelper.startFullSyncService()
when (initMenuIndex) {
1 -> logLogin("Grades")
3 -> logLogin("Timetable")
4 -> logLogin("More")
}
serviceHelper.startFullSyncService()
}
fun onViewStart() {
@ -69,4 +72,17 @@ class MainPresenter @Inject constructor(
}
} == true
}
fun onLoginSelected() {
disposable.add(studentRepository.getCurrentStudent(false)
.flatMapCompletable { studentRepository.logoutStudent(it) }
.andThen(studentRepository.getSavedStudents(false))
.flatMapCompletable {
if (it.isNotEmpty()) studentRepository.switchStudent(it[0])
else Completable.complete()
}
.subscribeOn(schedulers.backgroundThread)
.observeOn(schedulers.mainThread)
.subscribe({ view?.openLoginView() }, { errorHandler.dispatch(it) }))
}
}

View File

@ -20,6 +20,8 @@ interface MainView : BaseView {
fun showAccountPicker()
fun showExpiredDialog()
fun notifyMenuViewReselected()
fun setViewTitle(title: String)
@ -28,6 +30,8 @@ interface MainView : BaseView {
fun cancelNotifications()
fun openLoginView()
interface MainChildView {
fun onFragmentReselected()

View File

@ -1,19 +1,19 @@
package io.github.wulkanowy.ui.modules.note
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import io.github.wulkanowy.data.ErrorHandler
import io.github.wulkanowy.data.db.entities.Note
import io.github.wulkanowy.data.repositories.NoteRepository
import io.github.wulkanowy.data.repositories.SemesterRepository
import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.modules.main.MainErrorHandler
import io.github.wulkanowy.utils.SchedulersProvider
import io.github.wulkanowy.utils.logEvent
import timber.log.Timber
import javax.inject.Inject
class NotePresenter @Inject constructor(
private val errorHandler: ErrorHandler,
private val errorHandler: MainErrorHandler,
private val schedulers: SchedulersProvider,
private val studentRepository: StudentRepository,
private val noteRepository: NoteRepository,
@ -52,7 +52,7 @@ class NotePresenter @Inject constructor(
logEvent("Note load", mapOf("items" to it.size, "forceRefresh" to forceRefresh))
}, {
view?.run { showEmpty(isViewEmpty) }
errorHandler.proceed(it)
errorHandler.dispatch(it)
})
)
}
@ -76,7 +76,7 @@ class NotePresenter @Inject constructor(
.observeOn(schedulers.mainThread)
.subscribe({
Timber.d("Note ${note.id} updated")
}) { error -> errorHandler.proceed(error) }
}) { error -> errorHandler.dispatch(error) }
)
}
}

View File

@ -1,11 +1,11 @@
package io.github.wulkanowy.ui.modules.timetable
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import io.github.wulkanowy.data.ErrorHandler
import io.github.wulkanowy.data.repositories.SemesterRepository
import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.data.repositories.TimetableRepository
import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.modules.main.MainErrorHandler
import io.github.wulkanowy.utils.SchedulersProvider
import io.github.wulkanowy.utils.isHolidays
import io.github.wulkanowy.utils.logEvent
@ -20,7 +20,7 @@ import java.util.concurrent.TimeUnit.MILLISECONDS
import javax.inject.Inject
class TimetablePresenter @Inject constructor(
private val errorHandler: ErrorHandler,
private val errorHandler: MainErrorHandler,
private val schedulers: SchedulersProvider,
private val timetableRepository: TimetableRepository,
private val studentRepository: StudentRepository,
@ -89,7 +89,7 @@ class TimetablePresenter @Inject constructor(
logEvent("Timetable load", mapOf("items" to it.size, "forceRefresh" to forceRefresh, "date" to currentDate.toFormattedString()))
}) {
view?.run { showEmpty(isViewEmpty()) }
errorHandler.proceed(it)
errorHandler.dispatch(it)
})
}
}

View File

@ -11,10 +11,9 @@ import android.security.KeyPairGeneratorSpec
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties.DIGEST_SHA256
import android.security.keystore.KeyProperties.DIGEST_SHA512
import android.security.keystore.KeyProperties.ENCRYPTION_PADDING_RSA_PKCS1
import android.security.keystore.KeyProperties.ENCRYPTION_PADDING_RSA_OAEP
import android.security.keystore.KeyProperties.PURPOSE_DECRYPT
import android.security.keystore.KeyProperties.PURPOSE_ENCRYPT
import android.security.keystore.KeyProperties.SIGNATURE_PADDING_RSA_PKCS1
import android.util.Base64
import android.util.Base64.DEFAULT
import android.util.Base64.decode
@ -27,7 +26,7 @@ import java.math.BigInteger
import java.nio.charset.Charset
import java.security.KeyPairGenerator
import java.security.KeyStore
import java.security.PrivateKey
import java.security.spec.MGF1ParameterSpec.SHA1
import java.util.Calendar
import java.util.Calendar.YEAR
import javax.crypto.Cipher
@ -35,34 +34,28 @@ import javax.crypto.Cipher.DECRYPT_MODE
import javax.crypto.Cipher.ENCRYPT_MODE
import javax.crypto.CipherInputStream
import javax.crypto.CipherOutputStream
import javax.crypto.spec.OAEPParameterSpec
import javax.crypto.spec.PSource.PSpecified
import javax.security.auth.x500.X500Principal
private const val KEY_ALIAS = "USER_PASSWORD"
private const val ALGORITHM_RSA = "RSA"
private const val KEYSTORE_NAME = "AndroidKeyStore"
private const val KEY_TRANSFORMATION_ALGORITHM = "RSA/ECB/PKCS1Padding"
private const val KEY_CIPHER_JELLY_PROVIDER = "AndroidOpenSSL"
private const val KEY_CIPHER_M_PROVIDER = "AndroidKeyStoreBCWorkaround"
private const val KEY_ALIAS = "wulkanowy_password"
private val KEY_CHARSET = Charset.forName("UTF-8")
private val isKeyPairExists: Boolean
get() = keyStore.getKey(KEY_ALIAS, null) != null
private val cipher: Cipher
get() {
return if (SDK_INT >= M) Cipher.getInstance(KEY_TRANSFORMATION_ALGORITHM, KEY_CIPHER_M_PROVIDER)
else Cipher.getInstance(KEY_TRANSFORMATION_ALGORITHM, KEY_CIPHER_JELLY_PROVIDER)
}
private val keyStore: KeyStore
get() = KeyStore.getInstance(KEYSTORE_NAME).apply { load(null) }
private val cipher: Cipher
get() {
return if (SDK_INT >= M) Cipher.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding", "AndroidKeyStoreBCWorkaround")
else Cipher.getInstance("RSA/ECB/PKCS1Padding", "AndroidOpenSSL")
}
fun encrypt(plainText: String, context: Context): String {
if (plainText.isEmpty()) throw ScramblerException("Text to be encrypted is empty")
@ -72,8 +65,13 @@ fun encrypt(plainText: String, context: Context): String {
return try {
if (!isKeyPairExists) generateKeyPair(context)
cipher.let {
it.init(ENCRYPT_MODE, keyStore.getCertificate(KEY_ALIAS).publicKey)
if (SDK_INT >= M) {
OAEPParameterSpec("SHA-256", "MGF1", SHA1, PSpecified.DEFAULT).let { spec ->
it.init(ENCRYPT_MODE, keyStore.getCertificate(KEY_ALIAS).publicKey, spec)
}
} else it.init(ENCRYPT_MODE, keyStore.getCertificate(KEY_ALIAS).publicKey)
ByteArrayOutputStream().let { output ->
CipherOutputStream(output, it).apply {
@ -92,15 +90,19 @@ fun encrypt(plainText: String, context: Context): String {
fun decrypt(cipherText: String): String {
if (cipherText.isEmpty()) throw ScramblerException("Text to be encrypted is empty")
if (SDK_INT < JELLY_BEAN_MR2 || cipherText.length < 250) {
return String(decode(cipherText.toByteArray(KEY_CHARSET), DEFAULT), KEY_CHARSET)
}
if (!isKeyPairExists) throw ScramblerException("KeyPair doesn't exist")
return try {
if (SDK_INT < JELLY_BEAN_MR2 || cipherText.length < 250) {
return String(decode(cipherText.toByteArray(KEY_CHARSET), DEFAULT), KEY_CHARSET)
}
if (!isKeyPairExists) throw ScramblerException("KeyPair doesn't exist")
cipher.let {
it.init(DECRYPT_MODE, (keyStore.getKey(KEY_ALIAS, null) as PrivateKey))
if (SDK_INT >= M) {
OAEPParameterSpec("SHA-256", "MGF1", SHA1, PSpecified.DEFAULT).let { spec ->
it.init(DECRYPT_MODE, keyStore.getKey(KEY_ALIAS, null), spec)
}
} else it.init(DECRYPT_MODE, keyStore.getKey(KEY_ALIAS, null))
CipherInputStream(ByteArrayInputStream(decode(cipherText, DEFAULT)), it).let { input ->
val values = ArrayList<Byte>()
@ -124,22 +126,21 @@ fun decrypt(cipherText: String): String {
private fun generateKeyPair(context: Context) {
(if (SDK_INT >= M) {
KeyGenParameterSpec.Builder(KEY_ALIAS, PURPOSE_DECRYPT or PURPOSE_ENCRYPT)
.setDigests(DIGEST_SHA256, DIGEST_SHA512)
.setCertificateSubject(X500Principal("CN=Wulkanowy"))
.setEncryptionPaddings(ENCRYPTION_PADDING_RSA_PKCS1)
.setSignaturePaddings(SIGNATURE_PADDING_RSA_PKCS1)
.setCertificateSerialNumber(BigInteger.TEN)
.build()
.setDigests(DIGEST_SHA256, DIGEST_SHA512)
.setEncryptionPaddings(ENCRYPTION_PADDING_RSA_OAEP)
.setCertificateSerialNumber(BigInteger.TEN)
.setCertificateSubject(X500Principal("CN=Wulkanowy"))
.build()
} else {
KeyPairGeneratorSpec.Builder(context)
.setAlias(KEY_ALIAS)
.setSubject(X500Principal("CN=Wulkanowy"))
.setSerialNumber(BigInteger.TEN)
.setStartDate(Calendar.getInstance().time)
.setEndDate(Calendar.getInstance().apply { add(YEAR, 99) }.time)
.build()
.setAlias(KEY_ALIAS)
.setSubject(X500Principal("CN=Wulkanowy"))
.setSerialNumber(BigInteger.TEN)
.setStartDate(Calendar.getInstance().time)
.setEndDate(Calendar.getInstance().apply { add(YEAR, 99) }.time)
.build()
}).let {
KeyPairGenerator.getInstance(ALGORITHM_RSA, KEYSTORE_NAME).apply {
KeyPairGenerator.getInstance("RSA", KEYSTORE_NAME).apply {
initialize(it)
genKeyPair()
}

View File

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<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">
@ -30,4 +31,30 @@
android:layout_height="wrap_content"
android:layout_gravity="center"
android:indeterminate="true" />
<LinearLayout
android:id="@+id/gradeEmpty"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
android:visibility="invisible">
<androidx.appcompat.widget.AppCompatImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minWidth="100dp"
android:minHeight="100dp"
app:srcCompat="@drawable/ic_menu_main_grade_26dp"
app:tint="?android:attr/textColorPrimary"
tools:ignore="contentDescription" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:gravity="center"
android:text="@string/grade_no_items"
android:textSize="20sp" />
</LinearLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -35,6 +35,9 @@
<!--Main-->
<string name="main_account_picker">Menadżer kont</string>
<string name="main_log_in">Zaloguj się</string>
<string name="main_session_expired">Sesja wygasła</string>
<string name="main_session_relogin">Sesja wygasła, zaloguj się ponownie</string>
<!--Grade-->
@ -211,6 +214,6 @@
<string name="error_no_internet">Brak połączenia z internetem</string>
<string name="error_timeout">Zbyt długie oczekiwanie na połączenie</string>
<string name="error_login_failed">Logowanie nie powiodło się. Spróbuj ponownie lub zrestartuj aplikację</string>
<string name="error_service_unavaible">Dziennik jest niedostępny. Spróbuj ponownie później</string>
<string name="error_service_unavailable">Dziennik jest niedostępny. Spróbuj ponownie później</string>
<string name="error_unknown">Wystąpił nieoczekiwany błąd</string>
</resources>

View File

@ -35,6 +35,9 @@
<!--Main-->
<string name="main_account_picker">Account manager</string>
<string name="main_log_in">Log in</string>
<string name="main_session_expired">Session expired</string>
<string name="main_session_relogin">Session expired, log in again</string>
<!--Grade-->
@ -196,6 +199,6 @@
<string name="error_no_internet">No internet connection</string>
<string name="error_timeout">Too long wait for connection</string>
<string name="error_login_failed">Login is failed. Try again or restart the app</string>
<string name="error_service_unavaible">The log is not available. Try again later</string>
<string name="error_service_unavailable">The log is not available. Try again later</string>
<string name="error_unknown">An unexpected error occurred</string>
</resources>

View File

@ -140,7 +140,7 @@ class LoginFormPresenterTest {
verify(loginFormView).showProgress(false)
verify(loginFormView).showContent(false)
verify(loginFormView).showContent(true)
verify(errorHandler).proceed(testException)
verify(errorHandler).dispatch(testException)
}
}

View File

@ -63,7 +63,7 @@ class LoginOptionsPresenterTest {
doReturn(Single.error<List<Student>>(testException)).`when`(studentRepository).cachedStudents
presenter.onParentViewLoadData()
verify(loginOptionsView).showActionBar(true)
verify(errorHandler).proceed(testException)
verify(errorHandler).dispatch(testException)
}
@Test
@ -85,6 +85,6 @@ class LoginOptionsPresenterTest {
presenter.onItemSelected(LoginOptionsItem(testStudent))
verify(loginOptionsView).showContent(false)
verify(loginOptionsView).showProgress(true)
verify(errorHandler).proceed(testException)
verify(errorHandler).dispatch(testException)
}
}

View File

@ -1,7 +1,8 @@
package io.github.wulkanowy.ui.modules.main
import io.github.wulkanowy.data.ErrorHandler
import io.github.wulkanowy.TestSchedulersProvider
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.services.job.ServiceHelper
import org.junit.Before
import org.junit.Test
@ -13,7 +14,10 @@ import org.mockito.MockitoAnnotations
class MainPresenterTest {
@Mock
lateinit var errorHandler: ErrorHandler
lateinit var errorHandler: MainErrorHandler
@Mock
lateinit var studentRepository: StudentRepository
@Mock
lateinit var prefRepository: PreferencesRepository
@ -31,7 +35,7 @@ class MainPresenterTest {
MockitoAnnotations.initMocks(this)
clearInvocations(mainView)
presenter = MainPresenter(errorHandler, prefRepository, serviceHelper)
presenter = MainPresenter(errorHandler, studentRepository, prefRepository, TestSchedulersProvider(), serviceHelper)
presenter.onAttachView(mainView, -1)
}