diff --git a/README.md b/README.md new file mode 100644 index 0000000..5181038 --- /dev/null +++ b/README.md @@ -0,0 +1,27 @@ +# peacock + +![peacock](https://user-images.githubusercontent.com/31917346/61375032-123b4f80-a8b3-11e9-8cdd-0e41947ecb1c.jpg) + +Setting up development Environment +---------------------------------- + +### Installing Dependencies + + JDK + SDK + +### Setup Android environment + + Install Android Studio and config JDK and SDK. + +#### Upgrade SKD, BuildTool and etc. + + Sync gradle and install what it needs. + +### Debug Mode + + Run project on device/emulator. + +### Release Mode + + Run project on device/emulator. diff --git a/app/build.gradle b/app/build.gradle index 152bbfc..4d13104 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,8 +1,17 @@ apply plugin: 'com.android.application' - apply plugin: 'kotlin-android' - apply plugin: 'kotlin-android-extensions' +apply plugin: "androidx.navigation.safeargs.kotlin" +apply plugin: 'kotlin-kapt' + +ext.versionMajor = 00 +ext.versionMinor = 00 +ext.versionPatch = 01 +ext.versionClassifier = null +ext.isSnapshot = false +ext.minimumSdkVersion = 17 +ext.minimumScreenSize = 1 +ext.maximumScreenSize = 4 android { compileSdkVersion 29 @@ -11,9 +20,12 @@ android { applicationId "de.netalic.peacock" minSdkVersion 17 targetSdkVersion 29 - versionCode 1 - versionName "1.0" + versionCode generateVersionCode() + versionName generateVersionName() + vectorDrawables.useSupportLibrary = true testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + vectorDrawables.useSupportLibrary = true + multiDexEnabled true } buildTypes { release { @@ -21,14 +33,76 @@ android { proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } + + compileOptions { + sourceCompatibility 1.8 + targetCompatibility 1.8 + } +} + + +private Integer generateVersionCode() { + return ext.minimumSdkVersion * 100000000 + ext.minimumScreenSize * 10000000 + ext.maximumScreenSize * 1000000 + ext.versionMajor * 10000 + ext.versionMinor * 100 + ext.versionPatch +} + +private String generateVersionName() { + String versionName = "${ext.versionMajor}.${ext.versionMinor}.${ext.versionPatch}" + if (ext.versionClassifier == null) { + if (ext.isSnapshot) { + ext.versionClassifier = "SNAPSHOT" + } + } + + if (ext.versionClassifier != null) { + versionName += "-" + ext.versionClassifier + } + return versionName +} + +repositories { + mavenCentral() } dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) - implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation 'androidx.appcompat:appcompat:1.0.2' + implementation 'androidx.cardview:cardview:1.0.0' implementation 'androidx.core:core-ktx:1.0.2' + implementation 'com.google.android.material:material:1.1.0-alpha08' + implementation 'androidx.constraintlayout:constraintlayout:1.1.3' + implementation 'androidx.navigation:navigation-fragment-ktx:2.1.0-beta02' + implementation 'androidx.navigation:navigation-ui-ktx:2.1.0-beta02' + implementation 'com.andrognito.patternlockview:patternlockview:1.0.0' + implementation 'com.jakewharton.timber:timber:4.7.1' + implementation 'androidx.legacy:legacy-support-v4:1.0.0' + implementation "org.koin:koin-android:2.0.1" + implementation "org.koin:koin-android-viewmodel:2.0.1" + implementation 'com.mikhaellopez:circularimageview:4.0.1' + implementation 'com.github.EhsanMashhadi:CountryPicker:0.4.0' + implementation "com.squareup.retrofit2:retrofit:2.6.0" + implementation "com.squareup.retrofit2:converter-gson:2.6.0" + implementation 'io.reactivex.rxjava2:rxandroid:2.1.1' + implementation 'io.reactivex.rxjava2:rxjava:2.2.6' + implementation 'com.squareup.retrofit2:adapter-rxjava2:2.6.0' + implementation "android.arch.lifecycle:extensions:1.1.1" + implementation 'com.googlecode.libphonenumber:libphonenumber:8.2.0' + testImplementation 'junit:junit:4.12' + testImplementation "org.mockito:mockito-inline:3.0.0" + testImplementation 'org.koin:koin-test:2.0.1' + testImplementation "android.arch.core:core-testing:1.1.1" + + androidTestImplementation 'androidx.test.ext:junit:1.1.1' androidTestImplementation 'androidx.test:runner:1.2.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' -} + + + implementation ('com.alimuzaffar.lib:pinentryedittext:2.0.6') { + exclude group: 'androidx.appcompat', module: 'appcompat' + } + implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0' + + implementation 'com.github.EhsanMashhadi:helpdroid:0.9.0' + implementation 'com.github.franmontiel:PersistentCookieJar:v1.0.1' +} \ No newline at end of file diff --git a/app/src/androidTest/java/de/netalic/peacock/ExampleInstrumentedTest.kt b/app/src/androidTest/java/de/netalic/peacock/ExampleInstrumentedTest.kt index a738cd7..d3a4e9a 100644 --- a/app/src/androidTest/java/de/netalic/peacock/ExampleInstrumentedTest.kt +++ b/app/src/androidTest/java/de/netalic/peacock/ExampleInstrumentedTest.kt @@ -2,12 +2,10 @@ package de.netalic.peacock import androidx.test.InstrumentationRegistry import androidx.test.runner.AndroidJUnit4 - +import org.junit.Assert.assertEquals import org.junit.Test import org.junit.runner.RunWith -import org.junit.Assert.* - /** * Instrumented test, which will execute on an Android device. * diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 12789b7..656a32c 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,10 +1,30 @@ - - - - + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/de/netalic/peacock/common/MyApplication.kt b/app/src/main/java/de/netalic/peacock/common/MyApplication.kt new file mode 100644 index 0000000..95244a4 --- /dev/null +++ b/app/src/main/java/de/netalic/peacock/common/MyApplication.kt @@ -0,0 +1,43 @@ +package de.netalic.peacock.common + +import android.app.Application +import de.netalic.peacock.BuildConfig +import de.netalic.peacock.di.apiModule +import de.netalic.peacock.di.repositoryModule +import de.netalic.peacock.di.viewModelModule +import okhttp3.internal.Internal.instance +import org.koin.android.ext.koin.androidContext +import org.koin.android.ext.koin.androidLogger +import org.koin.core.context.startKoin +import timber.log.Timber + +class MyApplication : Application() { + + + override fun onCreate() { + super.onCreate() + + instance = this + + if (BuildConfig.DEBUG) { + Timber.plant(Timber.DebugTree()) + } + startKoin { + androidLogger() + androidContext(this@MyApplication) + modules( + listOf( + repositoryModule, + viewModelModule, + apiModule + ) + ) + } + } + + companion object { + + lateinit var instance: Application + } + +} \ No newline at end of file diff --git a/app/src/main/java/de/netalic/peacock/data/exception/ActivationCodeIsNotValid.kt b/app/src/main/java/de/netalic/peacock/data/exception/ActivationCodeIsNotValid.kt new file mode 100644 index 0000000..ac08047 --- /dev/null +++ b/app/src/main/java/de/netalic/peacock/data/exception/ActivationCodeIsNotValid.kt @@ -0,0 +1,3 @@ +package de.netalic.peacock.data.exception + +class ActivationCodeIsNotValid :Throwable() \ No newline at end of file diff --git a/app/src/main/java/de/netalic/peacock/data/exception/BadRequestException.kt b/app/src/main/java/de/netalic/peacock/data/exception/BadRequestException.kt new file mode 100644 index 0000000..7975f05 --- /dev/null +++ b/app/src/main/java/de/netalic/peacock/data/exception/BadRequestException.kt @@ -0,0 +1,3 @@ +package de.netalic.peacock.data.exception + +class BadRequestException : BaseException() \ No newline at end of file diff --git a/app/src/main/java/de/netalic/peacock/data/exception/BaseException.kt b/app/src/main/java/de/netalic/peacock/data/exception/BaseException.kt new file mode 100644 index 0000000..5f22074 --- /dev/null +++ b/app/src/main/java/de/netalic/peacock/data/exception/BaseException.kt @@ -0,0 +1,3 @@ +package de.netalic.peacock.data.exception + +open class BaseException:Throwable() \ No newline at end of file diff --git a/app/src/main/java/de/netalic/peacock/data/exception/InvalidDeviceName.kt b/app/src/main/java/de/netalic/peacock/data/exception/InvalidDeviceName.kt new file mode 100644 index 0000000..78d134a --- /dev/null +++ b/app/src/main/java/de/netalic/peacock/data/exception/InvalidDeviceName.kt @@ -0,0 +1,3 @@ +package de.netalic.peacock.data.exception + +class InvalidDeviceName:Throwable() \ No newline at end of file diff --git a/app/src/main/java/de/netalic/peacock/data/exception/InvalidPhoneNumberException.kt b/app/src/main/java/de/netalic/peacock/data/exception/InvalidPhoneNumberException.kt new file mode 100644 index 0000000..b11c8ba --- /dev/null +++ b/app/src/main/java/de/netalic/peacock/data/exception/InvalidPhoneNumberException.kt @@ -0,0 +1,3 @@ +package de.netalic.peacock.data.exception + +class InvalidPhoneNumberException:BaseException() \ No newline at end of file diff --git a/app/src/main/java/de/netalic/peacock/data/exception/InvalidUdidOrPhone.kt b/app/src/main/java/de/netalic/peacock/data/exception/InvalidUdidOrPhone.kt new file mode 100644 index 0000000..782a124 --- /dev/null +++ b/app/src/main/java/de/netalic/peacock/data/exception/InvalidUdidOrPhone.kt @@ -0,0 +1,3 @@ +package de.netalic.peacock.data.exception + +class InvalidUdidOrPhone :Throwable() \ No newline at end of file diff --git a/app/src/main/java/de/netalic/peacock/data/exception/InvalidUdidOrPhoneException.kt b/app/src/main/java/de/netalic/peacock/data/exception/InvalidUdidOrPhoneException.kt new file mode 100644 index 0000000..44b1883 --- /dev/null +++ b/app/src/main/java/de/netalic/peacock/data/exception/InvalidUdidOrPhoneException.kt @@ -0,0 +1,3 @@ +package de.netalic.peacock.data.exception + +class InvalidUdidOrPhoneException : BaseException() diff --git a/app/src/main/java/de/netalic/peacock/data/exception/ServerException.kt b/app/src/main/java/de/netalic/peacock/data/exception/ServerException.kt new file mode 100644 index 0000000..76ca31f --- /dev/null +++ b/app/src/main/java/de/netalic/peacock/data/exception/ServerException.kt @@ -0,0 +1,3 @@ +package de.netalic.peacock.data.exception + +class ServerException : BaseException() \ No newline at end of file diff --git a/app/src/main/java/de/netalic/peacock/data/model/MyResponse.kt b/app/src/main/java/de/netalic/peacock/data/model/MyResponse.kt new file mode 100644 index 0000000..a75101c --- /dev/null +++ b/app/src/main/java/de/netalic/peacock/data/model/MyResponse.kt @@ -0,0 +1,29 @@ +package de.netalic.peacock.data.model + +data class MyResponse( + val status: Status, + val data: T? = null, + val throwable: Throwable? = null +) { + + companion object { + + fun loading(): MyResponse { + return MyResponse(status = Status.LOADING) + } + + fun success(data: T): MyResponse { + return MyResponse(status = Status.SUCCESS, data = data) + } + + fun failed(throwable: Throwable): MyResponse { + return MyResponse(status = Status.FAILED, throwable = throwable) + } + } +} + +enum class Status { + LOADING, + SUCCESS, + FAILED +} \ No newline at end of file diff --git a/app/src/main/java/de/netalic/peacock/data/model/UserModel.kt b/app/src/main/java/de/netalic/peacock/data/model/UserModel.kt new file mode 100644 index 0000000..94539d8 --- /dev/null +++ b/app/src/main/java/de/netalic/peacock/data/model/UserModel.kt @@ -0,0 +1,13 @@ +package de.netalic.peacock.data.model + +import com.google.gson.annotations.SerializedName + +data class UserModel( + @SerializedName("id") val mId: String? = null, + @SerializedName("name") val mName: String? = null, + @SerializedName("phone") val mPhone: String, + @SerializedName("udid") val mUdid: String, + @SerializedName("firebaseToken") val mFirebaseToken: String = "", + val mActivateToken:String, + val mDeviceType:String +) \ No newline at end of file diff --git a/app/src/main/java/de/netalic/peacock/data/repository/BaseRepository.kt b/app/src/main/java/de/netalic/peacock/data/repository/BaseRepository.kt new file mode 100644 index 0000000..51ceee5 --- /dev/null +++ b/app/src/main/java/de/netalic/peacock/data/repository/BaseRepository.kt @@ -0,0 +1,3 @@ +package de.netalic.peacock.data.repository + +open class BaseRepository \ No newline at end of file diff --git a/app/src/main/java/de/netalic/peacock/data/repository/UserRepository.kt b/app/src/main/java/de/netalic/peacock/data/repository/UserRepository.kt new file mode 100644 index 0000000..8d335c6 --- /dev/null +++ b/app/src/main/java/de/netalic/peacock/data/repository/UserRepository.kt @@ -0,0 +1,21 @@ +package de.netalic.peacock.data.repository + +import de.netalic.peacock.data.model.UserModel +import de.netalic.peacock.data.webservice.ApiInterface +import io.reactivex.Single +import okhttp3.ResponseBody +import retrofit2.Response + +class UserRepository(private val apiInterface: ApiInterface) : BaseRepository() { + + + fun claim(phone: String, udid: String): Single> { + return apiInterface.claim(phone, udid) + } + + fun bind(user:UserModel) :Single>{ + + return apiInterface.bind(user.mPhone,user.mUdid, user.mName!!,user.mDeviceType,user.mFirebaseToken, + user.mActivateToken) + } +} \ No newline at end of file diff --git a/app/src/main/java/de/netalic/peacock/data/webservice/ApiClient.kt b/app/src/main/java/de/netalic/peacock/data/webservice/ApiClient.kt new file mode 100644 index 0000000..0df3ec3 --- /dev/null +++ b/app/src/main/java/de/netalic/peacock/data/webservice/ApiClient.kt @@ -0,0 +1,86 @@ +package de.netalic.peacock.data.webservice + +import com.franmontiel.persistentcookiejar.PersistentCookieJar +import com.franmontiel.persistentcookiejar.cache.SetCookieCache +import com.franmontiel.persistentcookiejar.persistence.SharedPrefsCookiePersistor +import de.netalic.peacock.common.MyApplication +import nuesoft.helpdroid.network.SharedPreferencesJwtPersistor +import okhttp3.Interceptor +import okhttp3.OkHttpClient +import okhttp3.Response +import okhttp3.ResponseBody +import retrofit2.Retrofit +import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory +import retrofit2.converter.gson.GsonConverterFactory +import java.io.IOException + +class ApiClient { + + + companion object { + private var sRetrofit: Retrofit? = null + private var sApi: ApiInterface? = null + + + private fun getClient(): Retrofit { + + if (sRetrofit == null) { + + val okHttpClient = OkHttpClient().newBuilder() + val cookieJar = PersistentCookieJar(SetCookieCache(), SharedPrefsCookiePersistor(MyApplication.instance)) + okHttpClient.cookieJar(cookieJar).addInterceptor(AuthorizationInterceptor()) + + sRetrofit = Retrofit.Builder().baseUrl("https://nightly-alpha.carrene.com/apiv1/") + .client(okHttpClient.build()) + .addConverterFactory(GsonConverterFactory.create()) + .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) + .build() + } + return sRetrofit!! + } + + + fun getService(): ApiInterface? { + + if (sApi == null) { + sApi = getClient().create(ApiInterface::class.java) + } + return sApi + } + } + + private class AuthorizationInterceptor : Interceptor { + + internal var sharedPreferencesJwtPersistor = SharedPreferencesJwtPersistor(MyApplication.instance) + + @Throws(IOException::class) + override fun intercept(chain: Interceptor.Chain): Response { + + val token = sharedPreferencesJwtPersistor.get() + var request = chain.request() + + if (token != null) { + request = request.newBuilder().addHeader("Authorization", "Bearer $token").build() + } + + var response = chain.proceed(request) + + if (response.request().method() == "BIND" && response.code() == 200) { + + val responseBodyString = response.body()!!.string() + val contentType = response.body()!!.contentType() + val body = ResponseBody.create(contentType, responseBodyString) + response = response.newBuilder().body(body).build() + } + + val newJwtToken = response.header("X-New-JWT-Token") + + if (newJwtToken != null) { + sharedPreferencesJwtPersistor.save(newJwtToken) + } + + return response + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/de/netalic/peacock/data/webservice/ApiInterface.kt b/app/src/main/java/de/netalic/peacock/data/webservice/ApiInterface.kt new file mode 100644 index 0000000..f30c864 --- /dev/null +++ b/app/src/main/java/de/netalic/peacock/data/webservice/ApiInterface.kt @@ -0,0 +1,27 @@ +package de.netalic.peacock.data.webservice + + +import de.netalic.peacock.data.model.UserModel +import io.reactivex.Single +import okhttp3.ResponseBody +import retrofit2.Response +import retrofit2.http.Field +import retrofit2.http.FormUrlEncoded +import retrofit2.http.HTTP + + +interface ApiInterface { + + @FormUrlEncoded + @HTTP(method = "CLAIM", path = "login", hasBody = true) + fun claim(@Field("phone") phone: String, @Field("udid") udid: String): Single> + + + @FormUrlEncoded + @HTTP(method = "BIND",path ="login", hasBody = true) + fun bind(@Field("phone") phone:String , @Field("udid") udid:String , + @Field("deviceName") deviceName:String,@Field("deviceType") deviceType:String , + @Field("firebaseRegistrationId") firebaseRegistrationId:String, + @Field("activationCode") activationCode:String):Single> + +} \ No newline at end of file diff --git a/app/src/main/java/de/netalic/peacock/di/Modules.kt b/app/src/main/java/de/netalic/peacock/di/Modules.kt new file mode 100644 index 0000000..38d3224 --- /dev/null +++ b/app/src/main/java/de/netalic/peacock/di/Modules.kt @@ -0,0 +1,34 @@ +package de.netalic.peacock.di + +import de.netalic.peacock.data.repository.UserRepository +import de.netalic.peacock.data.webservice.ApiClient +import de.netalic.peacock.ui.login.pattern.PatternViewModel +import de.netalic.peacock.ui.registration.CodeVerificationViewModel +import de.netalic.peacock.ui.registration.RegistrationViewModel +import org.koin.android.viewmodel.dsl.viewModel +import org.koin.dsl.module + + +val repositoryModule = module { + single { + UserRepository(get()) + } +} + +val viewModelModule = module { + viewModel { + RegistrationViewModel(get()) + } + viewModel { PatternViewModel() } + + viewModel { + CodeVerificationViewModel(get()) + } + +} + +val apiModule = module { + single { + ApiClient.getService() + } +} \ No newline at end of file diff --git a/app/src/main/java/de/netalic/peacock/ui/base/BaseActivity.kt b/app/src/main/java/de/netalic/peacock/ui/base/BaseActivity.kt new file mode 100644 index 0000000..f066364 --- /dev/null +++ b/app/src/main/java/de/netalic/peacock/ui/base/BaseActivity.kt @@ -0,0 +1,19 @@ +package de.netalic.peacock.ui.base + +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity + +abstract class BaseActivity : AppCompatActivity() { + + protected abstract fun initUiComponents() + protected abstract fun initUiListeners() + protected abstract fun getLayoutResourceId(): Int + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(getLayoutResourceId()) + initUiComponents() + initUiListeners() + } + +} \ No newline at end of file diff --git a/app/src/main/java/de/netalic/peacock/ui/base/BaseFragment.kt b/app/src/main/java/de/netalic/peacock/ui/base/BaseFragment.kt new file mode 100644 index 0000000..5ce52a9 --- /dev/null +++ b/app/src/main/java/de/netalic/peacock/ui/base/BaseFragment.kt @@ -0,0 +1,20 @@ +package de.netalic.peacock.ui.base + +import android.os.Bundle +import android.view.View +import androidx.fragment.app.Fragment + +abstract class BaseFragment : Fragment() { + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + initUiComponents() + initUiListeners() + initObserver() + } + + protected abstract fun initUiComponents() + protected abstract fun initUiListeners() + protected abstract fun initObserver() + +} \ No newline at end of file diff --git a/app/src/main/java/de/netalic/peacock/ui/base/BaseViewModel.kt b/app/src/main/java/de/netalic/peacock/ui/base/BaseViewModel.kt new file mode 100644 index 0000000..c733cf5 --- /dev/null +++ b/app/src/main/java/de/netalic/peacock/ui/base/BaseViewModel.kt @@ -0,0 +1,15 @@ +package de.netalic.peacock.ui.base + +import androidx.lifecycle.ViewModel +import io.reactivex.disposables.CompositeDisposable + + +open class BaseViewModel : ViewModel() { + + var mCompositeDisposable = CompositeDisposable() + + override fun onCleared() { + super.onCleared() + mCompositeDisposable.clear() + } +} \ No newline at end of file diff --git a/app/src/main/java/de/netalic/peacock/ui/login/pattern/PatternFragment.kt b/app/src/main/java/de/netalic/peacock/ui/login/pattern/PatternFragment.kt new file mode 100644 index 0000000..aaf5137 --- /dev/null +++ b/app/src/main/java/de/netalic/peacock/ui/login/pattern/PatternFragment.kt @@ -0,0 +1,85 @@ +package de.netalic.peacock.ui.login.pattern + + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Toast +import androidx.lifecycle.Observer +import com.andrognito.patternlockview.PatternLockView +import com.andrognito.patternlockview.listener.PatternLockViewListener +import com.andrognito.patternlockview.utils.PatternLockUtils +import de.netalic.peacock.R +import de.netalic.peacock.ui.base.BaseFragment +import de.netalic.peacock.ui.main.MainHostActivity +import kotlinx.android.synthetic.main.fragment_patternlogin.* +import org.koin.android.viewmodel.ext.android.viewModel + + +class PatternFragment : BaseFragment(), PatternLockViewListener { + + + override fun initObserver() { + + } + + private val mImageViewProfile by lazy { imageView_patternLogin_profile } + private val mPatternLockView by lazy { patternLockView_patternLogin_pattern } + private val mTextViewMessage by lazy { textView_patternLogin_message } + + private val mPatternViewModel: PatternViewModel by viewModel() + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + return inflater.inflate(R.layout.fragment_patternlogin, container, false) + } + + override fun initUiComponents() { + updateToolbar() + mImageViewProfile.setImageResource(R.drawable.temp) + initObservers() + } + + private fun initObservers() { + mPatternViewModel.getResponse().observe(this, Observer { + + when (it.data) { + ResponseStatus.FIRST_SUCCESS -> mTextViewMessage.text = + getString(R.string.patternLogin_messageDrawAgain) + ResponseStatus.SECOND_SUCCESS -> { + Toast.makeText(requireContext(), "MATCH", Toast.LENGTH_LONG).show() + mPatternLockView.removePatternLockListener(this) + } + ResponseStatus.FAILED -> { + Toast.makeText(requireContext(), "Error, different patterns. Try again", Toast.LENGTH_LONG).show() + mTextViewMessage.text = getString(R.string.patternLogin_messageDraw) + } + } + mPatternLockView.clearPattern() + }) + } + + private fun updateToolbar() { + val activity = requireActivity() + if (activity is MainHostActivity) { + activity.updateToolbarTitle(getString(R.string.all_stepNOfFour, "1")) + } + } + + override fun initUiListeners() { + mPatternLockView.addPatternLockListener(this) + } + + override fun onComplete(pattern: MutableList?) { + val result = PatternLockUtils.patternToString(mPatternLockView, pattern) + mPatternViewModel.onPatternListener(result) + } + + override fun onCleared() {} + override fun onStarted() {} + override fun onProgress(progressPattern: MutableList?) {} + +} diff --git a/app/src/main/java/de/netalic/peacock/ui/login/pattern/PatternViewModel.kt b/app/src/main/java/de/netalic/peacock/ui/login/pattern/PatternViewModel.kt new file mode 100644 index 0000000..8e6a981 --- /dev/null +++ b/app/src/main/java/de/netalic/peacock/ui/login/pattern/PatternViewModel.kt @@ -0,0 +1,41 @@ +package de.netalic.peacock.ui.login.pattern + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import de.netalic.peacock.data.model.MyResponse +import de.netalic.peacock.ui.base.BaseViewModel + +enum class ResponseStatus { + FIRST_SUCCESS, + SECOND_SUCCESS, + FAILED +} + +class PatternViewModel : BaseViewModel() { + + private var mCounter = 1 + private var mPattern: String? = null + + private val mResponse = MutableLiveData>() + + fun getResponse(): LiveData> { + return mResponse + } + + fun onPatternListener(pattern: String) { + + if (mCounter == 1) { + mPattern = pattern + ++mCounter + mResponse.value = MyResponse.success(ResponseStatus.FIRST_SUCCESS) + } else { + if (mPattern == pattern) { + mResponse.value = MyResponse.success(ResponseStatus.SECOND_SUCCESS) + } else { + mCounter = 1 + mPattern = null + mResponse.value = MyResponse.success(ResponseStatus.FAILED) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/de/netalic/peacock/ui/main/MainHostActivity.kt b/app/src/main/java/de/netalic/peacock/ui/main/MainHostActivity.kt new file mode 100644 index 0000000..bcc90e9 --- /dev/null +++ b/app/src/main/java/de/netalic/peacock/ui/main/MainHostActivity.kt @@ -0,0 +1,29 @@ +package de.netalic.peacock.ui.main + +import de.netalic.peacock.R +import de.netalic.peacock.ui.base.BaseActivity +import kotlinx.android.synthetic.main.activity_mainhost.* + +class MainHostActivity : BaseActivity() { + + private val mToolbar by lazy { toolbar_mainHost_toolbar } + private val mTextViewToolbarTitle by lazy { textView_mainHost_toolbarTitle } + + override fun getLayoutResourceId() = R.layout.activity_mainhost + + override fun initUiComponents() { + setupToolbar() + } + + private fun setupToolbar() { + setSupportActionBar(mToolbar) + supportActionBar?.setDisplayShowTitleEnabled(false) + } + + override fun initUiListeners() {} + + fun updateToolbarTitle(title: String) { + mTextViewToolbarTitle.text = title + } + +} diff --git a/app/src/main/java/de/netalic/peacock/ui/registration/CodeVerificationFragment.kt b/app/src/main/java/de/netalic/peacock/ui/registration/CodeVerificationFragment.kt new file mode 100644 index 0000000..e27dacd --- /dev/null +++ b/app/src/main/java/de/netalic/peacock/ui/registration/CodeVerificationFragment.kt @@ -0,0 +1,195 @@ +package de.netalic.peacock.ui.registration + +import android.os.Bundle +import android.text.Editable +import android.text.TextWatcher +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.lifecycle.Observer +import de.netalic.peacock.R +import de.netalic.peacock.data.model.Status +import de.netalic.peacock.data.model.UserModel +import de.netalic.peacock.ui.base.BaseFragment +import de.netalic.peacock.util.CommonUtils +import kotlinx.android.synthetic.main.fragment_codeverification.* +import org.koin.android.viewmodel.ext.android.viewModel + + +class CodeVerificationFragment : BaseFragment(){ + + + private var mIsRunning: Boolean = false + + private lateinit var mView: View + + private val mCodeVerificationViewModel: CodeVerificationViewModel by viewModel() + + private val mTextViewIn by lazy { textView_codeVerification_in } + private val mTextViewResendCode by lazy { textView_codeVerification_resendTitle } + private val mPinEntryEditText by lazy { pinEntryEditText_codeVerification_setPin } + private val mButton by lazy { button_codeVerification_continue } + private val mTextViewTimer by lazy { textView_codeVerification_resendTime } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + + mView = inflater.inflate(R.layout.fragment_codeverification, container, false) + return mView + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + disableButton() + mCodeVerificationViewModel.setTimer() + } + + + private fun buttonListener() { + + mButton.setOnClickListener { bind() } + + } + + private fun resendCodeListener() { + + mTextViewResendCode.setOnClickListener { + + if (!mIsRunning) { + mCodeVerificationViewModel.setTimer() + mTextViewTimer.visibility = View.VISIBLE + mTextViewIn.visibility = View.VISIBLE + //We have to call claim here + + } + } + } + + override fun initUiComponents() { + + } + + override fun initUiListeners() { + + buttonListener() + resendCodeListener() + pinEntryEditTextListener() + } + + private fun pinEntryEditTextListener() { + + mPinEntryEditText.addTextChangedListener(object : TextWatcher { + override fun afterTextChanged(p0: Editable?) { + + } + + override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) { + + } + + override fun onTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) { + + if (p0!!.length == 6) { + + enableButton() + CommonUtils.hideSoftKeyboard(requireActivity()) + } else { + disableButton() + } + } + }) + } + + override fun initObserver() { + + observeBindLiveData() + observerTimerLiveData() + } + + + private fun bind() { + + mCodeVerificationViewModel.bind( + UserModel( + mName = "salimi", + mPhone = "+989211499302", + mUdid = "D89707AC55BAED9E8F23B826FB2A28E96095A190", + mFirebaseToken = "eyJ0eXAiOiAiSldUIiwgImFsZyI6ICJSUzI1NiIsICJraWQiOiAiODk0NTkyNzQzMzlkMzNlZmNmNTE3MD" + + "c4NGM5ZGU1MjUzMjEyOWVmZiJ9.eyJpc3MiOiAiZmlyZWJhc2UtYWRtaW5zZGstaXp1MTNAYWxwaGEtZDY0ZTQuaWFtLmdz" + + "ZXJ2aWNlYWNjb3VudC5jb20iLCAic3ViIjogImZpcmViYXNlLWFkbWluc2RrLWl6dTEzQGFscGhhLWQ2NGU0LmlhbS5nc" + + "2VydmljZWFjY291bnQuY29tIiwgImF1ZCI6ICJodHRwczovL2lkZW50aXR5dG9vbGtpdC5nb29nbGVhcGlzLmNvbS9n" + + "b29nbGUuaWRlbnRpdHkuaWRlbnRpdHl0b29sa2l0LnYxLklkZW50aXR5VG9vbGtpdCIsICJ1aWQiOiAiKzk4OTIxMTQ5O" + + "TMwMiIsICJpYXQiOiAxNTY0ODA4NDg2LCAiZXhwIjogMTU2NDgxMjA4Nn0.JE9aGvNfgHpBJVTlhoMoBqngpg4yia624O" + + "FZomnhgfA1-cywwjfT9Zpz1SSaQy0-Ldy_x_EHsq4w856QfHaqAeve8jl2UuvYNz54m8HfUAuiYnbnQJrtaZx7ybCOJn5" + + "QJf1PaRC0Zu0ZJDznV-xa6a3t7yA2T6acEzCy576HSb1CBF_E24NaVH-s5z0JgEXqhH6KlsO_zMo8vF7nhme9EcPxDYaC" + + "dm8LMg93oce2vjs63EdbrUuv_ilOpVjC4ziRTBbTtLX9NLLlgcEvHyN0uoeYsw2FfMlF6JDxDchvraRGQVGJjjATXcVJzg" + + "Clq2dzMJd27Oeo35MFq24voNt3lw", + mActivateToken = "700497", + mDeviceType = "android" + ) + ) + } + + + private fun observeBindLiveData() { + + mCodeVerificationViewModel.getBindLiveData().observe(this, Observer { + + when (it.status) { + Status.LOADING -> { + } + Status.SUCCESS -> { + } + Status.FAILED -> { + } + } + }) + } + + private fun observerTimerLiveData() { + + mCodeVerificationViewModel.getTimerLiveData().observe(this, Observer { + + when (it.data) { + + getString(R.string.codeVerification_resendCode) -> { + + onFinish() + } + + else -> { + + onTick(it.data!!) + } + } + }) + } + + private fun onTick(time: String) { + + mTextViewResendCode.isEnabled = false + mTextViewTimer.text = time + + } + + private fun onFinish() { + + mTextViewResendCode.isEnabled = true + mIsRunning = false + if (context != null) { + + mTextViewIn.visibility = View.GONE + mTextViewTimer.visibility = View.GONE + } + } + + private fun disableButton() { + + mButton.isEnabled = false + } + + private fun enableButton() { + + mButton.isEnabled = true + } +} \ No newline at end of file diff --git a/app/src/main/java/de/netalic/peacock/ui/registration/CodeVerificationViewModel.kt b/app/src/main/java/de/netalic/peacock/ui/registration/CodeVerificationViewModel.kt new file mode 100644 index 0000000..3253bbf --- /dev/null +++ b/app/src/main/java/de/netalic/peacock/ui/registration/CodeVerificationViewModel.kt @@ -0,0 +1,93 @@ +package de.netalic.peacock.ui.registration + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import de.netalic.peacock.data.exception.ActivationCodeIsNotValid +import de.netalic.peacock.data.exception.BadRequestException +import de.netalic.peacock.data.exception.InvalidDeviceName +import de.netalic.peacock.data.exception.InvalidUdidOrPhone +import de.netalic.peacock.data.model.MyResponse +import de.netalic.peacock.data.model.UserModel +import de.netalic.peacock.data.repository.UserRepository +import de.netalic.peacock.ui.base.BaseViewModel +import io.reactivex.Observable +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.schedulers.Schedulers +import okhttp3.ResponseBody +import java.util.concurrent.TimeUnit + + +class CodeVerificationViewModel(private val userRepository: UserRepository) : BaseViewModel() { + + + companion object { + + const val sResend = "RESEND" + } + + private val mBindResponseLiveData = MutableLiveData>() + private val mTimerLiveData = MutableLiveData>() + + fun getTimerLiveData(): LiveData> { + + return mTimerLiveData + } + + fun getBindLiveData(): LiveData> { + + return mBindResponseLiveData + } + + fun setTimer(time: Long = 30) { + + val timerDisposable = Observable.interval(1, TimeUnit.SECONDS) + .take(time) + .map { time - it } + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe({ + val minuteTimer = TimeUnit.SECONDS.toMinutes(it) + val secondTimer = TimeUnit.SECONDS.toSeconds(it) - + TimeUnit.MINUTES.toSeconds(TimeUnit.SECONDS.toMinutes(it)) + mTimerLiveData.value = MyResponse.success(String.format("%02d:%02d ", minuteTimer, secondTimer)) + }, {}, { + mTimerLiveData.value = MyResponse.success(sResend) + }) + mCompositeDisposable.add(timerDisposable) + } + + fun bind(user: UserModel) { + + val disposable = userRepository.bind(user) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .doOnSubscribe { mBindResponseLiveData.value = MyResponse.loading() } + .subscribe({ + + when (it.code()) { + + 200 -> { + mBindResponseLiveData.value = MyResponse.success(it.body()!!) + } + 400 -> { + mBindResponseLiveData.value = MyResponse.failed(BadRequestException()) + } + 710 -> { + mBindResponseLiveData.value = MyResponse.failed(InvalidUdidOrPhone()) + } + 711 -> { + mBindResponseLiveData.value = MyResponse.failed(ActivationCodeIsNotValid()) + } + 716 -> { + mBindResponseLiveData.value = MyResponse.failed(InvalidDeviceName()) + } + + } + }, + { + + }) + + mCompositeDisposable.add(disposable) + } +} \ No newline at end of file diff --git a/app/src/main/java/de/netalic/peacock/ui/registration/RegistrationFragment.kt b/app/src/main/java/de/netalic/peacock/ui/registration/RegistrationFragment.kt new file mode 100644 index 0000000..be31514 --- /dev/null +++ b/app/src/main/java/de/netalic/peacock/ui/registration/RegistrationFragment.kt @@ -0,0 +1,163 @@ +package de.netalic.peacock.ui.registration + +import android.os.Build +import android.os.Bundle +import android.text.Editable +import android.text.TextWatcher +import android.view.KeyEvent +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.Observer +import com.ehsanmashhadi.library.view.CountryPicker +import com.google.android.material.snackbar.Snackbar +import de.netalic.peacock.R +import de.netalic.peacock.data.model.Status +import de.netalic.peacock.ui.base.BaseFragment +import de.netalic.peacock.ui.main.MainHostActivity +import de.netalic.peacock.util.CommonUtils +import de.netalic.peacock.util.CustomPhoneFormatTextWatcherUtils +import de.netalic.peacock.util.PhoneInfoUtils +import kotlinx.android.synthetic.main.fragment_registration.* +import org.koin.android.viewmodel.ext.android.viewModel + + +class RegistrationFragment : BaseFragment() { + + + private lateinit var mCustomPhoneFormatTextWatcher: CustomPhoneFormatTextWatcherUtils + + private lateinit var mViewRoot: View + private val mPhoneInputViewModel: RegistrationViewModel by viewModel() + private val mPhoneInputEditText by lazy { editText_registration_phoneInput } + private val mCountryFlagImageView by lazy { imageButton_registration_flags } + private val mCountryCodeTextView by lazy { textView_registration_countryCode } + private val mContinueButton by lazy { button_registration_continue } + + private var mCountryCode = "IR" + private var mDialCode = "+98" + //ToDo-tina min and max size of phone number + private val mPhoneNumberMax = 4 + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + mViewRoot = inflater.inflate(R.layout.fragment_registration, container, false) + return mViewRoot + } + + override fun initUiListeners() { + mCountryFlagImageView.setOnClickListener { countryPicker() } + mContinueButton.setOnClickListener { + disableContinueButton() + claim() + } + mPhoneInputEditText.setOnKeyListener { _, keyCode, keyEvent -> + if (keyCode == KeyEvent.KEYCODE_ENTER && keyEvent.action + == KeyEvent.ACTION_DOWN + ) { + mContinueButton.callOnClick() + CommonUtils.hideSoftKeyboard(requireActivity()) + true + } else + false + } + } + + override fun initUiComponents() { + initToolbar() + phoneInputEditTextWatcher() + phoneFormat() + initObserver() + changeCountryImage("ir") + } + + private fun initToolbar() { + val activity = requireActivity() + if (activity is MainHostActivity) { + activity.updateToolbarTitle(getString(R.string.all_stepNOfFour, "2")) + } + } + + override fun initObserver() { + mPhoneInputViewModel.getClaimLiveData().observe(this, Observer { + + //ToDo-tina: get all status for + when (it.status) { + Status.FAILED -> enableContinueButton() + } + Snackbar.make(mViewRoot, it.status.toString(), Snackbar.LENGTH_LONG).show() + }) + } + + private fun claim() { + val inputEditText: String = mPhoneInputEditText.text.toString() + val phoneNumberNoCode = inputEditText.replace(" ", "") + val phoneNumber = mDialCode + phoneNumberNoCode + mPhoneInputViewModel.claim(phoneNumber, PhoneInfoUtils.getPhoneUdid(requireContext())) + } + + private fun countryPicker() { + val countryPicker = CountryPicker.Builder(requireContext()).setCountrySelectionListener { country -> + + mCountryCode = country.code + mDialCode = country.dialCode + changeCountryImage(country.flagName) + setCodeDialText() + mPhoneInputEditText.setText("") + + }.showingFlag(true) + .showingDialCode(true) + .enablingSearch(true) + .setPreSelectedCountry("iran") + .setViewType(CountryPicker.ViewType.BOTTOMSHEET) + .build() + + countryPicker.show(activity as AppCompatActivity?) + } + + + private fun changeCountryImage(flagName: String) { + val iconResId = resources.getIdentifier(flagName, "drawable", activity?.packageName) + mCountryFlagImageView.setImageResource(iconResId) + } + + private fun setCodeDialText() { + val dialCodeText = + getString(R.string.registration_countryCode, mDialCode) + mCountryCodeTextView.text = dialCodeText + } + + private fun phoneInputEditTextWatcher() { + mPhoneInputEditText.addTextChangedListener(object : TextWatcher { + override fun afterTextChanged(p0: Editable?) { + } + + override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) { + } + + override fun onTextChanged(characters: CharSequence?, p1: Int, p2: Int, p3: Int) { + if (characters != null && characters.length > mPhoneNumberMax) + enableContinueButton() + else + disableContinueButton() + } + }) + } + + private fun phoneFormat() { + mCustomPhoneFormatTextWatcher = + CustomPhoneFormatTextWatcherUtils(mCountryCode, mDialCode) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + mPhoneInputEditText.removeTextChangedListener(mCustomPhoneFormatTextWatcher) + mPhoneInputEditText.addTextChangedListener(mCustomPhoneFormatTextWatcher) + } + } + + private fun enableContinueButton() { + mContinueButton.isEnabled = true + } + + private fun disableContinueButton() { + mContinueButton.isEnabled = false + } +} \ No newline at end of file diff --git a/app/src/main/java/de/netalic/peacock/ui/registration/RegistrationViewModel.kt b/app/src/main/java/de/netalic/peacock/ui/registration/RegistrationViewModel.kt new file mode 100644 index 0000000..526f042 --- /dev/null +++ b/app/src/main/java/de/netalic/peacock/ui/registration/RegistrationViewModel.kt @@ -0,0 +1,47 @@ +package de.netalic.peacock.ui.registration + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import de.netalic.peacock.data.exception.BadRequestException +import de.netalic.peacock.data.exception.InvalidPhoneNumberException +import de.netalic.peacock.data.exception.InvalidUdidOrPhoneException +import de.netalic.peacock.data.exception.ServerException +import de.netalic.peacock.data.model.MyResponse +import de.netalic.peacock.data.model.UserModel +import de.netalic.peacock.data.repository.UserRepository +import de.netalic.peacock.ui.base.BaseViewModel +import de.netalic.peacock.util.ValidatorUtils +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.schedulers.Schedulers + +class RegistrationViewModel(private val userRepository: UserRepository) : BaseViewModel() { + + private val mClaimResponseLiveData = MutableLiveData>() + + fun getClaimLiveData(): LiveData> { + return mClaimResponseLiveData + } + + fun claim(phone: String, udid: String) { + if (ValidatorUtils.phoneValidator(phone)) { + val disposable = userRepository.claim(phone, udid) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .doOnSubscribe { mClaimResponseLiveData.value = MyResponse.loading() } + .subscribe({ + when (it.code()) { + 200 -> mClaimResponseLiveData.value = MyResponse.success(it.body()!!) + 400 -> mClaimResponseLiveData.value = MyResponse.failed(BadRequestException()) + 500 -> mClaimResponseLiveData.value = MyResponse.failed(ServerException()) + 710 -> mClaimResponseLiveData.value = + MyResponse.failed(InvalidUdidOrPhoneException()) + } + }, { throwable -> + mClaimResponseLiveData.value = MyResponse.failed(throwable) + }) + mCompositeDisposable.add(disposable) + } else + mClaimResponseLiveData.value = MyResponse.failed(InvalidPhoneNumberException()) + } + +} \ No newline at end of file diff --git a/app/src/main/java/de/netalic/peacock/util/CommonUtils.kt b/app/src/main/java/de/netalic/peacock/util/CommonUtils.kt new file mode 100644 index 0000000..99a819d --- /dev/null +++ b/app/src/main/java/de/netalic/peacock/util/CommonUtils.kt @@ -0,0 +1,20 @@ +package de.netalic.peacock.util + +import android.app.Activity +import android.view.inputmethod.InputMethodManager +import androidx.fragment.app.FragmentActivity + + +class CommonUtils { + + companion object { + fun hideSoftKeyboard(activity: FragmentActivity) { + val inputMethodManager = activity.getSystemService( + Activity.INPUT_METHOD_SERVICE + ) as InputMethodManager + inputMethodManager.hideSoftInputFromWindow( + activity.currentFocus!!.windowToken, 0 + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/de/netalic/peacock/util/CustomPhoneFormatTextWatcherUtils.java b/app/src/main/java/de/netalic/peacock/util/CustomPhoneFormatTextWatcherUtils.java new file mode 100644 index 0000000..b614416 --- /dev/null +++ b/app/src/main/java/de/netalic/peacock/util/CustomPhoneFormatTextWatcherUtils.java @@ -0,0 +1,187 @@ +package de.netalic.peacock.util; + +import android.telephony.PhoneNumberUtils; +import android.text.Editable; +import android.text.Selection; +import android.text.TextWatcher; +import android.util.Log; +import com.google.i18n.phonenumbers.AsYouTypeFormatter; +import com.google.i18n.phonenumbers.PhoneNumberUtil; + +import java.util.Locale; + +/** + * Watches a {@link android.widget.TextView} and if a phone number is entered + * will format it. + *

