diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..0a8992a7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,51 @@ +# Built application files +*.apk +*.ap_ + +# Files for the ART/Dalvik VM +*.dex + +# Java class files +*.class + +# Generated files +bin/ +gen/ +out/ + +# Gradle files +.gradle/ +build/ + +# Local configuration file (sdk path, etc) +local.properties + +# Proguard folder generated by Eclipse +proguard/ + +# Log Files +*.log + +# Android Studio Navigation editor temp files +.navigation/ + +# Android Studio captures folder +captures/ + +# Intellij +*.iml +.idea +.idea/workspace.xml +.idea/tasks.xml +.idea/gradle.xml +.idea/dictionaries +.idea/libraries + +# Keystore files +*.jks + +# External native build folder generated in Android Studio 2.2 and later +.externalNativeBuild + +# Google Services (e.g. APIs or Firebase) +google-services.json \ No newline at end of file diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 00000000..796b96d1 --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 00000000..226fe3e4 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,173 @@ +apply plugin: 'com.android.application' +apply from: '../config/quality/quality.gradle' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-android-extensions' +apply plugin: 'kotlin-kapt' + +android { + compileSdkVersion 27 + defaultConfig { + applicationId "org.mifos.mobile.cn" + minSdkVersion rootProject.ext.minSdkVersion + targetSdkVersion rootProject.ext.targetSdkVersion + versionCode 1 + versionName "1.0" + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + vectorDrawables.useSupportLibrary = true + } + buildTypes { + + debug { + versionNameSuffix " Debug" + debuggable true + } + + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + + sourceSets { + def commonTestDir = 'src/commonTest/kotlin' + main.java.srcDirs += 'src/main/kotlin' + androidTest { + java.srcDir commonTestDir + } + test { + java.srcDir commonTestDir + } + } + + // Always show the result of every unit test, even if it passes. + testOptions.unitTests.all { + testLogging { + events 'passed', 'skipped', 'failed', 'standardOut', 'standardError' + } + } + + lintOptions { + abortOnError false + warning 'InvalidPackage' + } + + packagingOptions { + exclude 'META-INF/rxjava.properties' + } + + repositories { + maven { url 'https://maven.fabric.io/public' } + maven { url 'https://maven.google.com' } + } + + androidExtensions { + experimental = true + } +} + +dependencies { + def daggerCompiler = "com.google.dagger:dagger-compiler:$rootProject.daggerVersion" + def jUnit = "junit:junit:4.12" + def mockito = "org.mockito:mockito-core:$rootProject.mockitoVersion" + + implementation fileTree(dir: 'libs', include: ['*.jar']) + + // Kotlin Dependencies + implementation "org.jetbrains.kotlin:kotlin-stdlib:$rootProject.kotlinVersion" + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$rootProject.kotlinVersion" + + // Support Dependencies + implementation "com.android.support:appcompat-v7:$rootProject.supportLibraryVersion" + implementation "com.android.support:design:$rootProject.supportLibraryVersion" + implementation "com.android.support:recyclerview-v7:$rootProject.supportLibraryVersion" + implementation "com.android.support:cardview-v7:$rootProject.supportLibraryVersion" + implementation "com.android.support.test.espresso:espresso-idling-resource:$rootProject.espressoVersion" + implementation "com.android.support:support-annotations:$rootProject.supportLibraryVersion" + implementation 'com.android.support.constraint:constraint-layout:1.1.0' + + // Rx Dependencies + implementation "io.reactivex.rxjava2:rxjava:$rootProject.rxjavaVersion" + implementation "io.reactivex.rxjava2:rxandroid:$rootProject.rxandroidVersion" + implementation "io.reactivex.rxjava2:rxkotlin:$rootProject.rxKotlinVersion" + + // Timber for logging + implementation 'com.jakewharton.timber:timber:4.5.1' + + // Dagger 2 Dependencies + implementation "com.google.dagger:dagger:$rootProject.daggerVersion" + compileOnly "org.glassfish:javax.annotation:10.0-b28" //Required by Dagger2 + kapt daggerCompiler + + // Square Dependencies + implementation "com.squareup.retrofit2:retrofit:$rootProject.retrofitVersion" + implementation "com.squareup.retrofit2:converter-gson:$rootProject.retrofitVersion" + implementation "com.squareup.retrofit2:converter-scalars:$rootProject.retrofitVersion" + implementation "com.squareup.retrofit2:adapter-rxjava2:$rootProject.retrofitVersion" + implementation "com.squareup.okhttp3:okhttp:$rootProject.okHttp3Version" + implementation "com.squareup.okhttp3:logging-interceptor:$rootProject.okHttp3Version" + + // For debugging the local database + implementation 'com.facebook.stetho:stetho:1.5.0' + + // Sweet Error for error handling + implementation 'com.github.therajanmaurya:Sweet-Error:1.0.0' + + // Glide for Image Loading + implementation 'com.github.bumptech.glide:glide:4.7.1' + kapt 'com.github.bumptech.glide:compiler:4.7.1' + + // Instrumentation test dependencies + androidTestImplementation jUnit + androidTestImplementation mockito + androidTestImplementation "org.mockito:mockito-android:$rootProject.mockitoVersion" + androidTestImplementation "com.android.support:support-annotations:$rootProject.supportLibraryVersion" + androidTestImplementation("com.android.support.test.espresso:espresso-contrib:$rootProject.espressoVersion") { + exclude group: 'com.android.support', module: 'appcompat' + exclude group: 'com.android.support', module: 'support-v4' + exclude group: 'com.android.support', module: 'recyclerview-v7' + exclude group: 'com.android.support', module: 'design' + exclude group: 'com.android.support', module: 'support-annotations' + } + androidTestImplementation "com.android.support.test.espresso:espresso-core:$rootProject.espressoVersion" + androidTestImplementation "com.android.support.test:runner:$rootProject.runnerVersion" + androidTestImplementation "com.android.support.test:rules:$rootProject.rulesVersion" + + // Unit tests dependencies + testImplementation jUnit + testImplementation mockito + testImplementation "com.android.support:support-annotations:$rootProject.supportLibraryVersion" + testImplementation "org.hamcrest:hamcrest-core:$rootProject.hamcrestVersion" + testImplementation "org.hamcrest:hamcrest-library:$rootProject.hamcrestVersion" + testImplementation "org.hamcrest:hamcrest-integration:$rootProject.hamcrestVersion" + testImplementation 'org.robolectric:robolectric:3.3.1' +} + + // Log out test results to console + tasks.matching { it instanceof Test }.all { + testLogging.events = ["failed", "passed", "skipped"] + } + + /* + Resolves dependency versions across test and production APKs, specifically, transitive + dependencies. This is required since Espresso internally has a dependency on support-annotations. + */ + configurations.all { + resolutionStrategy { + force 'com.android.support:support-annotations:27.1.1' + force 'com.google.code.findbugs:jsr305:1.3.9' + } + } + + + /* + All direct/transitive dependencies shared between your test and production APKs need to be + excluded from the test APK! This is necessary because both APKs will contain the same classes. Not + excluding these dependencies from your test configuration will result in an dex pre-verifier error + at runtime. More info in this tools bug: (https://code.google.com/p/android/issues/detail?id=192497) + */ + configurations.compile.dependencies.each { compileDependency -> + println "Excluding compile dependency: ${compileDependency.getName()}" + configurations.androidTestCompile.dependencies.each { androidTestCompileDependency -> + configurations.androidTestCompile.exclude module: "${compileDependency.getName()}" + } + } diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 00000000..f1b42451 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/app/src/androidTest/java/org/mifos/mobile/cn/ExampleInstrumentedTest.kt b/app/src/androidTest/java/org/mifos/mobile/cn/ExampleInstrumentedTest.kt new file mode 100644 index 00000000..43416331 --- /dev/null +++ b/app/src/androidTest/java/org/mifos/mobile/cn/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package org.mifos.mobile.cn + +import android.support.test.InstrumentationRegistry +import android.support.test.runner.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getTargetContext() + assertEquals("org.mifos.mobile.cn", appContext.packageName) + } +} diff --git a/app/src/commonTest/kotlin/org/mifos/mobile/cn/FakeJsonName.kt b/app/src/commonTest/kotlin/org/mifos/mobile/cn/FakeJsonName.kt new file mode 100644 index 00000000..31fb5183 --- /dev/null +++ b/app/src/commonTest/kotlin/org/mifos/mobile/cn/FakeJsonName.kt @@ -0,0 +1,10 @@ +package org.mifos.mobile.cn + +/** + * @author Rajan Maurya + * On 30/04/18. + */ +object FakeJsonName { + + var TEST_JSON = "test.json" +} diff --git a/app/src/commonTest/kotlin/org/mifos/mobile/cn/FakeRemoteDataSource.kt b/app/src/commonTest/kotlin/org/mifos/mobile/cn/FakeRemoteDataSource.kt new file mode 100644 index 00000000..650b4b17 --- /dev/null +++ b/app/src/commonTest/kotlin/org/mifos/mobile/cn/FakeRemoteDataSource.kt @@ -0,0 +1,20 @@ +package xyz.idtlabs.icommit.fieldui + +import org.mifos.mobile.cn.TestDataFactory + +/** + * FakeRemoteDataSource is reading the local json files into the java object using gson. + * Created by Rajan Maurya on 25/6/17. + */ +class FakeRemoteDataSource { + + companion object { + + private val testDataFactory = TestDataFactory() + + /*fun getTestJson(): TestJson { + return testDataFactory.convertJsonToDataObject(object : TypeToken() { + }, FakeJsonName.TEST_JSON) + }*/ + } +} diff --git a/app/src/commonTest/kotlin/org/mifos/mobile/cn/TestDataFactory.kt b/app/src/commonTest/kotlin/org/mifos/mobile/cn/TestDataFactory.kt new file mode 100644 index 00000000..61cf0411 --- /dev/null +++ b/app/src/commonTest/kotlin/org/mifos/mobile/cn/TestDataFactory.kt @@ -0,0 +1,40 @@ +package org.mifos.mobile.cn + +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken +import com.google.gson.stream.JsonReader +import java.io.InputStreamReader + +class TestDataFactory { + + /** + * Note : This Generic Method DeSerialize Both Object and List Type Json in POJO + * + * + * Note : Do Not use Array [] in POJO classes for of any element initialization, + * Use Instead ArrayList. + * + * @param listModel Class of the List Model + * @param jsonName Name of the Json in resources + * @param return type + * @return Return the List of the listModel by Deserializing the Json of resources + * @Example of Deserializing List Type Json + * + * + * TestDataFactory mTestDataFactory = new TestDataFactory(); + * + * + * List listObject = mTestDataFactory.convertJsonToDataObject( + * new TypeToken>(){}, "ListObject.json") + * @Example Of Deserializing Object Type Json + * + * + * Object object = mTestDataFactory.convertJsonToDataObject( + * new TypeToken(){}, "Object.json") + */ + fun convertJsonToDataObject(listModel: TypeToken, jsonName: String): T { + val inputStream = javaClass.classLoader.getResourceAsStream(jsonName) + val jsonReader = JsonReader(InputStreamReader(inputStream)) + return Gson().fromJson(jsonReader, listModel.type) + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..b69f828e --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/kotlin/org/mifos/mobile/cn/MifosApplication.kt b/app/src/main/kotlin/org/mifos/mobile/cn/MifosApplication.kt new file mode 100644 index 00000000..8ed0042c --- /dev/null +++ b/app/src/main/kotlin/org/mifos/mobile/cn/MifosApplication.kt @@ -0,0 +1,58 @@ +package org.mifos.mobile.cn + +import android.app.Application +import android.content.Context +import android.support.v7.app.AppCompatDelegate +import com.facebook.stetho.Stetho +import org.mifos.mobile.cn.injection.component.ApplicationComponent +import org.mifos.mobile.cn.injection.component.DaggerApplicationComponent +import org.mifos.mobile.cn.injection.module.ApplicationModule + +/** + * @author Rajan Maurya + * On 22/01/18. + */ +class MifosApplication : Application() { + + lateinit var applicationComponent: ApplicationComponent + + companion object { + + lateinit var fineractApplication: MifosApplication + + fun get(context: Context): MifosApplication { + return context.applicationContext as MifosApplication + } + + fun getContext(context: Context): Context { + return context.applicationContext + } + + fun getContext(): Context { + return fineractApplication + } + } + + override fun onCreate() { + super.onCreate() + fineractApplication = this + AppCompatDelegate.setCompatVectorFromResourcesEnabled(true) + + // Initializing the Dagger component + initializeComponent() + applicationComponent.inject(this) + + // Initialize the Stetho + Stetho.initializeWithDefaults(this) + } + + fun getComponent(): ApplicationComponent { + return applicationComponent + } + + private fun initializeComponent() { + applicationComponent = DaggerApplicationComponent.builder() + .applicationModule(ApplicationModule(this)) + .build() + } +} diff --git a/app/src/main/kotlin/org/mifos/mobile/cn/data/datamanager/DataManagerAuth.kt b/app/src/main/kotlin/org/mifos/mobile/cn/data/datamanager/DataManagerAuth.kt new file mode 100644 index 00000000..9c910637 --- /dev/null +++ b/app/src/main/kotlin/org/mifos/mobile/cn/data/datamanager/DataManagerAuth.kt @@ -0,0 +1,44 @@ +package org.mifos.mobile.cn.data.datamanager + +import org.mifos.mobile.cn.data.local.PreferencesHelper +import org.mifos.mobile.cn.data.remote.BaseApiManager +import javax.inject.Inject +import javax.inject.Singleton + +/** + * @author Rajan Maurya + * On 22/01/18. + */ +@Singleton +class DataManagerAuth @Inject constructor(private val baseApiManager: BaseApiManager, + private val preferencesHelper: PreferencesHelper) { + + + /** + * Login with iCommit Account + * Logging the Username and password with Basic Authentication + * @loginRequest LoginRequest + *//* + fun login(loginRequest: LoginRequest): Observable { + return baseApiManager.getAuthApi().login(loginRequest) + .retry(2) + .concatMap({ loginResponse -> + preferencesHelper.putAccessToken(loginResponse.authToken) + preferencesHelper.putLoginStatus(true) + return@concatMap Observable.just(loginResponse) + }) + } + + + *//** + * Fake Login with iCommit Account + *//* + fun fakeLogin(loginRequest: LoginRequest): Observable { + return Observable.just(FakeRemoteDataSource.getLoginResponse()) + .concatMap({ loginResponse -> + preferencesHelper.putAccessToken(loginResponse.authToken) + preferencesHelper.putLoginStatus(true) + return@concatMap Observable.just(loginResponse) + }) + }*/ +} \ No newline at end of file diff --git a/app/src/main/kotlin/org/mifos/mobile/cn/data/local/PreferenceKey.kt b/app/src/main/kotlin/org/mifos/mobile/cn/data/local/PreferenceKey.kt new file mode 100644 index 00000000..fccb3a93 --- /dev/null +++ b/app/src/main/kotlin/org/mifos/mobile/cn/data/local/PreferenceKey.kt @@ -0,0 +1,10 @@ +package org.mifos.mobile.cn.data.local + +object PreferenceKey { + + const val PREF_ICOMMIT = "preferences_icommit" + + const val PREF_KEY_ACCESS_TOKEN = "PREF_KEY_ACCESS_TOKEN" + + const val PREF_KEY_LOGIN_STATUS = "PREF_KEY_LOGIN_STATUS" +} diff --git a/app/src/main/kotlin/org/mifos/mobile/cn/data/local/PreferencesHelper.kt b/app/src/main/kotlin/org/mifos/mobile/cn/data/local/PreferencesHelper.kt new file mode 100644 index 00000000..10b95a57 --- /dev/null +++ b/app/src/main/kotlin/org/mifos/mobile/cn/data/local/PreferencesHelper.kt @@ -0,0 +1,88 @@ +package org.mifos.mobile.cn.data.local + +import android.content.Context +import android.content.SharedPreferences +import com.google.gson.Gson +import com.google.gson.GsonBuilder +import org.mifos.mobile.cn.injection.ApplicationContext +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class PreferencesHelper +@Inject +constructor(@ApplicationContext context: Context) { + + private val preferences: SharedPreferences = context.getSharedPreferences( + PreferenceKey.PREF_ICOMMIT, Context.MODE_PRIVATE) + private val gson: Gson = GsonBuilder() + .setDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSz") + .create() + + fun getInt(preferenceKey: String, preferenceDefaultValue: Int): Int { + return preferences.getInt(preferenceKey, preferenceDefaultValue) + } + + fun putInt(preferenceKey: String, preferenceValue: Int) { + preferences.edit().putInt(preferenceKey, preferenceValue).apply() + } + + fun getLong(preferenceKey: String, preferenceDefaultValue: Long): Long { + return preferences.getLong(preferenceKey, preferenceDefaultValue) + } + + fun putLong(preferenceKey: String, preferenceValue: Long) { + preferences.edit().putLong(preferenceKey, preferenceValue).apply() + } + + fun getFloat(preferenceKey: String, preferenceDefaultValue: Float): Float { + return preferences.getFloat(preferenceKey, preferenceDefaultValue) + } + + fun putFloat(preferenceKey: String, preferenceValue: Float) { + preferences.edit().putFloat(preferenceKey, preferenceValue).apply() + } + + fun getBoolean(preferenceKey: String, preferenceDefaultValue: Boolean): Boolean { + return preferences.getBoolean(preferenceKey, preferenceDefaultValue) + } + + fun putBoolean(preferenceKey: String, preferenceValue: Boolean) { + preferences.edit().putBoolean(preferenceKey, preferenceValue).apply() + } + + fun getString(preferenceKey: String, preferenceDefaultValue: String): String? { + return preferences.getString(preferenceKey, preferenceDefaultValue) + } + + fun putString(preferenceKey: String, preferenceValue: String) { + preferences.edit().putString(preferenceKey, preferenceValue).apply() + } + + fun putStringSet(preferenceKey: String, stringSet: Set) { + preferences.edit().putStringSet(preferenceKey, stringSet).apply() + } + + fun getStringSet(preferenceKey: String): Set? { + return preferences.getStringSet(preferenceKey, null) + } + + fun clear() { + preferences.edit().clear().apply() + } + + fun putAccessToken(accessToken: String) { + preferences.edit().putString( + PreferenceKey.PREF_KEY_ACCESS_TOKEN, "Basic $accessToken").apply() + } + + val accessToken: String + get() = preferences.getString(PreferenceKey.PREF_KEY_ACCESS_TOKEN, null) + + fun putLoginStatus(loginStatus: Boolean) { + preferences.edit().putBoolean(PreferenceKey.PREF_KEY_LOGIN_STATUS, loginStatus).apply() + } + + val loginStatus: Boolean + get() = preferences.getBoolean(PreferenceKey.PREF_KEY_LOGIN_STATUS, false) +} diff --git a/app/src/main/kotlin/org/mifos/mobile/cn/data/models/TestModel.kt b/app/src/main/kotlin/org/mifos/mobile/cn/data/models/TestModel.kt new file mode 100644 index 00000000..ec60b0bd --- /dev/null +++ b/app/src/main/kotlin/org/mifos/mobile/cn/data/models/TestModel.kt @@ -0,0 +1,7 @@ +package org.mifos.mobile.cn.data.models + +/** + * @author Rajan Maurya + * On 29/05/18. + */ +data class TestModel (val data: String) \ No newline at end of file diff --git a/app/src/main/kotlin/org/mifos/mobile/cn/data/remote/BaseApiManager.kt b/app/src/main/kotlin/org/mifos/mobile/cn/data/remote/BaseApiManager.kt new file mode 100644 index 00000000..89a98572 --- /dev/null +++ b/app/src/main/kotlin/org/mifos/mobile/cn/data/remote/BaseApiManager.kt @@ -0,0 +1,75 @@ +package org.mifos.mobile.cn.data.remote + +import android.content.Context +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor +import org.mifos.mobile.cn.data.services.AnonymousService +import retrofit2.Retrofit +import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory +import retrofit2.converter.gson.GsonConverterFactory +import org.mifos.mobile.cn.data.services.AuthService + +class BaseApiManager constructor(context: Context) { + + private lateinit var retrofit: Retrofit + private lateinit var anonymousRetrofit: Retrofit + private lateinit var authApi: AuthService + private lateinit var anonymousService: AnonymousService + + init { + createService(context) + createAnonymousService() + } + + private fun init() { + authApi = createApi(AuthService::class.java) + } + + private fun initAnonymous() { + anonymousService = anonymousRetrofit.create(AnonymousService::class.java) + } + + private fun createApi(clazz: Class): T { + return retrofit.create(clazz) + } + + private fun createService(context: Context) { + + val interceptor = HttpLoggingInterceptor() + interceptor.level = HttpLoggingInterceptor.Level.BODY + + retrofit = Retrofit.Builder() + .baseUrl(BaseUrl.defaultBaseUrl) + .addConverterFactory(GsonConverterFactory.create()) + .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) + .client(MifosOkHttpClient(context).telescopeOkHttpClient) + .build() + init() + } + + private fun createAnonymousService() { + + val interceptor = HttpLoggingInterceptor() + interceptor.level = HttpLoggingInterceptor.Level.BODY + + val okHttpClient = OkHttpClient.Builder() + .addInterceptor(interceptor) + .build() + + anonymousRetrofit = Retrofit.Builder() + .baseUrl(BaseUrl.defaultBaseUrl) + .addConverterFactory(GsonConverterFactory.create()) + .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) + .client(okHttpClient) + .build() + initAnonymous() + } + + fun getAuthApi(): AuthService { + return authApi + } + + fun getAnonymousService(): AnonymousService { + return anonymousService + } +} diff --git a/app/src/main/kotlin/org/mifos/mobile/cn/data/remote/BaseUrl.kt b/app/src/main/kotlin/org/mifos/mobile/cn/data/remote/BaseUrl.kt new file mode 100644 index 00000000..c4e3ba89 --- /dev/null +++ b/app/src/main/kotlin/org/mifos/mobile/cn/data/remote/BaseUrl.kt @@ -0,0 +1,26 @@ +package org.mifos.mobile.cn.data.remote + +import org.mifos.mobile.cn.BuildConfig + +/** + * @author Rajan Maurya + * On 22/01/18. + */ +object BaseUrl { + + private const val PROTOCOL_HTTPS = "http://" + private const val API_TEST_ENDPOINT = "example.com" + private const val API_PRODUCTION_ENDPOINT = "example.com" + private const val PORT = "8000" + // "/" in the last of the base url always + + val defaultBaseUrl: String + get() = "$PROTOCOL_HTTPS$apiEndpoint:$PORT" + + val apiEndpoint: String + get() = if (BuildConfig.DEBUG) { + API_TEST_ENDPOINT + } else { + API_PRODUCTION_ENDPOINT + } +} diff --git a/app/src/main/kotlin/org/mifos/mobile/cn/data/remote/EndPoints.kt b/app/src/main/kotlin/org/mifos/mobile/cn/data/remote/EndPoints.kt new file mode 100644 index 00000000..9c103036 --- /dev/null +++ b/app/src/main/kotlin/org/mifos/mobile/cn/data/remote/EndPoints.kt @@ -0,0 +1,10 @@ +package org.mifos.mobile.cn.data.remote + +object EndPoints { + + /* + * API End Paths + * <- This section manage the different type of end points + */ + const val API_AUTH_PATH = "/customer" +} diff --git a/app/src/main/kotlin/org/mifos/mobile/cn/data/remote/MifosInterceptor.kt b/app/src/main/kotlin/org/mifos/mobile/cn/data/remote/MifosInterceptor.kt new file mode 100644 index 00000000..4d1baca0 --- /dev/null +++ b/app/src/main/kotlin/org/mifos/mobile/cn/data/remote/MifosInterceptor.kt @@ -0,0 +1,41 @@ +package org.mifos.mobile.cn.data.remote + +import android.content.Context +import android.support.annotation.NonNull +import android.text.TextUtils +import okhttp3.Interceptor +import okhttp3.Response +import org.mifos.mobile.cn.MifosApplication +import org.mifos.mobile.cn.data.local.PreferencesHelper +import org.mifos.mobile.cn.injection.ApplicationContext +import java.io.IOException +import javax.inject.Inject + +class MifosInterceptor @Inject constructor(@ApplicationContext context: Context) : Interceptor { + + @Inject + lateinit var preferencesHelper: PreferencesHelper + + init { + MifosApplication.get(context).getComponent().inject(this) + } + + companion object { + val HEADER_ACCESS_TOKEN = "access_token" + } + + @Throws(IOException::class) + override fun intercept(@NonNull chain: Interceptor.Chain): Response { + val chainRequest = chain.request() + val builder = chainRequest.newBuilder() + + val accessToken = preferencesHelper.accessToken + + if (!TextUtils.isEmpty(accessToken)) { + builder.header(HEADER_ACCESS_TOKEN, preferencesHelper.accessToken) + } + + val request = builder.build() + return chain.proceed(request) + } +} diff --git a/app/src/main/kotlin/org/mifos/mobile/cn/data/remote/MifosOkHttpClient.kt b/app/src/main/kotlin/org/mifos/mobile/cn/data/remote/MifosOkHttpClient.kt new file mode 100644 index 00000000..794f2418 --- /dev/null +++ b/app/src/main/kotlin/org/mifos/mobile/cn/data/remote/MifosOkHttpClient.kt @@ -0,0 +1,85 @@ +package org.mifos.mobile.cn.data.remote + +import android.annotation.SuppressLint +import android.content.Context +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor +import java.security.KeyStore +import java.security.cert.CertificateException +import java.security.cert.X509Certificate +import java.util.* +import java.util.concurrent.TimeUnit +import javax.net.ssl.SSLContext +import javax.net.ssl.TrustManager +import javax.net.ssl.TrustManagerFactory +import javax.net.ssl.X509TrustManager + +class MifosOkHttpClient constructor(private val context: Context) { + + val telescopeOkHttpClient: OkHttpClient + get() { + + val okHttpBuilder: OkHttpClient.Builder = OkHttpClient.Builder() + + try { + // Create a trust manager that does not validate certificate chains + val trustAllCerts = arrayOf(object : X509TrustManager { + @SuppressLint("TrustAllX509TrustManager") + @Throws(CertificateException::class) + override fun checkClientTrusted( + chain: Array, + authType: String) { + } + + @SuppressLint("TrustAllX509TrustManager") + @Throws(CertificateException::class) + override fun checkServerTrusted( + chain: Array, + authType: String) { + } + + override fun getAcceptedIssuers(): Array { + return arrayOfNulls(0) + } + }) + + // Install the all-trusting trust manager + val sslContext = SSLContext.getInstance("SSL") + sslContext.init(null, trustAllCerts, java.security.SecureRandom()) + + // Create an ssl socket factory with our all-trusting manager + val sslSocketFactory = sslContext.socketFactory + + val trustManagerFactory = TrustManagerFactory + .getInstance(TrustManagerFactory.getDefaultAlgorithm()) + trustManagerFactory.init(null as KeyStore?) + val trustManagers = trustManagerFactory.trustManagers + if (trustManagers.size != 1 || trustManagers[0] !is X509TrustManager) { + throw IllegalStateException("Unexpected default trust managers:" + + Arrays.toString(trustManagers)) + } + val trustManager = trustManagers[0] as X509TrustManager + + //Set SSL certificate to OkHttpClient Builder + okHttpBuilder.sslSocketFactory(sslSocketFactory, trustManager) + + okHttpBuilder.hostnameVerifier { _, _ -> true } + } catch (e: Exception) { + throw RuntimeException(e) + } + + //Enable Full Body Logging + val logger = HttpLoggingInterceptor() + logger.level = HttpLoggingInterceptor.Level.BODY + + //Setting Timeout 120 Seconds + okHttpBuilder.connectTimeout(120, TimeUnit.SECONDS) + okHttpBuilder.readTimeout(120, TimeUnit.SECONDS) + + //Interceptor :> Full Body Logger and ApiRequest Header + okHttpBuilder.addInterceptor(logger) + okHttpBuilder.addInterceptor(MifosInterceptor(context)) + + return okHttpBuilder.build() + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/org/mifos/mobile/cn/data/services/AnonymousService.kt b/app/src/main/kotlin/org/mifos/mobile/cn/data/services/AnonymousService.kt new file mode 100644 index 00000000..4da602ee --- /dev/null +++ b/app/src/main/kotlin/org/mifos/mobile/cn/data/services/AnonymousService.kt @@ -0,0 +1,10 @@ +package org.mifos.mobile.cn.data.services + + +/** + * @author Rajan Maurya + * On 22/01/18. + */ +interface AnonymousService { + +} \ No newline at end of file diff --git a/app/src/main/kotlin/org/mifos/mobile/cn/data/services/AuthService.kt b/app/src/main/kotlin/org/mifos/mobile/cn/data/services/AuthService.kt new file mode 100644 index 00000000..87156087 --- /dev/null +++ b/app/src/main/kotlin/org/mifos/mobile/cn/data/services/AuthService.kt @@ -0,0 +1,11 @@ +package org.mifos.mobile.cn.data.services + +/** + * @author Rajan Maurya + * On 22/01/18. + */ +interface AuthService { + + /*@GET("/authentication") + fun login(loginRequest: LoginRequest): Observable*/ +} \ No newline at end of file diff --git a/app/src/main/kotlin/org/mifos/mobile/cn/exceptions/ExceptionStatusCode.kt b/app/src/main/kotlin/org/mifos/mobile/cn/exceptions/ExceptionStatusCode.kt new file mode 100644 index 00000000..260b340c --- /dev/null +++ b/app/src/main/kotlin/org/mifos/mobile/cn/exceptions/ExceptionStatusCode.kt @@ -0,0 +1,18 @@ +package xyz.idtlabs.icommit.fieldui.exceptions + +import retrofit2.HttpException + +/** + * @author Rajan Maurya + * On 07/05/18. + */ +object ExceptionStatusCode { + + fun isHttp401Error(throwable: Throwable): Boolean { + return (throwable as HttpException).code() == 401 + } + + fun isHttp500Error(throwable: Throwable): Boolean { + return (throwable as HttpException).code() == 500 + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/org/mifos/mobile/cn/fakesource/FakeJsonName.kt b/app/src/main/kotlin/org/mifos/mobile/cn/fakesource/FakeJsonName.kt new file mode 100644 index 00000000..31fb5183 --- /dev/null +++ b/app/src/main/kotlin/org/mifos/mobile/cn/fakesource/FakeJsonName.kt @@ -0,0 +1,10 @@ +package org.mifos.mobile.cn + +/** + * @author Rajan Maurya + * On 30/04/18. + */ +object FakeJsonName { + + var TEST_JSON = "test.json" +} diff --git a/app/src/main/kotlin/org/mifos/mobile/cn/fakesource/FakeRemoteDataSource.kt b/app/src/main/kotlin/org/mifos/mobile/cn/fakesource/FakeRemoteDataSource.kt new file mode 100644 index 00000000..650b4b17 --- /dev/null +++ b/app/src/main/kotlin/org/mifos/mobile/cn/fakesource/FakeRemoteDataSource.kt @@ -0,0 +1,20 @@ +package xyz.idtlabs.icommit.fieldui + +import org.mifos.mobile.cn.TestDataFactory + +/** + * FakeRemoteDataSource is reading the local json files into the java object using gson. + * Created by Rajan Maurya on 25/6/17. + */ +class FakeRemoteDataSource { + + companion object { + + private val testDataFactory = TestDataFactory() + + /*fun getTestJson(): TestJson { + return testDataFactory.convertJsonToDataObject(object : TypeToken() { + }, FakeJsonName.TEST_JSON) + }*/ + } +} diff --git a/app/src/main/kotlin/org/mifos/mobile/cn/fakesource/TestDataFactory.kt b/app/src/main/kotlin/org/mifos/mobile/cn/fakesource/TestDataFactory.kt new file mode 100644 index 00000000..61cf0411 --- /dev/null +++ b/app/src/main/kotlin/org/mifos/mobile/cn/fakesource/TestDataFactory.kt @@ -0,0 +1,40 @@ +package org.mifos.mobile.cn + +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken +import com.google.gson.stream.JsonReader +import java.io.InputStreamReader + +class TestDataFactory { + + /** + * Note : This Generic Method DeSerialize Both Object and List Type Json in POJO + * + * + * Note : Do Not use Array [] in POJO classes for of any element initialization, + * Use Instead ArrayList. + * + * @param listModel Class of the List Model + * @param jsonName Name of the Json in resources + * @param return type + * @return Return the List of the listModel by Deserializing the Json of resources + * @Example of Deserializing List Type Json + * + * + * TestDataFactory mTestDataFactory = new TestDataFactory(); + * + * + * List listObject = mTestDataFactory.convertJsonToDataObject( + * new TypeToken>(){}, "ListObject.json") + * @Example Of Deserializing Object Type Json + * + * + * Object object = mTestDataFactory.convertJsonToDataObject( + * new TypeToken(){}, "Object.json") + */ + fun convertJsonToDataObject(listModel: TypeToken, jsonName: String): T { + val inputStream = javaClass.classLoader.getResourceAsStream(jsonName) + val jsonReader = JsonReader(InputStreamReader(inputStream)) + return Gson().fromJson(jsonReader, listModel.type) + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/org/mifos/mobile/cn/injection/ActivityContext.kt b/app/src/main/kotlin/org/mifos/mobile/cn/injection/ActivityContext.kt new file mode 100644 index 00000000..c459f0cb --- /dev/null +++ b/app/src/main/kotlin/org/mifos/mobile/cn/injection/ActivityContext.kt @@ -0,0 +1,7 @@ +package org.mifos.mobile.cn.injection + +import javax.inject.Qualifier + +@Qualifier +@Retention(AnnotationRetention.RUNTIME) +annotation class ActivityContext diff --git a/app/src/main/kotlin/org/mifos/mobile/cn/injection/ApplicationContext.kt b/app/src/main/kotlin/org/mifos/mobile/cn/injection/ApplicationContext.kt new file mode 100644 index 00000000..4cd21cee --- /dev/null +++ b/app/src/main/kotlin/org/mifos/mobile/cn/injection/ApplicationContext.kt @@ -0,0 +1,7 @@ +package org.mifos.mobile.cn.injection + +import javax.inject.Qualifier + +@Qualifier +@Retention(AnnotationRetention.RUNTIME) +annotation class ApplicationContext diff --git a/app/src/main/kotlin/org/mifos/mobile/cn/injection/ConfigPersistent.kt b/app/src/main/kotlin/org/mifos/mobile/cn/injection/ConfigPersistent.kt new file mode 100644 index 00000000..8752b647 --- /dev/null +++ b/app/src/main/kotlin/org/mifos/mobile/cn/injection/ConfigPersistent.kt @@ -0,0 +1,12 @@ +package org.mifos.mobile.cn.injection + +import javax.inject.Scope + +/** + * A scoping annotation to permit dependencies conform to the life of the + * [@ConfigPersistentComponent] + */ +@MustBeDocumented +@Scope +@Retention(AnnotationRetention.RUNTIME) +annotation class ConfigPersistent diff --git a/app/src/main/kotlin/org/mifos/mobile/cn/injection/PerActivity.kt b/app/src/main/kotlin/org/mifos/mobile/cn/injection/PerActivity.kt new file mode 100755 index 00000000..c41cd9a3 --- /dev/null +++ b/app/src/main/kotlin/org/mifos/mobile/cn/injection/PerActivity.kt @@ -0,0 +1,13 @@ +package org.mifos.mobile.cn.injection + +import javax.inject.Scope + +/** + * A scoping annotation to permit objects whose lifetime should + * conform to the life of the Activity to be memorised in the + * correct component. + */ +@MustBeDocumented +@Scope +@Retention(AnnotationRetention.RUNTIME) +annotation class PerActivity diff --git a/app/src/main/kotlin/org/mifos/mobile/cn/injection/component/ActivityComponent.kt b/app/src/main/kotlin/org/mifos/mobile/cn/injection/component/ActivityComponent.kt new file mode 100644 index 00000000..cbf8aa86 --- /dev/null +++ b/app/src/main/kotlin/org/mifos/mobile/cn/injection/component/ActivityComponent.kt @@ -0,0 +1,14 @@ +package org.mifos.mobile.cn.injection.component + +import dagger.Subcomponent +import org.mifos.mobile.cn.injection.PerActivity +import org.mifos.mobile.cn.injection.module.ActivityModule + +/** + * This component inject dependencies to all Activities across the application + */ +@PerActivity +@Subcomponent(modules = arrayOf(ActivityModule::class)) +interface ActivityComponent { + +} diff --git a/app/src/main/kotlin/org/mifos/mobile/cn/injection/component/ApplicationComponent.kt b/app/src/main/kotlin/org/mifos/mobile/cn/injection/component/ApplicationComponent.kt new file mode 100644 index 00000000..79f24696 --- /dev/null +++ b/app/src/main/kotlin/org/mifos/mobile/cn/injection/component/ApplicationComponent.kt @@ -0,0 +1,28 @@ +package org.mifos.mobile.cn.injection.component + +import android.app.Application +import android.content.Context +import dagger.Component +import org.mifos.mobile.cn.MifosApplication +import org.mifos.mobile.cn.data.local.PreferencesHelper +import org.mifos.mobile.cn.data.remote.MifosInterceptor +import org.mifos.mobile.cn.injection.ApplicationContext +import org.mifos.mobile.cn.injection.module.ApplicationModule +import org.mifos.mobile.cn.data.datamanager.DataManagerAuth +import javax.inject.Singleton + +@Singleton +@Component(modules = arrayOf(ApplicationModule::class)) +interface ApplicationComponent { + + @ApplicationContext + fun context(): Context + + fun application(): Application + fun preferencesHelper(): PreferencesHelper + + fun dataManagerAuth(): DataManagerAuth + + fun inject(fineractInterceptor: MifosInterceptor) + fun inject(fineractApplication: MifosApplication) +} diff --git a/app/src/main/kotlin/org/mifos/mobile/cn/injection/component/ConfigPersistentComponent.kt b/app/src/main/kotlin/org/mifos/mobile/cn/injection/component/ConfigPersistentComponent.kt new file mode 100644 index 00000000..a30f31ee --- /dev/null +++ b/app/src/main/kotlin/org/mifos/mobile/cn/injection/component/ConfigPersistentComponent.kt @@ -0,0 +1,18 @@ +package org.mifos.mobile.cn.injection.component + +import dagger.Component +import org.mifos.mobile.cn.injection.ConfigPersistent +import org.mifos.mobile.cn.injection.module.ActivityModule + +/** + * A dagger component that will live during the lifecycle of an Activity but it won't + * be destroy during configuration changes. Check [@MifosBaseActivity] to see how this components + * survives configuration changes. + * Use the [ConfigPersistent] scope to annotate dependencies that need to survive + * configuration changes (for example Presenters). + */ +@ConfigPersistent +@Component(dependencies = arrayOf(ApplicationComponent::class)) +interface ConfigPersistentComponent { + fun activityComponent(activityModule: ActivityModule): ActivityComponent +} diff --git a/app/src/main/kotlin/org/mifos/mobile/cn/injection/module/ActivityModule.kt b/app/src/main/kotlin/org/mifos/mobile/cn/injection/module/ActivityModule.kt new file mode 100644 index 00000000..d19dff2c --- /dev/null +++ b/app/src/main/kotlin/org/mifos/mobile/cn/injection/module/ActivityModule.kt @@ -0,0 +1,26 @@ +package org.mifos.mobile.cn.injection.module + +import android.app.Activity +import android.content.Context +import dagger.Module +import dagger.Provides +import org.mifos.mobile.cn.injection.ActivityContext +import org.mifos.mobile.cn.injection.PerActivity + + +@Module +class ActivityModule(private val activity: Activity) { + + @Provides + @PerActivity + internal fun provideActivity(): Activity { + return activity + } + + @Provides + @PerActivity + @ActivityContext + internal fun providesContext(): Context { + return activity + } +} diff --git a/app/src/main/kotlin/org/mifos/mobile/cn/injection/module/ApplicationModule.kt b/app/src/main/kotlin/org/mifos/mobile/cn/injection/module/ApplicationModule.kt new file mode 100644 index 00000000..851e2715 --- /dev/null +++ b/app/src/main/kotlin/org/mifos/mobile/cn/injection/module/ApplicationModule.kt @@ -0,0 +1,42 @@ +package org.mifos.mobile.cn.injection.module + +import android.app.Application +import android.content.Context +import dagger.Module +import dagger.Provides +import org.mifos.mobile.cn.data.local.PreferencesHelper +import org.mifos.mobile.cn.injection.ApplicationContext +import org.mifos.mobile.cn.data.remote.BaseApiManager +import javax.inject.Singleton + +/** + * Provide application-level dependencies. + */ +@Module +class ApplicationModule(val application: Application) { + + @Provides + @Singleton + internal fun provideApplication(): Application { + return application + } + + @Provides + @Singleton + @ApplicationContext + internal fun provideContext(): Context { + return application + } + + @Provides + @Singleton + internal fun providePreferencesHelper(@ApplicationContext context: Context): PreferencesHelper { + return PreferencesHelper(context) + } + + @Provides + @Singleton + internal fun provideBaseApiManager(): BaseApiManager { + return BaseApiManager(application) + } +} diff --git a/app/src/main/kotlin/org/mifos/mobile/cn/ui/base/BaseActivityCallback.kt b/app/src/main/kotlin/org/mifos/mobile/cn/ui/base/BaseActivityCallback.kt new file mode 100644 index 00000000..12d8d62f --- /dev/null +++ b/app/src/main/kotlin/org/mifos/mobile/cn/ui/base/BaseActivityCallback.kt @@ -0,0 +1,18 @@ +package org.mifos.mobile.cn.ui.base + +import android.support.v7.widget.Toolbar + +interface BaseActivityCallback { + + fun getToolbar(): Toolbar + + fun showJusticeProgressDialog(message: String) + + fun showTabLayout(show: Boolean) + + fun setToolbarTitle(toolbarTitle: String) + + fun hideJusticeProgressDialog() + + fun logout() +} diff --git a/app/src/main/kotlin/org/mifos/mobile/cn/ui/base/BasePresenter.kt b/app/src/main/kotlin/org/mifos/mobile/cn/ui/base/BasePresenter.kt new file mode 100644 index 00000000..b3aedf69 --- /dev/null +++ b/app/src/main/kotlin/org/mifos/mobile/cn/ui/base/BasePresenter.kt @@ -0,0 +1,34 @@ +package org.mifos.mobile.cn.ui.base + +import android.content.Context + +/** + * Base class that implements the CasePresenter interface and provides a base implementation for + * attachView() and detachView(). It also handles keeping a reference to the mvpView that + * can be accessed from the children classes by calling getMvpView(). + */ +open class BasePresenter constructor(var context: Context) : Presenter { + + private var mvpView: T? = null + val getMvpView: T + get() { return mvpView ?: throw MvpViewNotAttachedException() } + + fun isViewAttached(): Boolean { + return mvpView != null + } + + override fun attachView(mvpView: T) { + this.mvpView = mvpView + } + + override fun detachView() { + mvpView = null + } + + fun checkViewAttached() { + if (!isViewAttached()) throw MvpViewNotAttachedException() + } + + class MvpViewNotAttachedException : RuntimeException( + "Please call Presenter.attachView(MvpView) before requesting data to the Presenter") +} \ No newline at end of file diff --git a/app/src/main/kotlin/org/mifos/mobile/cn/ui/base/MifosBaseActivity.kt b/app/src/main/kotlin/org/mifos/mobile/cn/ui/base/MifosBaseActivity.kt new file mode 100644 index 00000000..bb3d38d0 --- /dev/null +++ b/app/src/main/kotlin/org/mifos/mobile/cn/ui/base/MifosBaseActivity.kt @@ -0,0 +1,179 @@ +package org.mifos.mobile.cn.ui.base + +import android.annotation.SuppressLint +import android.content.Context +import android.os.Bundle +import android.support.v4.app.Fragment +import android.support.v4.app.FragmentManager +import android.support.v7.app.AppCompatActivity +import android.support.v7.widget.Toolbar +import android.view.MenuItem +import android.view.View +import android.view.inputmethod.InputMethodManager +import android.widget.Toast +import org.mifos.mobile.cn.MifosApplication +import org.mifos.mobile.cn.injection.component.ActivityComponent +import org.mifos.mobile.cn.injection.component.ConfigPersistentComponent +import org.mifos.mobile.cn.injection.component.DaggerConfigPersistentComponent +import org.mifos.mobile.cn.injection.module.ActivityModule +import timber.log.Timber +import org.mifos.mobile.cn.ui.utils.ProgressBarHandler +import java.util.* +import java.util.concurrent.atomic.AtomicLong + + +/** + * @author Rajan Maurya + * On 22/01/18. + */ +@SuppressLint("Registered") +open class MifosBaseActivity : AppCompatActivity(), BaseActivityCallback { + + companion object { + @JvmStatic + private val KEY_ACTIVITY_ID = "KEY_ACTIVITY_ID" + @JvmStatic + private val NEXT_ID = AtomicLong(0) + @SuppressLint("UseSparseArrays") + @JvmStatic + private val componentsMap = HashMap() + } + + private var activityId: Long = 0 + lateinit var activityComponent: ActivityComponent + private lateinit var progressBarHandler: ProgressBarHandler + private lateinit var toolbar: Toolbar + + override fun setContentView(layoutResID: Int) { + super.setContentView(layoutResID) + // toolbar = findViewById(R.id.toolbar) + setSupportActionBar(toolbar) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + progressBarHandler = ProgressBarHandler(this) + + // Create the ActivityComponent and reuses cached ConfigPersistentComponent if this is + // being called after a configuration change. + activityId = savedInstanceState?.getLong(KEY_ACTIVITY_ID) ?: NEXT_ID.getAndIncrement() + + if (componentsMap[activityId] != null) + Timber.i("Reusing ConfigPersistentComponent id=%d", activityId) + + val configPersistentComponent = componentsMap.getOrPut(activityId, { + Timber.i("Creating new ConfigPersistentComponent id=%d", activityId) + + val component = (applicationContext as MifosApplication).applicationComponent + DaggerConfigPersistentComponent.builder() + .applicationComponent(component) + .build() + }) + + activityComponent = configPersistentComponent.activityComponent(ActivityModule(this)) + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + outState.putLong(KEY_ACTIVITY_ID, activityId) + } + + fun hideKeyBoard(view: View) { + val inputManager = this.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + inputManager.hideSoftInputFromWindow(view.windowToken, InputMethodManager + .RESULT_UNCHANGED_SHOWN) + } + + fun showBackButton() { + supportActionBar?.setHomeButtonEnabled(true) + supportActionBar?.setDisplayHomeAsUpEnabled(true) + } + + fun showProgressbar() { + progressBarHandler.show() + } + + fun hideProgressbar() { + progressBarHandler.hide() + } + + fun showToast(message: String) { + Toast.makeText(this, message, Toast.LENGTH_LONG).show() + } + + override fun getToolbar(): Toolbar { + return toolbar + } + + override fun showJusticeProgressDialog(message: String) { + + } + + override fun showTabLayout(show: Boolean) { + + } + + override fun setToolbarTitle(toolbarTitle: String) { + title = toolbarTitle + } + + override fun hideJusticeProgressDialog() { + + } + + override fun logout() { + + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + return when (item.itemId) { + android.R.id.home -> { + onBackPressed() + true + } + else -> super.onOptionsItemSelected(item) + } + } + + /** + * Replace Fragment in FrameLayout Container. + * + * @param fragment Fragment + * @param addToBackStack Add to BackStack + * @param containerId Container Id + */ + fun replaceFragment(fragment: Fragment, addToBackStack: Boolean, containerId: Int) { + invalidateOptionsMenu() + val backStateName = fragment.javaClass.name + val fragmentPopped = supportFragmentManager.popBackStackImmediate(backStateName, 0) + if (!fragmentPopped && supportFragmentManager.findFragmentByTag(backStateName) == null) { + val transaction = supportFragmentManager.beginTransaction() + transaction.replace(containerId, fragment, backStateName) + if (addToBackStack) { + transaction.addToBackStack(backStateName) + } + transaction.commit() + } + } + + fun clearFragmentBackStack() { + val fm = supportFragmentManager + val backStackCount = supportFragmentManager.backStackEntryCount + for (i in 0 until backStackCount) { + val backStackId = supportFragmentManager.getBackStackEntryAt(i).id + fm.popBackStack(backStackId, FragmentManager.POP_BACK_STACK_INCLUSIVE) + } + } + + fun stackCount(): Int { + return supportFragmentManager.backStackEntryCount + } + + override fun onDestroy() { + if (!isChangingConfigurations) { + Timber.i("Clearing ConfigPersistentComponent id=%d", activityId) + componentsMap.remove(activityId) + } + super.onDestroy() + } +} diff --git a/app/src/main/kotlin/org/mifos/mobile/cn/ui/base/MifosBaseFragment.kt b/app/src/main/kotlin/org/mifos/mobile/cn/ui/base/MifosBaseFragment.kt new file mode 100644 index 00000000..f4683b36 --- /dev/null +++ b/app/src/main/kotlin/org/mifos/mobile/cn/ui/base/MifosBaseFragment.kt @@ -0,0 +1,76 @@ +package org.mifos.mobile.cn.ui.base + +import android.app.Activity +import android.content.Context +import android.os.Bundle +import android.support.v4.app.Fragment +import android.support.v7.widget.Toolbar +import android.view.View +import android.view.inputmethod.InputMethodManager +import org.mifos.mobile.cn.ui.utils.ProgressBarHandler + + +open class MifosBaseFragment : Fragment() { + + private lateinit var callback: BaseActivityCallback + private lateinit var progressBarHandler: ProgressBarHandler + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + progressBarHandler = ProgressBarHandler(activity!!) + } + + fun getToolbar() : Toolbar { + return callback.getToolbar() + } + + fun showMiKashBɔksProgressDialog(message: String) { + callback.showJusticeProgressDialog(message) + } + + fun hideMiKashBɔksProgressDialog() { + callback.hideJusticeProgressDialog() + } + + fun hideKeyboard(view: View, context: Context) { + val inputManager = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + inputManager.hideSoftInputFromWindow(view.windowToken, InputMethodManager + .RESULT_UNCHANGED_SHOWN) + } + + fun setToolbarTitle(title: String) { + callback.setToolbarTitle(title) + hideTabLayout() + } + + fun parent(): MifosBaseActivity { + return activity as MifosBaseActivity + } + + fun showMiKashBɔksProgressBar() { + progressBarHandler.show() + } + + fun hideMiKashBɔksProgressBar() { + progressBarHandler.hide() + } + + fun showTabLayout() { + callback.showTabLayout(true) + } + + fun hideTabLayout() { + callback.showTabLayout(false) + } + + override fun onAttach(context: Context?) { + super.onAttach(context) + val activity = context as? Activity + try { + callback = activity as BaseActivityCallback + } catch (e: ClassCastException) { + throw ClassCastException(activity!!.toString() + + " must implement BaseActivityCallback methods") + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/org/mifos/mobile/cn/ui/base/MvpView.kt b/app/src/main/kotlin/org/mifos/mobile/cn/ui/base/MvpView.kt new file mode 100644 index 00000000..05bc4889 --- /dev/null +++ b/app/src/main/kotlin/org/mifos/mobile/cn/ui/base/MvpView.kt @@ -0,0 +1,8 @@ +package org.mifos.mobile.cn.ui.base + +/** + * Base interface that any class that wants to act as a View in the MVP (Model View Presenter) + * pattern must implement. Generally this interface will be extended by a more specific interface + * that then usually will be implemented by an Activity or Fragment. + */ +interface MvpView diff --git a/app/src/main/kotlin/org/mifos/mobile/cn/ui/base/OnItemClickListener.kt b/app/src/main/kotlin/org/mifos/mobile/cn/ui/base/OnItemClickListener.kt new file mode 100644 index 00000000..635172e1 --- /dev/null +++ b/app/src/main/kotlin/org/mifos/mobile/cn/ui/base/OnItemClickListener.kt @@ -0,0 +1,26 @@ +package org.mifos.mobile.cn.ui.base + +import android.view.View + +/** + * A click listener for items. + */ +interface OnItemClickListener { + + /** + * Called when an item is clicked. + * + * @param childView View of the item that was clicked. + * @param position Position of the item that was clicked. + */ + fun onItemClick(childView: View, position: Int) + + /** + * Called when an item is long pressed. + * + * @param childView View of the item that was long pressed. + * @param position Position of the item that was long pressed. + */ + fun onItemLongPress(childView: View, position: Int) + +} diff --git a/app/src/main/kotlin/org/mifos/mobile/cn/ui/base/Presenter.kt b/app/src/main/kotlin/org/mifos/mobile/cn/ui/base/Presenter.kt new file mode 100644 index 00000000..81b44a77 --- /dev/null +++ b/app/src/main/kotlin/org/mifos/mobile/cn/ui/base/Presenter.kt @@ -0,0 +1,12 @@ +package org.mifos.mobile.cn.ui.base + +/** + * Every presenter in the app must either implement this interface or extend BasePresenter + * indicating the MvpView type that wants to be attached with. + */ +interface Presenter { + + fun attachView(mvpView: V) + + fun detachView() +} diff --git a/app/src/main/kotlin/org/mifos/mobile/cn/ui/mifos/MainActivity.kt b/app/src/main/kotlin/org/mifos/mobile/cn/ui/mifos/MainActivity.kt new file mode 100644 index 00000000..cab2b5ab --- /dev/null +++ b/app/src/main/kotlin/org/mifos/mobile/cn/ui/mifos/MainActivity.kt @@ -0,0 +1,13 @@ +package org.mifos.mobile.cn.ui.mifos + +import android.os.Bundle +import android.support.v7.app.AppCompatActivity +import org.mifos.mobile.cn.R + +class MainActivity : AppCompatActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + } +} diff --git a/app/src/main/kotlin/org/mifos/mobile/cn/ui/utils/CheckSelfPermissionAndRequest.kt b/app/src/main/kotlin/org/mifos/mobile/cn/ui/utils/CheckSelfPermissionAndRequest.kt new file mode 100644 index 00000000..f13bc881 --- /dev/null +++ b/app/src/main/kotlin/org/mifos/mobile/cn/ui/utils/CheckSelfPermissionAndRequest.kt @@ -0,0 +1,257 @@ +package org.mifos.mobile.cn.ui.utils + +import android.annotation.TargetApi +import android.content.Context +import android.content.DialogInterface +import android.content.Intent +import android.content.pm.PackageManager +import android.net.Uri +import android.os.Build +import android.provider.Settings +import android.support.v4.app.ActivityCompat +import android.support.v4.content.ContextCompat +import android.support.v7.app.AppCompatActivity +import android.widget.Toast +import org.mifos.mobile.cn.R +import org.mifos.mobile.cn.data.local.PreferencesHelper + + +/** + * This Class is the CheckSelfPermissionAndRequest Class + * Created by Rajan Maurya on 03/08/16. + */ +object CheckSelfPermissionAndRequest { + + + /** + * This Method Check the Permission is granted or not to the App. If the Permission granted, + * returns true and If not permission denied then returns false. + * + * @param context Context + * @param permission Manifest.permission...Permission... + * @return Boolean True or False. + */ + fun checkSelfPermission(context: Context, permission: String): Boolean { + return ContextCompat.checkSelfPermission(context, permission) == + PackageManager.PERMISSION_GRANTED + } + + fun checkMultiplePermissions(context: Context, vararg permissions: String): Boolean { + for (permission in permissions) { + if (ContextCompat.checkSelfPermission(context, permission) != + PackageManager.PERMISSION_GRANTED) { + return false + } + } + return true + } + + /** + * This Method is requesting to device to grant the permission. When App is trying to + * request the device to grant the permission, then their is Three cases. + * 1. First case Device Prompt the Permission Dialog to user and user accepted or denied the + * Permission. + * 2. Second case will come, if user will denied the permission, after onclick dialog denied + * button and next time App ask for permission, It will show a Material Dialog and there + * will be a message to tell the user that you have denied the permission before, So do + * you want to give this permission to app or not, If yes then click on Re-Try dialog button + * and if not then click on Dialog button "I'M Sure", to not to give this permission to the + * app. + * + * + * And as user will click on "Re-Try" dialog button, he will be prompt with the with + * permission dialog with "[-] never ask again" and have two options first one to click on + * denied button again and put Un check the never ask check box. In this case, user will + * prompt with permission dialog with "[-] never ask again" in the loop, whenever app ask + * for that permission. + * + * + * and If user will click on "[_/] never ask again" check box then permission dialog with + * that permission will not prompt to the user. + * 3. Third case will came. when user have denied to accept permission with never ask again. + * then user will prompt with dialog and message that you have denied this permission with + * never ask again. but this is necessary permission to this app feature. and to grant + * this permission please click on dialog app settings button and give the permission to + * work with this feature. + * + * @param activity AppCompatActivity + * @param permissions Manifest.permission...Permission... + * @param permissionRequestCode Permission Request Code. + * @param dialogMessageRetry Dialog Message Retry + * @param messageNeverAskAgain Dialog Message Never Ask Again + * @param permissionDeniedStatus Permission Denied Status + */ + @TargetApi(Build.VERSION_CODES.JELLY_BEAN) + fun requestPermissions(activity: AppCompatActivity, + permissions: Array, + permissionRequestCode: Int, + dialogMessageRetry: Array, + messageNeverAskAgain: Array, + permissionDeniedStatus: Array) { + + for (i in permissions.indices) { + + if (ActivityCompat.shouldShowRequestPermissionRationale(activity, permissions[i])) { + + // Show an explanation to the user *asynchronously* -- don't block + // this thread waiting for the user's response! After the user + // sees the explanation, try again to request the permission. + MaterialDialog.Builder().init(activity) + .setTitle(R.string.dialog_permission_denied) + .setMessage(dialogMessageRetry[i]) + .setPositiveButton(R.string.dialog_action_re_try, + DialogInterface.OnClickListener { _, _ -> + ActivityCompat.requestPermissions(activity, permissions, + permissionRequestCode) + }) + .setNegativeButton(R.string.dialog_action_i_am_sure) + .createMaterialDialog() + .show() + } else { + requestSinglePermissions(activity, permissions, permissionRequestCode, + messageNeverAskAgain[i], permissionDeniedStatus[i]) + } + + } + } + + @TargetApi(Build.VERSION_CODES.JELLY_BEAN) + fun requestSinglePermissions(activity: AppCompatActivity, + permissions: Array, + permissionRequestCode: Int, + messageNeverAskAgain: String, + permissionDeniedStatus: String) { + //Requesting Permission, first time to the device. + val preferencesHelper = PreferencesHelper(activity.applicationContext) + if (preferencesHelper.getBoolean(permissionDeniedStatus, true)) { + preferencesHelper.putBoolean(permissionDeniedStatus, false) + + ActivityCompat.requestPermissions(activity, permissions, + permissionRequestCode) + } else { + //Requesting Permission, more the one time and opening the setting to change + // the Permission in App Settings. + MaterialDialog.Builder().init(activity) + .setMessage(messageNeverAskAgain) + .setNegativeButton(R.string.dialog_action_cancel) + .setPositiveButton(R.string.dialog_action_app_settings, + DialogInterface.OnClickListener { _, _ -> + //Making the Intent to grant the permission + val intent = Intent(Settings + .ACTION_APPLICATION_DETAILS_SETTINGS) + val uri = Uri.fromParts(activity.resources.getString( + R.string.package_name), activity.packageName, null) + intent.data = uri + val pm = activity.packageManager + if (intent.resolveActivity(pm) != null) { + activity.startActivityForResult(intent, + ConstantKeys.REQUEST_PERMISSION_SETTING) + } else { + Toast.makeText(activity, activity.getString( + R.string.msg_setting_activity_not_found), + Toast.LENGTH_LONG).show() + } + }) + .createMaterialDialog() + .show() + } + } + + /** + * This Method is requesting to device to grant the permission. When App is trying to + * request the device to grant the permission, then their is Three cases. + * 1. First case Device Prompt the Permission Dialog to user and user accepted or denied the + * Permission. + * 2. Second case will come, if user will denied the permission, after onclick dialog denied + * button and next time App ask for permission, It will show a Material Dialog and there + * will be a message to tell the user that you have denied the permission before, So do + * you want to give this permission to app or not, If yes then click on Re-Try dialog button + * and if not then click on Dialog button "I'M Sure", to not to give this permission to the + * app. + * + * + * And as user will click on "Re-Try" dialog button, he will be prompt with the with + * permission dialog with "[-] never ask again" and have two options first one to click on + * denied button again and put Un check the never ask check box. In this case, user will + * prompt with permission dialog with "[-] never ask again" in the loop, whenever app ask + * for that permission. + * + * + * and If user will click on "[_/] never ask again" check box then permission dialog with + * that permission will not prompt to the user. + * 3. Third case will came. when user have denied to accept permission with never ask again. + * then user will prompt with dialog and message that you have denied this permission with + * never ask again. but this is necessary permission to this app feature. and to grant + * this permission please click on dialog app settings button and give the permission to + * work with this feature. + * + * @param activity AppCompatActivity + * @param permission Manifest.permission...Permission... + * @param permissionRequestCode Permission Request Code. + * @param dialogMessageRetry Dialog Message Retry + * @param messageNeverAskAgain Dialog Message Never Ask Again + * @param permissionDeniedStatus Permission Denied Status + */ + @TargetApi(Build.VERSION_CODES.JELLY_BEAN) + fun requestPermission(activity: AppCompatActivity, + permission: String, + permissionRequestCode: Int, + dialogMessageRetry: String, + messageNeverAskAgain: String, + permissionDeniedStatus: String) { + // Should we show an explanation? + if (ActivityCompat.shouldShowRequestPermissionRationale(activity, permission)) { + + // Show an explanation to the user *asynchronously* -- don't block + // this thread waiting for the user's response! After the user + // sees the explanation, try again to request the permission. + MaterialDialog.Builder().init(activity) + .setTitle(R.string.dialog_permission_denied) + .setMessage(dialogMessageRetry) + .setPositiveButton(R.string.dialog_action_re_try, + DialogInterface.OnClickListener { _, _ -> + ActivityCompat.requestPermissions(activity, arrayOf(permission), + permissionRequestCode) + }) + .setNegativeButton(R.string.dialog_action_i_am_sure) + .createMaterialDialog() + .show() + } else { + + //Requesting Permission, first time to the device. + val preferencesHelper = PreferencesHelper(activity.applicationContext) + if (preferencesHelper.getBoolean(permissionDeniedStatus, true)) { + preferencesHelper.putBoolean(permissionDeniedStatus, false) + + ActivityCompat.requestPermissions(activity, arrayOf(permission), + permissionRequestCode) + } else { + //Requesting Permission, more the one time and opening the setting to change + // the Permission in App Settings. + MaterialDialog.Builder().init(activity) + .setMessage(messageNeverAskAgain) + .setNegativeButton(R.string.dialog_action_cancel) + .setPositiveButton(R.string.dialog_action_app_settings, + DialogInterface.OnClickListener { _, _ -> + //Making the Intent to grant the permission + val intent = Intent(Settings + .ACTION_APPLICATION_DETAILS_SETTINGS) + val uri = Uri.fromParts(activity.resources.getString( + R.string.package_name), activity.packageName, null) + intent.data = uri + val pm = activity.packageManager + if (intent.resolveActivity(pm) != null) { + activity.startActivityForResult(intent, + ConstantKeys.REQUEST_PERMISSION_SETTING) + } else { + Toast.makeText(activity, activity.getString( + R.string.msg_setting_activity_not_found), + Toast.LENGTH_LONG).show() + } + }) + .createMaterialDialog() + .show() + } + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/org/mifos/mobile/cn/ui/utils/ConstantKeys.kt b/app/src/main/kotlin/org/mifos/mobile/cn/ui/utils/ConstantKeys.kt new file mode 100644 index 00000000..ddbe4cb9 --- /dev/null +++ b/app/src/main/kotlin/org/mifos/mobile/cn/ui/utils/ConstantKeys.kt @@ -0,0 +1,16 @@ +package org.mifos.mobile.cn.ui.utils + +/** + * @author Rajan Maurya + * On 18/05/18. + */ +object ConstantKeys { + + const val REQUEST_PERMISSION_SETTING = 254 + const val PERMISSION_REQUEST_ALL = 4 + const val PERMISSION_REQUEST_CAMERA = 5 + + const val PERMISSIONS_WRITE_EXTERNAL_STORAGE_STATUS = "write_status" + const val PERMISSION_READ_EXTERNAL_STORAGE_STATUS = "read_status" + const val PERMISSIONS_CAMERA_STATUS = "camera_status" +} \ No newline at end of file diff --git a/app/src/main/kotlin/org/mifos/mobile/cn/ui/utils/MaterialDialog.kt b/app/src/main/kotlin/org/mifos/mobile/cn/ui/utils/MaterialDialog.kt new file mode 100644 index 00000000..5ef03624 --- /dev/null +++ b/app/src/main/kotlin/org/mifos/mobile/cn/ui/utils/MaterialDialog.kt @@ -0,0 +1,166 @@ +package org.mifos.mobile.cn.ui.utils + +import android.content.Context +import android.content.DialogInterface +import android.support.annotation.StringRes +import android.support.v7.app.AlertDialog +import org.mifos.mobile.cn.R + + +/** + * This Class is the Material Dialog Builder Class + * Created by Rajan Maurya on 03/08/16. + */ +class MaterialDialog { + + class Builder { + + private var materialDialogBuilder: AlertDialog.Builder? = null + + //This is the Default Builder Initialization with Material Style + fun init(context: Context): Builder { + materialDialogBuilder = AlertDialog.Builder(context, R.style.MaterialAlertDialogStyle) + return this + } + + //This method set the custom Material Style + fun init(context: Context, theme: Int): Builder { + materialDialogBuilder = AlertDialog.Builder(context, theme) + return this + } + + //This method set the String Title + fun setTitle(title: String): Builder { + materialDialogBuilder!!.setTitle(title) + return this + } + + //This Method set the String Resources to Title + fun setTitle(@StringRes title: Int): Builder { + materialDialogBuilder!!.setTitle(title) + return this + } + + //This Method set the String Message + fun setMessage(message: String): Builder { + materialDialogBuilder!!.setMessage(message) + return this + } + + //This Method set the String Resources message + fun setMessage(@StringRes message: Int): Builder { + materialDialogBuilder!!.setMessage(message) + return this + } + + //This Method set String Test to the Positive Button and set the Onclick null + fun setPositiveButton(positiveText: String): Builder { + materialDialogBuilder!!.setPositiveButton(positiveText, null) + return this + } + + //This Method Set the String Resources Text To Positive Button + fun setPositiveButton(@StringRes positiveText: Int): Builder { + materialDialogBuilder!!.setPositiveButton(positiveText, null) + return this + } + + //This Method set the String Text to Positive Button and set the OnClick Event to it + fun setPositiveButton(positiveText: String, + listener: DialogInterface.OnClickListener): Builder { + materialDialogBuilder!!.setPositiveButton(positiveText, listener) + return this + } + + //This method set the String Resources text To Positive button and set the Onclick Event + fun setPositiveButton(@StringRes positiveText: Int, + listener: DialogInterface.OnClickListener): Builder { + materialDialogBuilder!!.setPositiveButton(positiveText, listener) + return this + } + + //This Method the String Text to Negative Button and Set the onclick event to null + fun setNegativeButton(negativeText: String): Builder { + materialDialogBuilder!!.setNegativeButton(negativeText, null) + return this + } + + //This Method set the String Resources Text to Negative button + // and set the onclick event to null + fun setNegativeButton(@StringRes negativeText: Int): Builder { + materialDialogBuilder!!.setNegativeButton(negativeText, null) + return this + } + + //This Method set String Text to Negative Button and + //Set the Onclick event + fun setNegativeButton(negativeText: String, + listener: DialogInterface.OnClickListener): Builder { + materialDialogBuilder!!.setNegativeButton(negativeText, listener) + return this + } + + //This method set String Resources Text to Negative Button and set Onclick Event + fun setNegativeButton(@StringRes negativeText: Int, + listener: DialogInterface.OnClickListener): Builder { + materialDialogBuilder!!.setNegativeButton(negativeText, listener) + return this + } + + //This Method the String Text to Neutral Button and Set the onclick event to null + fun setNeutralButton(neutralText: String): Builder { + materialDialogBuilder!!.setNeutralButton(neutralText, null) + return this + } + + //This Method set the String Resources Text to Neutral button + // and set the onclick event to null + fun setNeutralButton(@StringRes neutralText: Int): Builder { + materialDialogBuilder!!.setNeutralButton(neutralText, null) + return this + } + + //This Method set String Text to Neutral Button and + //Set the Onclick event + fun setNeutralButton(neutralText: String, + listener: DialogInterface.OnClickListener): Builder { + materialDialogBuilder!!.setNeutralButton(neutralText, listener) + return this + } + + //This method set String Resources Text to Neutral Button and set Onclick Event + fun setNeutralButton(@StringRes neutralText: Int, + listener: DialogInterface.OnClickListener): Builder { + materialDialogBuilder!!.setNeutralButton(neutralText, listener) + return this + } + + fun setCancelable(cancelable: Boolean?): Builder { + materialDialogBuilder!!.setCancelable(cancelable!!) + return this + } + + fun setItems(items: Int, listener: DialogInterface.OnClickListener): Builder { + materialDialogBuilder!!.setItems(items, listener) + return this + } + + fun setItems(items: Array, + listener: DialogInterface.OnClickListener): Builder { + materialDialogBuilder!!.setItems(items, listener) + return this + } + + //This Method Create the Final Material Dialog + fun createMaterialDialog(): Builder { + materialDialogBuilder!!.create() + return this + } + + //This Method Show the Dialog + fun show(): Builder { + materialDialogBuilder!!.show() + return this + } + } +} diff --git a/app/src/main/kotlin/org/mifos/mobile/cn/ui/utils/ODateUtils.kt b/app/src/main/kotlin/org/mifos/mobile/cn/ui/utils/ODateUtils.kt new file mode 100644 index 00000000..6880fdd2 --- /dev/null +++ b/app/src/main/kotlin/org/mifos/mobile/cn/ui/utils/ODateUtils.kt @@ -0,0 +1,280 @@ +package org.mifos.mobile.cn.ui.utils + +import android.util.Log +import java.text.ParseException +import java.text.SimpleDateFormat +import java.util.* +import java.util.concurrent.TimeUnit + +object ODateUtils { + val TAG = ODateUtils::class.java.simpleName + val DEFAULT_FORMAT = "yyyy-MM-dd HH:mm:ss" + val DEFAULT_DATE_FORMAT = "yyyy-MM-dd" + val DEFAULT_TIME_FORMAT = "HH:mm:ss" + val DATE_DISPLAY_FORMAT = "dd/MM/yyyy" + val DATETIME_DISPLAY_FORMAT = "dd/MM/yyyy HH:mm" + val DATETIME_SERVER_FORMAT = "yyyy/MM/dd HH:mm" + val DATE_FORMAT_SORTABLE = "yyyyMMdd_HHmmss_SSS" + val EXTERNAL_STORAGE_FOLDER = "Judiciary Notes Foss" + val DATE_REGEX_FORMAT = "[0-9]{2}/[0-9]{2}/[0-9]{4}" + val DATETIME_REGEX_FORMAT = "[0-9]{2}/[0-9]{2}/[0-9]{4} [0-9]{2}:[0-9]{2}" + val DATE_HINT_FORMAT = "Date format should be dd/mm/yyyy e.g. 20/09/2014" + + + /** + * Return Current date string in "yyyy-MM-dd HH:mm:ss" format + * + * @return current date string (Default timezone) + */ + val date: String + get() = getDate(Date(), DEFAULT_FORMAT) + + /** + * Returns UTC date string in "yyyy-MM-dd HH:mm:ss" format. + * + * @return string, UTC Date + */ + val utcDate: String + get() = getUTCDate(Date(), DEFAULT_FORMAT) + + fun getFutureDate(date: String, second: Int): String { + return getFutureDate(date, DEFAULT_FORMAT, second) + } + + /** + * Returns current date string in given format + * + * @param format, date format + * @return current date string (Default timezone) + */ + fun getDate(format: String): String { + return getDate(Date(), format) + } + + /** + * Returns current date string in given format + * + * @param date, date object + * @param defaultFormat, date format + * @return current date string (default timezone) + */ + fun getDate(date: Date, defaultFormat: String): String { + return createDate(date, defaultFormat, false) + } + + fun getFutureDate(date: String, defaultFormat: String, second: Int): String { + return createFutureDate(date, defaultFormat, false, second) + } + + /** + * Return UTC date in given format + * + * @param format, date format + * @return UTC date string + */ + fun getUTCDate(format: String): String { + return getUTCDate(Date(), format) + } + + /** + * Returns UTC Date string in given date format + * + * @param date, Date object + * @param defaultFormat, Date pattern format + * @return UTC date string + */ + fun getUTCDate(date: Date, defaultFormat: String): String { + return createDate(date, defaultFormat, true) + } + + /** + * Convert UTC date to default timezone + * + * @param date UTC date string + * @param dateFormat default date format + * @param toFormat converting date format + * @return string converted date string + */ + @JvmOverloads + fun convertToDefault(date: String, dateFormat: String, toFormat: String = dateFormat): String { + return createDate(createDateObject(date, dateFormat, false), toFormat, false) + } + + /** + * Convert default timezone date to UTC timezone + * + * @param date, date in string + * @param dateFormat default date format + * @param toFormat display format + * @return string, returns string converted to UTC + */ + @JvmOverloads + fun convertToUTC(date: String, dateFormat: String, toFormat: String = dateFormat): String { + return createDate(createDateObject(date, dateFormat, true), toFormat, true) + } + + fun parseDate(date: String, dateFormat: String, toFormat: String): String { + return createDate(createDateObject(date, dateFormat, false), toFormat, true) + } + + /** + * Create Date instance from given date string. + * + * @param date date in string + * @param dateFormat, original date format + * @param hasDefaultTimezone if date is in default timezone than true, otherwise false + * @return Date, returns Date object with given date + */ + fun createDateObject(date: String, dateFormat: String, + hasDefaultTimezone: Boolean?): Date? { + var dateObj: Date? = null + try { + val simpleDateFormat = SimpleDateFormat(dateFormat) + if ((!hasDefaultTimezone!!)) { + simpleDateFormat.timeZone = TimeZone.getTimeZone("GMT") + } + dateObj = simpleDateFormat.parse(date) + } catch (e: Exception) { + Log.e(TAG, e.message) + } + + return dateObj + } + + /** + * Returns date before given days + * + * @param days days to before + * @return string date string before days + */ + fun getDateBefore(days: Int): String { + val today = Date() + val cal = GregorianCalendar() + cal.time = today + cal.add(Calendar.DAY_OF_MONTH, days * -1) + val date = cal.time + val gmtFormat = SimpleDateFormat() + gmtFormat.applyPattern("yyyy-MM-dd 00:00:00") + val gmtTime = TimeZone.getTimeZone("GMT") + gmtFormat.timeZone = gmtTime + return gmtFormat.format(date) + } + + fun setDateTime(originalDate: Date, hour: Int, minute: Int, second: Int): Date { + val cal = GregorianCalendar() + cal.time = originalDate + cal.set(Calendar.HOUR, hour) + cal.set(Calendar.MINUTE, minute) + cal.set(Calendar.SECOND, second) + cal.set(Calendar.MILLISECOND, 0) + return cal.time + } + + fun getDateDayBeforeAfterUTC(utcDate: String, days: Int): String { + val dt = createDateObject(utcDate, DEFAULT_FORMAT, false) + val cal = GregorianCalendar() + cal.time = dt + cal.add(Calendar.DAY_OF_MONTH, days) + return createDate(cal.time, DEFAULT_FORMAT, true) + } + + fun getDateDayBefore(originalDate: Date, days: Int): Date { + val cal = GregorianCalendar() + cal.time = originalDate + cal.add(Calendar.DAY_OF_MONTH, days * -1) + return cal.time + } + + fun getCurrentDateWithHour(addHour: Int): String { + val cal = Calendar.getInstance() + val hour = cal.get(Calendar.HOUR) + cal.set(Calendar.HOUR, hour + addHour) + val date = cal.time + return createDate(date, DEFAULT_FORMAT, true) + } + + fun getDateMinuteBefore(originalDate: Date, minutes: Int): Date { + val cal = GregorianCalendar() + cal.time = originalDate + cal.add(Calendar.MINUTE, minutes * -1) + return cal.time + } + + private fun createDate(date: Date?, defaultFormat: String, utc: Boolean?): String { + val gmtFormat = SimpleDateFormat() + gmtFormat.applyPattern(defaultFormat) + val gmtTime = if (utc!!) TimeZone.getTimeZone("GMT") else TimeZone.getDefault() + gmtFormat.timeZone = gmtTime + return gmtFormat.format(date) + } + + private fun createFutureDate(date: String, defaultFormat: String, utc: Boolean?, + second: Int): String { + val gmtFormat = SimpleDateFormat() + val previousDateFormat = SimpleDateFormat(defaultFormat) + gmtFormat.applyPattern(defaultFormat) + val gmtTime = if (utc!!) TimeZone.getTimeZone("GMT") else TimeZone.getDefault() + gmtFormat.timeZone = gmtTime + val calendar = Calendar.getInstance() + try { + calendar.time = previousDateFormat.parse(date) + } catch (e: ParseException) { + Log.d(TAG, e.localizedMessage) + } + + calendar.add(Calendar.SECOND, second) + return gmtFormat.format(calendar.time) + } + + fun floatToDuration(durationInFloat: String): String { + var durationInFloat = durationInFloat + durationInFloat = String.format("%2.2f", java.lang.Float.parseFloat(durationInFloat)) + val parts = durationInFloat.split("\\.".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() + val minute = java.lang.Long.parseLong(parts[0]) + val seconds = 60 * java.lang.Long.parseLong(parts[1]) / 100 + return String.format("%02d:%02d", minute, seconds) + } + + fun durationToFloat(duration: String): String { + val parts = duration.split("\\:".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() + if (parts.size == 2) { + var minute = java.lang.Long.parseLong(parts[0]) + var seconds = java.lang.Long.parseLong(parts[1]) + if (seconds == 60L) { + minute += 1 + seconds = 0 + } else { + seconds = 100 * seconds / 60 + } + return String.format("%d.%d", minute, seconds) + } + return "false" + } + + fun durationToFloat(milliseconds: Long): String { + var minute = TimeUnit.MILLISECONDS.toMinutes(milliseconds) + var seconds = TimeUnit.MILLISECONDS.toSeconds(milliseconds) - + TimeUnit.MINUTES.toSeconds(minute) + if (seconds == 60L) { + minute += 1 + seconds = 0 + } else { + seconds = 100 * seconds / 60 + } + return String.format("%d.%d", minute, seconds) + } +} +/** + * Convert UTC date to default timezone date + * + * @param date date in string + * @param dateFormat default date format + * @return string converted date string + */ +/** + * Convert to UTC date + * + * @param date date in string + * @param dateFormat default date format + * @return string date string in UTC timezone + */ \ No newline at end of file diff --git a/app/src/main/kotlin/org/mifos/mobile/cn/ui/utils/ProgressBarHandler.kt b/app/src/main/kotlin/org/mifos/mobile/cn/ui/utils/ProgressBarHandler.kt new file mode 100644 index 00000000..ab10b741 --- /dev/null +++ b/app/src/main/kotlin/org/mifos/mobile/cn/ui/utils/ProgressBarHandler.kt @@ -0,0 +1,48 @@ +package org.mifos.mobile.cn.ui.utils + +import android.R.attr +import android.R.id +import android.app.Activity +import android.content.Context +import android.view.Gravity +import android.view.View +import android.view.ViewGroup +import android.widget.ProgressBar +import android.widget.RelativeLayout +import android.widget.RelativeLayout.LayoutParams + + +/** + * @author Rajan Maurya + */ +class ProgressBarHandler constructor(context: Context) { + + private val progressBar: ProgressBar + + init { + + val layout = (context as Activity).findViewById(id.content).rootView as ViewGroup + + progressBar = ProgressBar(context, null, attr.progressBarStyleInverse) + progressBar.isIndeterminate = true + + val params = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT) + + val relativeLayout = RelativeLayout(context) + + relativeLayout.gravity = Gravity.CENTER + relativeLayout.addView(progressBar) + + layout.addView(relativeLayout, params) + + hide() + } + + fun show() { + progressBar.visibility = View.VISIBLE + } + + fun hide() { + progressBar.visibility = View.INVISIBLE + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/org/mifos/mobile/cn/ui/utils/Toaster.kt b/app/src/main/kotlin/org/mifos/mobile/cn/ui/utils/Toaster.kt new file mode 100644 index 00000000..9bd64077 --- /dev/null +++ b/app/src/main/kotlin/org/mifos/mobile/cn/ui/utils/Toaster.kt @@ -0,0 +1,59 @@ +package org.mifos.mobile.cn.ui.utils + +import android.graphics.Color +import android.support.design.widget.Snackbar +import android.util.Log +import android.view.View +import android.widget.TextView +import org.mifos.mobile.cn.MifosApplication + +object Toaster { + + val LOG_TAG = Toaster::class.java.simpleName + + val INDEFINITE = Snackbar.LENGTH_INDEFINITE + val LONG = Snackbar.LENGTH_LONG + val SHORT = Snackbar.LENGTH_SHORT + + private var hideSnackbad: Snackbar? = null + + @JvmOverloads + fun show(view: View, text: String, duration: Int = Snackbar.LENGTH_LONG) { + val snackbar = Snackbar.make(view, text, duration) + val sbView = snackbar.view + val textView = sbView.findViewById(android.support.design.R.id + .snackbar_text) as TextView + textView.setTextColor(Color.WHITE) + textView.textSize = 12f + textView.maxLines = 5 + snackbar.setAction("OK") { snackbar.dismiss() } + snackbar.show() + } + + fun showProgressMessage(view: View, text: String, duration: Int) { + val snackbar = Snackbar.make(view, text, duration) + val sbView = snackbar.view + val textView = sbView.findViewById(android.support.design.R.id + .snackbar_text) as TextView + textView.setTextColor(Color.WHITE) + textView.textSize = 12f + snackbar.show() + hideSnackbad = snackbar + } + + fun hideSnackbar() { + try { + hideSnackbad!!.dismiss() + } catch (e: NullPointerException) { + Log.d(LOG_TAG, e.localizedMessage) + } + } + + fun show(view: View, res: Int, duration: Int) { + show(view, MifosApplication.getContext().resources.getString(res), duration) + } + + fun show(view: View, res: Int) { + show(view, MifosApplication.getContext().resources.getString(res)) + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/org/mifos/mobile/cn/ui/utils/Utils.kt b/app/src/main/kotlin/org/mifos/mobile/cn/ui/utils/Utils.kt new file mode 100644 index 00000000..9a15bd6e --- /dev/null +++ b/app/src/main/kotlin/org/mifos/mobile/cn/ui/utils/Utils.kt @@ -0,0 +1,9 @@ +package org.mifos.mobile.cn.ui.utils + +/** + * @author Rajan Maurya + * On 28/05/18. + */ +class Utils { + +} \ No newline at end of file diff --git a/app/src/main/kotlin/org/mifos/mobile/cn/ui/utils/ValidationUtil.kt b/app/src/main/kotlin/org/mifos/mobile/cn/ui/utils/ValidationUtil.kt new file mode 100644 index 00000000..8fa388ca --- /dev/null +++ b/app/src/main/kotlin/org/mifos/mobile/cn/ui/utils/ValidationUtil.kt @@ -0,0 +1,45 @@ +package org.mifos.mobile.cn.ui.utils + +import android.support.design.widget.TextInputLayout +import android.text.TextUtils +import android.view.View +import android.widget.EditText +import android.widget.Spinner + +/** + * @author Rajan Maurya + * On 26/05/18. + */ +class ValidationUtil { + + companion object { + + fun validateTextFields(editText: EditText, textInputLayout: TextInputLayout, + message: String): Boolean { + if (TextUtils.isEmpty(editText.text.toString().trim())) { + showTextInputLayoutError(textInputLayout, message) + return true + } + hideTextInputLayoutError(textInputLayout) + return false + } + + fun validateSpinners(rootView: View, spinner: Spinner, message: String): Boolean { + if (spinner.selectedItemPosition == 0) { + Toaster.show(rootView, message) + return true + } + return false + } + + fun showTextInputLayoutError(textInputLayout: TextInputLayout, errorMessage: String) { + textInputLayout.isErrorEnabled = true + textInputLayout.error = errorMessage + } + + fun hideTextInputLayoutError(textInputLayout: TextInputLayout) { + textInputLayout.isErrorEnabled = false + textInputLayout.error = null + } + } +} diff --git a/app/src/main/kotlin/org/mifos/mobile/cn/ui/utils/glideutils/MifosGlideModule.kt b/app/src/main/kotlin/org/mifos/mobile/cn/ui/utils/glideutils/MifosGlideModule.kt new file mode 100644 index 00000000..ed3a5b4c --- /dev/null +++ b/app/src/main/kotlin/org/mifos/mobile/cn/ui/utils/glideutils/MifosGlideModule.kt @@ -0,0 +1,11 @@ +package org.mifos.mobile.cn.ui.utils.glideutils + +import com.bumptech.glide.annotation.GlideModule +import com.bumptech.glide.module.AppGlideModule + +/** + * @author Rajan Maurya + * On 18/05/18. + */ +@GlideModule +class MifosGlideModule : AppGlideModule() \ No newline at end of file diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 00000000..8e9dacc9 --- /dev/null +++ b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 00000000..d981c551 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 00000000..e21f93a5 --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,19 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 00000000..bbd3e021 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 00000000..bbd3e021 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 00000000..a2f59082 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 00000000..1b523998 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 00000000..ff10afd6 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 00000000..115a4c76 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 00000000..dcd3cd80 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 00000000..459ca609 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 00000000..8ca12fe0 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 00000000..8e19b410 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 00000000..b824ebdd Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 00000000..4c19a13c Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 00000000..e8a99712 --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,67 @@ + + + #3F51B5 + #303F9F + #FF4081 + #212121 + #757575 + #9F8E8E + + #ffffff + + #ff33b5e5 + + #33999999 + + #BB666666 + + #ff99cc00 + + #ffff4444 + + #ff0099cc + + #ff669900 + + #ffcc0000 + + #ffaa66cc + + #ffffbb33 + + #ffff8800 + + #ff00ddff + + #33CCCCCC + + #0a46b1 + + #c9d8e9 + + #000000 + #ffd1d1d1 + + #ff14c416 + #ff8bf98a + #fff9ac06 + #FF87DBF9 + #fff9393c + + #bbbbbb + #9e9e9e + #E9E2E2 + #049314 + #787171 + #eaeaea + #c1c1c1 + + + + + @color/blue_light + @color/green_light + @color/red_light + @color/orange_light + + diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml new file mode 100644 index 00000000..475ea69e --- /dev/null +++ b/app/src/main/res/values/dimens.xml @@ -0,0 +1,36 @@ + + + 2dp + 4dp + 8dp + 5dp + 7dp + 10dp + 15dp + 16dp + 24dp + 30dp + 50dp + 64dp + 75dp + + 16dp + 16dp + 8dp + 176dp + 16dp + 5dp + 8dp + + 12sp + 18sp + 14sp + 16sp + 18sp + 20sp + + 24sp + 20sp + 16sp + 14sp + \ No newline at end of file diff --git a/app/src/main/res/values/ids.xml b/app/src/main/res/values/ids.xml new file mode 100755 index 00000000..220456b8 --- /dev/null +++ b/app/src/main/res/values/ids.xml @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 00000000..7b68995b --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,15 @@ + + mifos-mobile-cn + + OK + Cancel + Back + Permission Denied + I\'M Sure + Retry + App Settings + package + Something went wrong finding Settings activity. + \nGo to \'Settings\' and grant permission manually. + + diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml new file mode 100644 index 00000000..a0a7cd84 --- /dev/null +++ b/app/src/main/res/values/styles.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/test/java/org/mifos/mobile/cn/ExampleUnitTest.kt b/app/src/test/java/org/mifos/mobile/cn/ExampleUnitTest.kt new file mode 100644 index 00000000..f4bf5f50 --- /dev/null +++ b/app/src/test/java/org/mifos/mobile/cn/ExampleUnitTest.kt @@ -0,0 +1,17 @@ +package org.mifos.mobile.cn + +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/resources/test.json b/app/src/test/resources/test.json new file mode 100644 index 00000000..0e0dcd23 --- /dev/null +++ b/app/src/test/resources/test.json @@ -0,0 +1,3 @@ +{ + +} \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 00000000..b82c0b5c --- /dev/null +++ b/build.gradle @@ -0,0 +1,62 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. + +buildscript { + ext.kotlin_version = '1.2.41' + repositories { + google() + jcenter() + mavenCentral() + } + dependencies { + classpath 'com.android.tools.build:gradle:3.1.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 + } +} + +allprojects { + repositories { + google() + jcenter() + mavenCentral() + } +} + +task clean(type: Delete) { + delete rootProject.buildDir +} + +ext { + // Sdk and tools + minSdkVersion = 15 + targetSdkVersion = 27 + compileSdkVersion = 27 + buildToolsVersion = '27.0.3' + + // App dependencies + supportLibraryVersion = '27.1.1' + daggerVersion = '2.8' + retrofitVersion = '2.3.0' + okHttp3Version = '3.8.0' + + // Kotlin dependencies + kotlinVersion = '1.2.41' + + // rxjava dependencies + rxjavaVersion = '2.1.8' + rxandroidVersion = '2.0.2' + rxKotlinVersion = '2.2.0' + + // DBFlow dependencies + dbFlowVersion = '4.2.4' + + junitVersion = '4.12' + mockitoVersion = '2.6.2' + powerMockito = '1.6.2' + hamcrestVersion = '1.3' + runnerVersion = '0.5' + rulesVersion = '0.5' + espressoVersion = '3.0.1' +} \ No newline at end of file diff --git a/config/quality/checkstyle/checkstyle-config.xml b/config/quality/checkstyle/checkstyle-config.xml new file mode 100644 index 00000000..f0f9cbf6 --- /dev/null +++ b/config/quality/checkstyle/checkstyle-config.xml @@ -0,0 +1,163 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/config/quality/findbugs/android-exclude-filter.xml b/config/quality/findbugs/android-exclude-filter.xml new file mode 100644 index 00000000..7731046b --- /dev/null +++ b/config/quality/findbugs/android-exclude-filter.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/config/quality/pmd/pmd-ruleset.xml b/config/quality/pmd/pmd-ruleset.xml new file mode 100644 index 00000000..b9d2aa03 --- /dev/null +++ b/config/quality/pmd/pmd-ruleset.xml @@ -0,0 +1,40 @@ + + + + Custom ruleset for Mifos-Mobile-CN Android application + + .*/R.java + .*/gen/.* + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/config/quality/quality.gradle b/config/quality/quality.gradle new file mode 100644 index 00000000..2787d9b4 --- /dev/null +++ b/config/quality/quality.gradle @@ -0,0 +1,90 @@ +/** + * Set up Checkstyle, Findbugs and PMD to perform extensive code analysis. + * + * Gradle tasks added: + * - checkstyle + * - findbugs + * - pmd + * + * The three tasks above are added as dependencies of the check task so running check will + * run all of them. + */ + +apply plugin: 'checkstyle' +apply plugin: 'findbugs' +apply plugin: 'pmd' + +dependencies { + checkstyle 'com.puppycrawl.tools:checkstyle:8.7' +} + +def qualityConfigDir = "$project.rootDir/config/quality" +def reportsDir = "$project.buildDir/reports" + +check.dependsOn 'checkstyle', 'findbugs', 'pmd' + +task checkstyle(type: Checkstyle, group: 'Verification', description: 'Runs code style checks') { + configFile file("$qualityConfigDir/checkstyle/checkstyle-config.xml") + source 'src' + include '**/*.java' + + reports { + xml.enabled = true + xml { + destination file("$reportsDir/checkstyle/checkstyle.xml") + } + } + + classpath = files( ) +} + +task findbugs(type: FindBugs, + group: 'Verification', + description: 'Inspect java bytecode for bugs', + dependsOn: ['compileDebugSources','compileReleaseSources']) { + + ignoreFailures = false + effort = "max" + reportLevel = "high" + excludeFilter = new File("$qualityConfigDir/findbugs/android-exclude-filter.xml") + classes = files("$project.rootDir/app/build/intermediates/classes") + + source 'src' + include '**/*.java' + exclude '**/gen/**' + + reports { + xml.enabled = false + html.enabled = true + xml { + destination file("$reportsDir/findbugs/findbugs.xml") + } + html { + destination file("$reportsDir/findbugs/findbugs.html") + } + } + + classpath = files() +} + + +task pmd(type: Pmd, group: 'Verification', description: 'Inspect sourcecode for bugs') { + ruleSetFiles = files("$qualityConfigDir/pmd/pmd-ruleset.xml") + ignoreFailures = false + ruleSets = [] + + source 'src' + include '**/*.java' + exclude '**/gen/**' + + reports { + xml.enabled = true + html.enabled = true + xml { + destination file("$reportsDir/pmd/pmd.xml") + } + html { + destination file("$reportsDir/pmd/pmd.html") + } + } +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 00000000..743d692c --- /dev/null +++ b/gradle.properties @@ -0,0 +1,13 @@ +# Project-wide Gradle settings. +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx1536m +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..7a3265ee Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..97e6a21d --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Tue May 29 20:44:38 IST 2018 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip diff --git a/gradlew b/gradlew new file mode 100755 index 00000000..cccdd3d5 --- /dev/null +++ b/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 00000000..e95643d6 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 00000000..e7b4def4 --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +include ':app'