+ * Stop formatting when the user + *

    + *
  • Inputs non-dialable characters
  • + *
  • Removes the separator in the middle of string.
  • + *
+ *

+ * The formatting will be restarted once the text is cleared. + * This class get phone code and locale + */ +public class CustomPhoneFormatTextWatcherUtils implements TextWatcher { + + /** + * Indicates the change was caused by ourselves. + */ + private boolean mSelfChange = false; + + /** + * Indicates the formatting has been stopped. + */ + private boolean mStopFormatting; + + private AsYouTypeFormatter mFormatter; + + + private String code; + + /** + * The formatting is based on the current system locale and future locale changes + * may not take effect on this instance. + */ + public CustomPhoneFormatTextWatcherUtils() { + + this(Locale.getDefault().getCountry(), ""); + } + + /** + * The formatting is based on the given countryCode. + * + * @param countryCode the ISO 3166-1 two-letter country code that indicates the country/region + * where the phone number is being entered. + * @hide + */ + public CustomPhoneFormatTextWatcherUtils(String countryCode, String code) { + + if (countryCode == null) throw new IllegalArgumentException(); + mFormatter = PhoneNumberUtil.getInstance().getAsYouTypeFormatter(countryCode); + this.code = code; + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, + int after) { + + if (mSelfChange || mStopFormatting) { + return; + } + // If the user manually deleted any non-dialable characters, stop formatting + if (count > 0 && hasSeparator(s, start, count)) { + stopFormatting(); + } + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + + if (mSelfChange || mStopFormatting) { + return; + } + // If the user inserted any non-dialable characters, stop formatting + if (count > 0 && hasSeparator(s, start, count)) { + stopFormatting(); + } + } + + @Override + public synchronized void afterTextChanged(Editable s) { + + if (mStopFormatting) { + // Restart the formatting when all texts were clear. + mStopFormatting = !(s.length() == 0); + return; + } + if (mSelfChange) { + // Ignore the change caused by s.replace(). + return; + } + String formatted = reformat(s, Selection.getSelectionEnd(s)); + if (formatted != null) { + int rememberedPos = formatted.length(); + Log.v("rememberedPos", "" + rememberedPos); + mSelfChange = true; + s.replace(0, s.length(), formatted, 0, formatted.length()); + + + // The text could be changed by other TextWatcher after we changed it. If we found the + // text is not the one we were expecting, just give up calling setSelection(). + if (formatted.equals(s.toString())) { + Selection.setSelection(s, rememberedPos); + } + mSelfChange = false; + } + } + + /** + * Generate the formatted number by ignoring all non-dialable chars and stick the cursor to the + * nearest dialable char to the left. For instance, if the number is (650) 123-45678 and '4' is + * removed then the cursor should be behind '3' instead of '-'. + */ + private String reformat(CharSequence s, int cursor) { + // The index of char to the leftward of the cursor. + int curIndex = cursor - 1; + String formatted = null; + mFormatter.clear(); + char lastNonSeparator = 0; + boolean hasCursor = false; + + String countryCallingCode = this.code; + s = countryCallingCode + s; + int len = s.length(); + for (int i = 0; i < len; i++) { + char c = s.charAt(i); + if (PhoneNumberUtils.isNonSeparator(c)) { + if (lastNonSeparator != 0) { + formatted = getFormattedNumber(lastNonSeparator, hasCursor); + hasCursor = false; + } + lastNonSeparator = c; + } + if (i == curIndex) { + hasCursor = true; + } + } + if (lastNonSeparator != 0) { + Log.v("lastNonSeparator", "" + lastNonSeparator); + formatted = getFormattedNumber(lastNonSeparator, hasCursor); + } + + if (formatted.length() > countryCallingCode.length()) { + if (formatted.charAt(countryCallingCode.length()) == ' ') + return formatted.substring(countryCallingCode.length() + 1); + return formatted.substring(countryCallingCode.length()); + } + + return formatted.substring(formatted.length()); + } + + private String getFormattedNumber(char lastNonSeparator, boolean hasCursor) { + + return hasCursor ? mFormatter.inputDigitAndRememberPosition(lastNonSeparator) + : mFormatter.inputDigit(lastNonSeparator); + } + + private void stopFormatting() { + + mStopFormatting = true; + mFormatter.clear(); + } + + private boolean hasSeparator(final CharSequence s, final int start, final int count) { + + for (int i = start; i < start + count; i++) { + char c = s.charAt(i); + if (!PhoneNumberUtils.isNonSeparator(c)) { + return true; + } + } + return false; + } + +} + diff --git a/app/src/main/java/de/netalic/peacock/util/PhoneInfoUtils.kt b/app/src/main/java/de/netalic/peacock/util/PhoneInfoUtils.kt new file mode 100644 index 0000000..a880a72 --- /dev/null +++ b/app/src/main/java/de/netalic/peacock/util/PhoneInfoUtils.kt @@ -0,0 +1,34 @@ +package de.netalic.peacock.util + +import android.content.Context +import android.provider.Settings +import java.security.MessageDigest + + +class PhoneInfoUtils { + + companion object { + private const val sSHA_1 = "SHA-1" + private const val sHEX_CHARS = "0123456789ABCDEF" + + fun getPhoneUdid(context: Context): String { + val phoneId = Settings.System.getString(context.contentResolver, Settings.Secure.ANDROID_ID) + + return hashString(sSHA_1, phoneId) + } + + private fun hashString(type: String, input: String): String { + val bytes = MessageDigest.getInstance(type).digest(input.toByteArray()) + val result = StringBuilder(bytes.size * 2) + + bytes.forEach { + val i = it.toInt() + result.append(sHEX_CHARS[i shr 4 and 0x0f]) + result.append(sHEX_CHARS[i and 0x0f]) + } + + return result.toString() + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/de/netalic/peacock/util/ValidatorUtils.kt b/app/src/main/java/de/netalic/peacock/util/ValidatorUtils.kt new file mode 100644 index 0000000..3ce4533 --- /dev/null +++ b/app/src/main/java/de/netalic/peacock/util/ValidatorUtils.kt @@ -0,0 +1,13 @@ +package de.netalic.peacock.util + +class ValidatorUtils { + + companion object { + //ToDo-tina min and max size of phone number + private val mPhonePatternMarcher = "[+0-9 ]{5,15}".toRegex() + + fun phoneValidator(phoneNumber: String): Boolean { + return mPhonePatternMarcher.matches(phoneNumber) + } + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/all_buttonselector.xml b/app/src/main/res/drawable/all_buttonselector.xml new file mode 100644 index 0000000..0709fdb --- /dev/null +++ b/app/src/main/res/drawable/all_buttonselector.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/codeverification_buttonselector.xml b/app/src/main/res/drawable/codeverification_buttonselector.xml new file mode 100644 index 0000000..4903637 --- /dev/null +++ b/app/src/main/res/drawable/codeverification_buttonselector.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/codeverification_buttontextcolorselector.xml b/app/src/main/res/drawable/codeverification_buttontextcolorselector.xml new file mode 100644 index 0000000..25ddc61 --- /dev/null +++ b/app/src/main/res/drawable/codeverification_buttontextcolorselector.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/codeverification_resendtextcolorselector.xml b/app/src/main/res/drawable/codeverification_resendtextcolorselector.xml new file mode 100644 index 0000000..4a5a222 --- /dev/null +++ b/app/src/main/res/drawable/codeverification_resendtextcolorselector.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/everywhere_colorprimarydarkbackgroundwithcornerradius.xml b/app/src/main/res/drawable/everywhere_colorprimarydarkbackgroundwithcornerradius.xml new file mode 100644 index 0000000..5744224 --- /dev/null +++ b/app/src/main/res/drawable/everywhere_colorprimarydarkbackgroundwithcornerradius.xml @@ -0,0 +1,8 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/everywhere_colortertiarybackgroundwithcornerradius.xml b/app/src/main/res/drawable/everywhere_colortertiarybackgroundwithcornerradius.xml new file mode 100644 index 0000000..802d56f --- /dev/null +++ b/app/src/main/res/drawable/everywhere_colortertiarybackgroundwithcornerradius.xml @@ -0,0 +1,8 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/everywhere_icon.xml b/app/src/main/res/drawable/everywhere_icon.xml new file mode 100644 index 0000000..0e30870 --- /dev/null +++ b/app/src/main/res/drawable/everywhere_icon.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/everywhere_whitbackgroundwithcornerradius.xml b/app/src/main/res/drawable/everywhere_whitbackgroundwithcornerradius.xml new file mode 100644 index 0000000..63c3282 --- /dev/null +++ b/app/src/main/res/drawable/everywhere_whitbackgroundwithcornerradius.xml @@ -0,0 +1,8 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_arrow_forward.xml b/app/src/main/res/drawable/ic_arrow_forward.xml new file mode 100644 index 0000000..f233031 --- /dev/null +++ b/app/src/main/res/drawable/ic_arrow_forward.xml @@ -0,0 +1,4 @@ + + + diff --git a/app/src/main/res/drawable/registration_buttonovalshapepurple.xml b/app/src/main/res/drawable/registration_buttonovalshapepurple.xml new file mode 100644 index 0000000..1e9f1dc --- /dev/null +++ b/app/src/main/res/drawable/registration_buttonovalshapepurple.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/registration_buttonovalshapewhite.xml b/app/src/main/res/drawable/registration_buttonovalshapewhite.xml new file mode 100644 index 0000000..fe6c3d0 --- /dev/null +++ b/app/src/main/res/drawable/registration_buttonovalshapewhite.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/registration_circleshape.xml b/app/src/main/res/drawable/registration_circleshape.xml new file mode 100644 index 0000000..6063613 --- /dev/null +++ b/app/src/main/res/drawable/registration_circleshape.xml @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/registration_cornershape.xml b/app/src/main/res/drawable/registration_cornershape.xml new file mode 100644 index 0000000..aa08eed --- /dev/null +++ b/app/src/main/res/drawable/registration_cornershape.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/app/src/main/res/drawable/temp.xml b/app/src/main/res/drawable/temp.xml new file mode 100644 index 0000000..3b80fe6 --- /dev/null +++ b/app/src/main/res/drawable/temp.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/font/roboto_bold.ttf b/app/src/main/res/font/roboto_bold.ttf new file mode 100644 index 0000000..e612852 Binary files /dev/null and b/app/src/main/res/font/roboto_bold.ttf differ diff --git a/app/src/main/res/font/roboto_medium.ttf b/app/src/main/res/font/roboto_medium.ttf new file mode 100644 index 0000000..86d1c52 Binary files /dev/null and b/app/src/main/res/font/roboto_medium.ttf differ diff --git a/app/src/main/res/font/roboto_regular.ttf b/app/src/main/res/font/roboto_regular.ttf new file mode 100644 index 0000000..cb8ffcf Binary files /dev/null and b/app/src/main/res/font/roboto_regular.ttf differ diff --git a/app/src/main/res/layout/activity_mainhost.xml b/app/src/main/res/layout/activity_mainhost.xml new file mode 100644 index 0000000..8c01cfc --- /dev/null +++ b/app/src/main/res/layout/activity_mainhost.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_codeverification.xml b/app/src/main/res/layout/fragment_codeverification.xml new file mode 100644 index 0000000..a2485ac --- /dev/null +++ b/app/src/main/res/layout/fragment_codeverification.xml @@ -0,0 +1,147 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_patternlogin.xml b/app/src/main/res/layout/fragment_patternlogin.xml new file mode 100644 index 0000000..8c643d9 --- /dev/null +++ b/app/src/main/res/layout/fragment_patternlogin.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_registration.xml b/app/src/main/res/layout/fragment_registration.xml new file mode 100644 index 0000000..5e444d7 --- /dev/null +++ b/app/src/main/res/layout/fragment_registration.xml @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/navigation/graph_main.xml b/app/src/main/res/navigation/graph_main.xml new file mode 100644 index 0000000..2ee21ca --- /dev/null +++ b/app/src/main/res/navigation/graph_main.xml @@ -0,0 +1,10 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-v23/styles.xml b/app/src/main/res/values-v23/styles.xml new file mode 100644 index 0000000..3fc33ac --- /dev/null +++ b/app/src/main/res/values-v23/styles.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 69b2233..e84377c 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -1,6 +1,22 @@ - #008577 - #00574B + #F4F3F5 + #E6E6E6 + #FFFFFF + #D81B60 + + #7A6AE9 + #D4D1F1 + + #000000 + + #00141E + #F2F1F6 + #8B989F + #91999C + + #F2F1F6 + + diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml new file mode 100644 index 0000000..3fb67f4 --- /dev/null +++ b/app/src/main/res/values/dimens.xml @@ -0,0 +1,43 @@ + + + + 16sp + 16sp + 16sp + 14sp + 14sp + 12sp + + 8dp + 16dp + 24dp + 32dp + + 8dp + + 0dp + + 0dp + 90dp + + 3 + + 25dp + + + 8dp + 5dp + 100dp + 28dp + 20dp + 10dp + 16dp + 30dp + 2dp + 4dp + + 10dp + + 20sp + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2a2f6f0..9496d3c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,3 +1,26 @@ - + Peacock + Authorization + Draw a sample pattern for more secure + Draw your pattern again + Switch to Password + - + Registration + Enter your phone number, we will send you OTP to verify later. + Continue + - - - - - - - - - - + (+98) + (%1$s) + Step %1$s/4 + + VALUE + + Steps 3/4 + Code Verification + Enter 6 digit number that sent to + Continue + Re_Send code + RESEND + in + diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 5885930..d8146a9 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -1,11 +1,100 @@ - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/test/java/de/netalic/peacock/ExampleUnitTest.kt b/app/src/test/java/de/netalic/peacock/ExampleUnitTest.kt deleted file mode 100644 index 5935b93..0000000 --- a/app/src/test/java/de/netalic/peacock/ExampleUnitTest.kt +++ /dev/null @@ -1,17 +0,0 @@ -package de.netalic.peacock - -import org.junit.Test - -import org.junit.Assert.* - -/** - * Example local unit test, which will execute on the development machine (host). - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -class ExampleUnitTest { - @Test - fun addition_isCorrect() { - assertEquals(4, 2 + 2) - } -} diff --git a/app/src/test/java/de/netalic/peacock/base/BaseTest.kt b/app/src/test/java/de/netalic/peacock/base/BaseTest.kt new file mode 100644 index 0000000..0990899 --- /dev/null +++ b/app/src/test/java/de/netalic/peacock/base/BaseTest.kt @@ -0,0 +1,50 @@ +package de.netalic.peacock.base + +import io.reactivex.Scheduler +import io.reactivex.android.plugins.RxAndroidPlugins +import io.reactivex.disposables.Disposable +import io.reactivex.internal.schedulers.ExecutorScheduler +import io.reactivex.plugins.RxJavaPlugins +import org.junit.AfterClass +import org.junit.BeforeClass +import java.util.concurrent.Executor +import java.util.concurrent.TimeUnit + +open class BaseTest { + + companion object { + + @JvmStatic + @BeforeClass + fun setUpClass() { + + val immediate = object : Scheduler() { + override fun scheduleDirect(run: Runnable, delay: Long, unit: TimeUnit): Disposable { + // this prevents StackOverflowErrors when scheduling with a delay + return super.scheduleDirect(run, 0, unit) + } + + override fun createWorker(): Worker { + return ExecutorScheduler.ExecutorWorker(Executor { it.run() }, true) + } + } + RxJavaPlugins.setInitIoSchedulerHandler { scheduler -> immediate } + RxJavaPlugins.setInitComputationSchedulerHandler { scheduler -> immediate } + RxJavaPlugins.setInitNewThreadSchedulerHandler { scheduler -> immediate } + RxJavaPlugins.setInitSingleSchedulerHandler { scheduler -> immediate } + RxAndroidPlugins.setInitMainThreadSchedulerHandler { scheduler -> immediate } + } + + @JvmStatic + @AfterClass + fun tearDownClass() { + RxJavaPlugins.reset() + RxAndroidPlugins.reset() + } + + fun resetSchedulers() { + setUpClass() + } + + } +} \ No newline at end of file diff --git a/app/src/test/java/de/netalic/peacock/data/repository/UserRepositoryTest.kt b/app/src/test/java/de/netalic/peacock/data/repository/UserRepositoryTest.kt new file mode 100644 index 0000000..7bab5b8 --- /dev/null +++ b/app/src/test/java/de/netalic/peacock/data/repository/UserRepositoryTest.kt @@ -0,0 +1,69 @@ +package de.netalic.peacock.data.repository + +import de.netalic.peacock.base.BaseTest +import de.netalic.peacock.data.model.UserModel +import de.netalic.peacock.data.webservice.ApiInterface +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.mockito.Mock +import org.mockito.Mockito +import org.mockito.MockitoAnnotations + +class UserRepositoryTest : BaseTest() { + + val sUser = UserModel( + + mName = "salimi", + mPhone = "+989211499302", + mUdid = "D89707AC55BAED9E8F23B826FB2A28E96095A190", + mFirebaseToken = "eyJ0eXAiOiAiSldUIiwgImFsZyI6ICJSUzI1NiIsICJraWQiOiAiODk0NTkyNzQzMzlkMzNlZmNmNTE3MDc" + + "4NGM5ZGU1MjUzMjEyOWVmZiJ9.eyJpc3MiOiAiZmlyZWJhc2UtYWRtaW5zZGstaXp1MTNAYWxwaGEtZDY0ZTQuaWFtLmd" + + "zZXJ2aWNlYWNjb3VudC5jb20iLCAic3ViIjogImZpcmViYXNlLWFkbWluc2RrLWl6dTEzQGFscGhhLWQ2NGU0LmlhbS5nc" + + "2VydmljZWFjY291bnQuY29tIiwgImF1ZCI6ICJodHRwczovL2lkZW50aXR5dG9vbGtpdC5nb29nbGVhcGlzLmNvbS9nb" + + "29nbGUuaWRlbnRpdHkuaWRlbnRpdHl0b29sa2l0LnYxLklkZW50aXR5VG9vbGtpdCIsICJ1aWQiOiAiKzk4OTIxMTQ5OTM" + + "wMiIsICJpYXQiOiAxNTYzOTYwNDU3LCAiZXhwIjogMTU2Mzk2NDA1N30.HOUVBzwbmGwsglQHukGwrijlUuSZ241KdN2Eo" + + "l3Gy80mmd4Kxoc58m3VhL71AWv3WS99eE7uz6xctl--yLPilhN3WJ_z2nxySqkhxiZ9OtaH_U8sTek63SJgfINeTFzJFp" + + "WHkT_DlQNPTVoH_AqbXjh0gZwdpVdMyoLmmuJf-WIqx2y7BdwudCTiAqY_RoK7DdDwS8Jf28J-czpWi7Q4neUo1pC0WLi" + + "986u9n0mZcfIhWoVB_fV0A2-fWRV6yhT647sfHntC2eSg-OJZKO-MAyBsgKDIZm_ubX7m3LHD6rahpnUHtY8m33eJyD-" + + "EfZcKboRWalJkmje69abirvep1A", + mActivateToken = "082016", + mDeviceType = "android" + ) + + + private lateinit var mUserRepository: UserRepository + + @Mock + private lateinit var apiInterface: ApiInterface + + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + mUserRepository = UserRepository(apiInterface) + } + + @After + fun tearDown() { + Mockito.reset(apiInterface) + } + + @Test + fun claimUser_claimToApi() { + mUserRepository.claim(sUser.mPhone, sUser.mUdid) + Mockito.verify(apiInterface).claim(sUser.mPhone, sUser.mUdid) + } + + @Test + fun bindUser_bindToApi() { + + mUserRepository.bind(sUser) + Mockito.verify(apiInterface).bind( + sUser.mPhone, sUser.mUdid, sUser.mName!!, sUser.mDeviceType, + sUser.mFirebaseToken, sUser.mActivateToken + ) + + } + +} diff --git a/app/src/test/java/de/netalic/peacock/ui/login/pattern/PatternViewModelTest.kt b/app/src/test/java/de/netalic/peacock/ui/login/pattern/PatternViewModelTest.kt new file mode 100644 index 0000000..7671c2d --- /dev/null +++ b/app/src/test/java/de/netalic/peacock/ui/login/pattern/PatternViewModelTest.kt @@ -0,0 +1,55 @@ +package de.netalic.peacock.ui.login.pattern + +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import de.netalic.peacock.util.LiveDataTestUtil +import org.junit.Assert +import org.junit.Before +import org.junit.Rule +import org.junit.Test + +class PatternViewModelTest { + + @get:Rule + var instantExecutorRule = InstantTaskExecutorRule() + + private lateinit var mPatternViewModel: PatternViewModel + + @Before + fun setUp() { + mPatternViewModel = PatternViewModel() + } + + @Test + fun patternViewModel_drawPatternSuccess() { + + val pattern = "123456" + mPatternViewModel.onPatternListener(pattern) + Assert.assertEquals( + LiveDataTestUtil.getValue(mPatternViewModel.getResponse()).data, + ResponseStatus.FIRST_SUCCESS + ) + mPatternViewModel.onPatternListener(pattern) + Assert.assertEquals( + LiveDataTestUtil.getValue(mPatternViewModel.getResponse()).data, + ResponseStatus.SECOND_SUCCESS + ) + } + + @Test + fun patternViewModel_drawPatternFailed() { + + val firstPattern = "123456" + val secondPattern = "654321" + + mPatternViewModel.onPatternListener(firstPattern) + Assert.assertEquals( + LiveDataTestUtil.getValue(mPatternViewModel.getResponse()).data, + ResponseStatus.FIRST_SUCCESS + ) + mPatternViewModel.onPatternListener(secondPattern) + Assert.assertEquals( + LiveDataTestUtil.getValue(mPatternViewModel.getResponse()).data, + ResponseStatus.FAILED + ) + } +} \ No newline at end of file diff --git a/app/src/test/java/de/netalic/peacock/ui/registration/CodeVerificationViewModelTest.kt b/app/src/test/java/de/netalic/peacock/ui/registration/CodeVerificationViewModelTest.kt new file mode 100644 index 0000000..ff8230e --- /dev/null +++ b/app/src/test/java/de/netalic/peacock/ui/registration/CodeVerificationViewModelTest.kt @@ -0,0 +1,285 @@ +package de.netalic.peacock.ui.registration + +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import de.netalic.peacock.base.BaseTest +import de.netalic.peacock.data.exception.ActivationCodeIsNotValid +import de.netalic.peacock.data.exception.BadRequestException +import de.netalic.peacock.data.exception.InvalidDeviceName +import de.netalic.peacock.data.exception.InvalidUdidOrPhone +import de.netalic.peacock.data.model.Status +import de.netalic.peacock.data.model.UserModel +import de.netalic.peacock.data.repository.UserRepository +import de.netalic.peacock.util.LiveDataTestUtil +import io.reactivex.Single +import io.reactivex.plugins.RxJavaPlugins +import io.reactivex.schedulers.TestScheduler +import io.reactivex.subjects.PublishSubject +import okhttp3.MediaType +import okhttp3.ResponseBody +import org.junit.Assert +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.mockito.Mock +import org.mockito.Mockito +import org.mockito.MockitoAnnotations +import retrofit2.Response +import java.util.concurrent.TimeUnit + + +class CodeVerificationViewModelTest : BaseTest() { + + + @get:Rule + val instantExecutorRule = InstantTaskExecutorRule() + + @Mock + private lateinit var mUserRepository: UserRepository + + private lateinit var mCodeVerificationViewModel: CodeVerificationViewModel + + private val mResponseBody = ResponseBody.create( + MediaType.parse("text/plain"), "" + ) + private val mUser = UserModel( + + mName = "salimi", + mPhone = "+989211499302", + mUdid = "D89707AC55BAED9E8F23B826FB2A28E96095A190", + mFirebaseToken = "eyJ0eXAiOiAiSldUIiwgImFsZyI6ICJSUzI1NiIsICJraWQiOiAiODk0NTkyNzQzMzlkMzNlZmNmNTE3MDc" + + "4NGM5ZGU1MjUzMjEyOWVmZiJ9.eyJpc3MiOiAiZmlyZWJhc2UtYWRtaW5zZGstaXp1MTNAYWxwaGEtZDY0ZTQuaWFtLmd" + + "zZXJ2aWNlYWNjb3VudC5jb20iLCAic3ViIjogImZpcmViYXNlLWFkbWluc2RrLWl6dTEzQGFscGhhLWQ2NGU0LmlhbS5nc" + + "2VydmljZWFjY291bnQuY29tIiwgImF1ZCI6ICJodHRwczovL2lkZW50aXR5dG9vbGtpdC5nb29nbGVhcGlzLmNvbS9nb" + + "29nbGUuaWRlbnRpdHkuaWRlbnRpdHl0b29sa2l0LnYxLklkZW50aXR5VG9vbGtpdCIsICJ1aWQiOiAiKzk4OTIxMTQ5OTM" + + "wMiIsICJpYXQiOiAxNTYzOTYwNDU3LCAiZXhwIjogMTU2Mzk2NDA1N30.HOUVBzwbmGwsglQHukGwrijlUuSZ241KdN2Eo" + + "l3Gy80mmd4Kxoc58m3VhL71AWv3WS99eE7uz6xctl--yLPilhN3WJ_z2nxySqkhxiZ9OtaH_U8sTek63SJgfINeTFzJFp" + + "WHkT_DlQNPTVoH_AqbXjh0gZwdpVdMyoLmmuJf-WIqx2y7BdwudCTiAqY_RoK7DdDwS8Jf28J-czpWi7Q4neUo1pC0WLi" + + "986u9n0mZcfIhWoVB_fV0A2-fWRV6yhT647sfHntC2eSg-OJZKO-MAyBsgKDIZm_ubX7m3LHD6rahpnUHtY8m33eJyD-" + + "EfZcKboRWalJkmje69abirvep1A", + mActivateToken = "082016", + mDeviceType = "android" + ) + + @Before + fun setup() { + + MockitoAnnotations.initMocks(this) + mCodeVerificationViewModel = CodeVerificationViewModel(mUserRepository) + + } + + @Test + fun binUser_showSuccess() { + + Mockito.`when`(mUserRepository.bind(mUser)).thenReturn(Single.just(Response.success(mResponseBody))) + mCodeVerificationViewModel.bind(mUser) + Mockito.verify(mUserRepository).bind(mUser) + Assert.assertEquals( + LiveDataTestUtil.getValue(mCodeVerificationViewModel.getBindLiveData()).status, + Status.SUCCESS + ) + + Assert.assertEquals(LiveDataTestUtil.getValue(mCodeVerificationViewModel.getBindLiveData()).data, mResponseBody) + Assert.assertNull(LiveDataTestUtil.getValue(mCodeVerificationViewModel.getBindLiveData()).throwable) + } + + @Test + fun bindUser_showBadRequest() { + + val delayer = PublishSubject.create() + + val singleResponse = Single.just( + Response.error(400, mResponseBody) + ).delaySubscription(delayer) + + Mockito.`when`(mUserRepository.bind(mUser)).thenReturn(singleResponse) + + mCodeVerificationViewModel.bind(mUser) + + Mockito.verify(mUserRepository).bind(mUser) + + Assert.assertEquals( + LiveDataTestUtil.getValue(mCodeVerificationViewModel.getBindLiveData()).status, + Status.LOADING + ) + + delayer.onComplete() + + Assert.assertEquals( + LiveDataTestUtil.getValue(mCodeVerificationViewModel.getBindLiveData()).status, + Status.FAILED + ) + + Assert.assertEquals( + LiveDataTestUtil.getValue(mCodeVerificationViewModel.getBindLiveData()) + .throwable!!::class.java, BadRequestException::class.java + ) + + Assert.assertEquals( + LiveDataTestUtil.getValue(mCodeVerificationViewModel.getBindLiveData()).data, + null + ) + } + + @Test + fun bindUser_invalidUdidOrPhone() { + + val delayer = PublishSubject.create() + + val singleResponse = Single.just( + Response.error(710, mResponseBody) + ).delaySubscription(delayer) + + Mockito.`when`(mUserRepository.bind(mUser)).thenReturn(singleResponse) + + mCodeVerificationViewModel.bind(mUser) + + Mockito.verify(mUserRepository).bind(mUser) + + Assert.assertEquals( + LiveDataTestUtil.getValue(mCodeVerificationViewModel.getBindLiveData()).status, + Status.LOADING + ) + + delayer.onComplete() + + Assert.assertEquals( + LiveDataTestUtil.getValue(mCodeVerificationViewModel.getBindLiveData()).status, + Status.FAILED + ) + + Assert.assertEquals( + LiveDataTestUtil.getValue(mCodeVerificationViewModel.getBindLiveData()) + .throwable!!::class.java, InvalidUdidOrPhone::class.java + ) + + Assert.assertEquals( + LiveDataTestUtil.getValue(mCodeVerificationViewModel.getBindLiveData()).data, + null + ) + } + + @Test + fun bindUser_invalidDeviceName() { + + val delayer = PublishSubject.create() + + val singleResponse = Single.just( + Response.error(716, mResponseBody) + ).delaySubscription(delayer) + + Mockito.`when`(mUserRepository.bind(mUser)).thenReturn(singleResponse) + + mCodeVerificationViewModel.bind(mUser) + + Mockito.verify(mUserRepository).bind(mUser) + + Assert.assertEquals( + LiveDataTestUtil.getValue(mCodeVerificationViewModel.getBindLiveData()).status, + Status.LOADING + ) + + delayer.onComplete() + + Assert.assertEquals( + LiveDataTestUtil.getValue(mCodeVerificationViewModel.getBindLiveData()).status, + Status.FAILED + ) + + Assert.assertEquals( + LiveDataTestUtil.getValue(mCodeVerificationViewModel.getBindLiveData()) + .throwable!!::class.java, InvalidDeviceName::class.java + ) + + Assert.assertEquals( + LiveDataTestUtil.getValue(mCodeVerificationViewModel.getBindLiveData()).data, + null + ) + } + + @Test + fun bindUser_activationCodeIsNotValid() { + + val delayer = PublishSubject.create() + + val singleResponse = Single.just( + Response.error(711, mResponseBody) + ).delaySubscription(delayer) + + Mockito.`when`(mUserRepository.bind(mUser)).thenReturn(singleResponse) + + mCodeVerificationViewModel.bind(mUser) + + Mockito.verify(mUserRepository).bind(mUser) + + Assert.assertEquals( + LiveDataTestUtil.getValue(mCodeVerificationViewModel.getBindLiveData()).status, + Status.LOADING + ) + + delayer.onComplete() + + Assert.assertEquals( + LiveDataTestUtil.getValue(mCodeVerificationViewModel.getBindLiveData()).status, + Status.FAILED + ) + + Assert.assertEquals( + LiveDataTestUtil.getValue(mCodeVerificationViewModel.getBindLiveData()) + .throwable!!::class.java, ActivationCodeIsNotValid::class.java + ) + + Assert.assertEquals( + LiveDataTestUtil.getValue(mCodeVerificationViewModel.getBindLiveData()).data, + null + ) + } + + @Test + fun setTimer() { + + val testScheduler = TestScheduler() + + RxJavaPlugins.setComputationSchedulerHandler { scheduler -> testScheduler } + + mCodeVerificationViewModel.setTimer(3) + + testScheduler.advanceTimeBy(1, TimeUnit.SECONDS) + + Assert.assertEquals( + LiveDataTestUtil.getValue(mCodeVerificationViewModel.getTimerLiveData()).data.toString(), + String.format("%02d:%02d ", 0, 3) + ) + Assert.assertEquals( + LiveDataTestUtil.getValue(mCodeVerificationViewModel.getTimerLiveData()).status, + Status.SUCCESS + ) + Assert.assertNull(LiveDataTestUtil.getValue(mCodeVerificationViewModel.getTimerLiveData()).throwable) + + testScheduler.advanceTimeBy(1, TimeUnit.SECONDS) + + Assert.assertEquals( + LiveDataTestUtil.getValue(mCodeVerificationViewModel.getTimerLiveData()).data.toString(), + String.format("%02d:%02d ", 0, 2) + ) + Assert.assertEquals( + LiveDataTestUtil.getValue(mCodeVerificationViewModel.getTimerLiveData()).status, + Status.SUCCESS + ) + Assert.assertNull(LiveDataTestUtil.getValue(mCodeVerificationViewModel.getTimerLiveData()).throwable) + + testScheduler.advanceTimeBy(1, TimeUnit.SECONDS) + + Assert.assertEquals( + LiveDataTestUtil.getValue(mCodeVerificationViewModel.getTimerLiveData()).data.toString(), + "RESEND" + ) + Assert.assertEquals( + LiveDataTestUtil.getValue(mCodeVerificationViewModel.getTimerLiveData()).status, + Status.SUCCESS + ) + Assert.assertNull(LiveDataTestUtil.getValue(mCodeVerificationViewModel.getTimerLiveData()).throwable) + + resetSchedulers() + } +} \ No newline at end of file diff --git a/app/src/test/java/de/netalic/peacock/ui/registration/PhoneInputViewModelTest.kt b/app/src/test/java/de/netalic/peacock/ui/registration/PhoneInputViewModelTest.kt new file mode 100644 index 0000000..0bb3087 --- /dev/null +++ b/app/src/test/java/de/netalic/peacock/ui/registration/PhoneInputViewModelTest.kt @@ -0,0 +1,223 @@ +package de.netalic.peacock.ui.registration + +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import de.netalic.peacock.base.BaseTest +import de.netalic.peacock.data.exception.BadRequestException +import de.netalic.peacock.data.exception.InvalidPhoneNumberException +import de.netalic.peacock.data.exception.InvalidUdidOrPhoneException +import de.netalic.peacock.data.exception.ServerException +import de.netalic.peacock.data.model.Status +import de.netalic.peacock.data.model.UserModel +import de.netalic.peacock.data.repository.UserRepository +import de.netalic.peacock.util.LiveDataTestUtil +import io.reactivex.Single +import io.reactivex.subjects.PublishSubject +import okhttp3.MediaType +import okhttp3.ResponseBody +import org.junit.* +import org.mockito.Mock +import org.mockito.Mockito +import org.mockito.MockitoAnnotations +import retrofit2.Response + +class PhoneInputViewModelTest : BaseTest() { + + + companion object { + val sUser = UserModel( + + mName = "salimi", + mPhone = "+989211499302", + mUdid = "D89707AC55BAED9E8F23B826FB2A28E96095A190", + mFirebaseToken = "eyJ0eXAiOiAiSldUIiwgImFsZyI6ICJSUzI1NiIsICJraWQiOiAiODk0NTkyNzQzMzlkMzNlZmNmNTE3MDc" + + "4NGM5ZGU1MjUzMjEyOWVmZiJ9.eyJpc3MiOiAiZmlyZWJhc2UtYWRtaW5zZGstaXp1MTNAYWxwaGEtZDY0ZTQuaWFtLmd" + + "zZXJ2aWNlYWNjb3VudC5jb20iLCAic3ViIjogImZpcmViYXNlLWFkbWluc2RrLWl6dTEzQGFscGhhLWQ2NGU0LmlhbS5nc" + + "2VydmljZWFjY291bnQuY29tIiwgImF1ZCI6ICJodHRwczovL2lkZW50aXR5dG9vbGtpdC5nb29nbGVhcGlzLmNvbS9nb" + + "29nbGUuaWRlbnRpdHkuaWRlbnRpdHl0b29sa2l0LnYxLklkZW50aXR5VG9vbGtpdCIsICJ1aWQiOiAiKzk4OTIxMTQ5OTM" + + "wMiIsICJpYXQiOiAxNTYzOTYwNDU3LCAiZXhwIjogMTU2Mzk2NDA1N30.HOUVBzwbmGwsglQHukGwrijlUuSZ241KdN2Eo" + + "l3Gy80mmd4Kxoc58m3VhL71AWv3WS99eE7uz6xctl--yLPilhN3WJ_z2nxySqkhxiZ9OtaH_U8sTek63SJgfINeTFzJFp" + + "WHkT_DlQNPTVoH_AqbXjh0gZwdpVdMyoLmmuJf-WIqx2y7BdwudCTiAqY_RoK7DdDwS8Jf28J-czpWi7Q4neUo1pC0WLi" + + "986u9n0mZcfIhWoVB_fV0A2-fWRV6yhT647sfHntC2eSg-OJZKO-MAyBsgKDIZm_ubX7m3LHD6rahpnUHtY8m33eJyD-" + + "EfZcKboRWalJkmje69abirvep1A", + mActivateToken = "082016", + mDeviceType = "android" + ) + val wrongUserPhone = UserModel( + "", + "salimi", + "a89211499302", + "D89707AC55BAED9E8F23B826FB2A28E96095A190", + "eyJ0eXAiOiAiSldUIiwgImFsZyI6ICJSUzI1NiIsICJraWQiOiAiODk0NTkyNzQzMzlkMzNlZmNmNTE3MDc" + + "4NGM5ZGU1MjUzMjEyOWVmZiJ9.eyJpc3MiOiAiZmlyZWJhc2UtYWRtaW5zZGstaXp1MTNAYWxwaGEtZDY0ZTQuaWFtLmd" + + "zZXJ2aWNlYWNjb3VudC5jb20iLCAic3ViIjogImZpcmViYXNlLWFkbWluc2RrLWl6dTEzQGFscGhhLWQ2NGU0LmlhbS5nc" + + "2VydmljZWFjY291bnQuY29tIiwgImF1ZCI6ICJodHRwczovL2lkZW50aXR5dG9vbGtpdC5nb29nbGVhcGlzLmNvbS9nb" + + "29nbGUuaWRlbnRpdHkuaWRlbnRpdHl0b29sa2l0LnYxLklkZW50aXR5VG9vbGtpdCIsICJ1aWQiOiAiKzk4OTIxMTQ5OTM" + + "wMiIsICJpYXQiOiAxNTYzOTYwNDU3LCAiZXhwIjogMTU2Mzk2NDA1N30.HOUVBzwbmGwsglQHukGwrijlUuSZ241KdN2Eo" + + "l3Gy80mmd4Kxoc58m3VhL71AWv3WS99eE7uz6xctl--yLPilhN3WJ_z2nxySqkhxiZ9OtaH_U8sTek63SJgfINeTFzJFp" + + "WHkT_DlQNPTVoH_AqbXjh0gZwdpVdMyoLmmuJf-WIqx2y7BdwudCTiAqY_RoK7DdDwS8Jf28J-czpWi7Q4neUo1pC0WLi" + + "986u9n0mZcfIhWoVB_fV0A2-fWRV6yhT647sfHntC2eSg-OJZKO-MAyBsgKDIZm_ubX7m3LHD6rahpnUHtY8m33eJyD-" + + "EfZcKboRWalJkmje69abirvep1A", + "082016", + "android" + ) + } + + @get:Rule + var instantExecutorRule = InstantTaskExecutorRule() + + @Mock + private lateinit var mUserRepository: UserRepository + + private lateinit var mRegistrationViewModel: RegistrationViewModel + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + mRegistrationViewModel = RegistrationViewModel(mUserRepository) + } + + @After + fun tearDown() { + Mockito.reset(mUserRepository) + } + + @Test + fun claim_showSuccess() { + val delayer = PublishSubject.create() + + val singleResponse = Single.just( + Response.success(200, sUser) + ).delaySubscription(delayer) + Mockito.`when`(mUserRepository.claim(sUser.mPhone, sUser.mUdid)).thenReturn(singleResponse) + mRegistrationViewModel.claim(sUser.mPhone, sUser.mUdid) + Mockito.verify(mUserRepository).claim(sUser.mPhone, sUser.mUdid) + Assert.assertEquals( + LiveDataTestUtil.getValue(mRegistrationViewModel.getClaimLiveData()).status, + Status.LOADING + ) + delayer.onComplete() + Assert.assertEquals(LiveDataTestUtil.getValue(mRegistrationViewModel.getClaimLiveData()).status, Status.SUCCESS) + Assert.assertEquals(LiveDataTestUtil.getValue(mRegistrationViewModel.getClaimLiveData()).data, sUser) + Assert.assertNull(LiveDataTestUtil.getValue(mRegistrationViewModel.getClaimLiveData()).throwable) + + } + + @Test + fun claimUser_showBadRequestException() { + val delayer = PublishSubject.create() + + val singleResponse = Single.just( + Response.error( + 400, + ResponseBody.create(MediaType.parse("text/plain"), "") + ) + ) + .delaySubscription(delayer) + Mockito.`when`(mUserRepository.claim(sUser.mPhone, sUser.mUdid)).thenReturn(singleResponse) + mRegistrationViewModel.claim(sUser.mPhone, sUser.mUdid) + Mockito.verify(mUserRepository).claim(sUser.mPhone, sUser.mUdid) + Assert.assertEquals( + LiveDataTestUtil.getValue(mRegistrationViewModel.getClaimLiveData()).status, + Status.LOADING + ) + delayer.onComplete() + Assert.assertEquals(LiveDataTestUtil.getValue(mRegistrationViewModel.getClaimLiveData()).status, Status.FAILED) + Assert.assertEquals( + LiveDataTestUtil.getValue(mRegistrationViewModel.getClaimLiveData()).throwable!!::class.java, + BadRequestException::class.java + ) + Assert.assertNull(LiveDataTestUtil.getValue(mRegistrationViewModel.getClaimLiveData()).data) + } + + @Test + fun claimUser_showServerException() { + val delayer = PublishSubject.create() + + val singleResponse = Single.just( + Response.error( + 500, + ResponseBody.create(MediaType.parse("text/plain"), "") + ) + ) + .delaySubscription(delayer) + Mockito.`when`(mUserRepository.claim(sUser.mPhone, sUser.mUdid)).thenReturn(singleResponse) + mRegistrationViewModel.claim(sUser.mPhone, sUser.mUdid) + Mockito.verify(mUserRepository).claim(sUser.mPhone, sUser.mUdid) + Assert.assertEquals( + LiveDataTestUtil.getValue(mRegistrationViewModel.getClaimLiveData()).status, + Status.LOADING + ) + delayer.onComplete() + Assert.assertEquals(LiveDataTestUtil.getValue(mRegistrationViewModel.getClaimLiveData()).status, Status.FAILED) + Assert.assertEquals( + LiveDataTestUtil.getValue(mRegistrationViewModel.getClaimLiveData()).throwable!!::class.java, + ServerException::class.java + ) + Assert.assertNull(LiveDataTestUtil.getValue(mRegistrationViewModel.getClaimLiveData()).data) + } + + + @Test + fun claimUser_showInvalidmUdidOrmPhoneException() { + val delayer = PublishSubject.create() + + val singleResponse = Single.just( + Response.error( + 710, + ResponseBody.create(MediaType.parse("text/plain"), "") + ) + ) + .delaySubscription(delayer) + Mockito.`when`(mUserRepository.claim(sUser.mPhone, sUser.mUdid)).thenReturn(singleResponse) + mRegistrationViewModel.claim(sUser.mPhone, sUser.mUdid) + Mockito.verify(mUserRepository).claim(sUser.mPhone, sUser.mUdid) + Assert.assertEquals( + LiveDataTestUtil.getValue(mRegistrationViewModel.getClaimLiveData()).status, + Status.LOADING + ) + delayer.onComplete() + Assert.assertEquals(LiveDataTestUtil.getValue(mRegistrationViewModel.getClaimLiveData()).status, Status.FAILED) + Assert.assertEquals( + LiveDataTestUtil.getValue(mRegistrationViewModel.getClaimLiveData()).throwable!!::class.java, + InvalidUdidOrPhoneException::class.java + ) + Assert.assertNull(LiveDataTestUtil.getValue(mRegistrationViewModel.getClaimLiveData()).data) + } + + @Test + fun claimUser_throwException() { + val delayer = PublishSubject.create() + + val singleResponse = Single.error>(Exception()) + .delaySubscription(delayer) + + Mockito.`when`(mUserRepository.claim(sUser.mPhone, sUser.mUdid)).thenReturn(singleResponse) + mRegistrationViewModel.claim(sUser.mPhone, sUser.mUdid) + Mockito.verify(mUserRepository).claim(sUser.mPhone, sUser.mUdid) + Assert.assertEquals( + LiveDataTestUtil.getValue(mRegistrationViewModel.getClaimLiveData()).status, + Status.LOADING + ) + delayer.onComplete() + + Assert.assertEquals(LiveDataTestUtil.getValue(mRegistrationViewModel.getClaimLiveData()).status, Status.FAILED) + Assert.assertEquals( + LiveDataTestUtil.getValue(mRegistrationViewModel.getClaimLiveData()).throwable!!::class.java, + Exception::class.java + ) + Assert.assertNull(LiveDataTestUtil.getValue(mRegistrationViewModel.getClaimLiveData()).data) + } + + @Test + fun phoneValidator_invalidPhoneNumber() { + + mRegistrationViewModel.claim(wrongUserPhone.mPhone, wrongUserPhone.mUdid) + Assert.assertEquals(LiveDataTestUtil.getValue(mRegistrationViewModel.getClaimLiveData()).status, Status.FAILED) + Assert.assertEquals( + LiveDataTestUtil.getValue(mRegistrationViewModel.getClaimLiveData()).throwable!!::class.java, + InvalidPhoneNumberException::class.java + ) + Assert.assertNull(LiveDataTestUtil.getValue(mRegistrationViewModel.getClaimLiveData()).data) + } + +} \ No newline at end of file diff --git a/app/src/test/java/de/netalic/peacock/util/LiveDataUtil.kt b/app/src/test/java/de/netalic/peacock/util/LiveDataUtil.kt new file mode 100644 index 0000000..e4b9ef2 --- /dev/null +++ b/app/src/test/java/de/netalic/peacock/util/LiveDataUtil.kt @@ -0,0 +1,29 @@ +package de.netalic.peacock.util + +import androidx.lifecycle.LiveData +import androidx.lifecycle.Observer +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit + +object LiveDataTestUtil { + + /** + * Get the value from a LiveData object. We're waiting for LiveData to emit, for 2 seconds. + * Once we got a notification via onChanged, we stop observing. + */ + @Throws(InterruptedException::class) + fun getValue(liveData: LiveData): T { + val data = arrayOfNulls(1) + val latch = CountDownLatch(1) + val observer = object : Observer { + override fun onChanged(o: T?) { + data[0] = o + latch.countDown() + liveData.removeObserver(this) + } + } + liveData.observeForever(observer) + latch.await(2, TimeUnit.SECONDS) + return data[0] as T + } +} diff --git a/build.gradle b/build.gradle index 438d280..27aa807 100644 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,3 @@ -// Top-level build file where you can add configuration options common to all sub-projects/modules. - buildscript { ext.kotlin_version = '1.3.41' repositories { @@ -10,8 +8,7 @@ buildscript { dependencies { classpath 'com.android.tools.build:gradle:3.4.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - // NOTE: Do not place your application dependencies here; they belong - // in the individual module build.gradle files + classpath 'androidx.navigation:navigation-safe-args-gradle-plugin:2.1.0-beta02' } } @@ -19,10 +16,11 @@ allprojects { repositories { google() jcenter() - + maven { url 'https://jitpack.io' } } } + task clean(type: Delete) { delete rootProject.buildDir -} +} \ No newline at end of file