store API key encrypted

This commit is contained in:
Phillip Thelen 2019-06-03 12:15:56 +02:00
parent 4e5f092869
commit 49c7795be5
37 changed files with 429 additions and 55 deletions

View file

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal">
<com.facebook.drawee.view.SimpleDraweeView
android:id="@+id/icon_view"
android:layout_width="48dp"
android:layout_height="52dp"
android:scaleType="center"
android:layout_marginBottom="@dimen/spacing_large"/>
<TextView
android:id="@+id/description_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/Body2"
android:gravity="center_horizontal"/>
</LinearLayout>

View file

@ -244,7 +244,7 @@ abstract class HabiticaBaseApplication : MultiDexApplication() {
}
fun checkUserAuthentication(context: Context, hostConfig: HostConfig?): Boolean {
if (hostConfig?.api == null || hostConfig.api == "" || hostConfig.user == "") {
if (hostConfig?.apiKey == null || hostConfig.apiKey == "" || hostConfig.userID == "") {
startActivity(IntroActivity::class.java, context)
return false

View file

@ -2,10 +2,11 @@ package com.habitrpg.android.habitica.api
import android.content.Context
import android.content.SharedPreferences
import android.preference.PreferenceManager
import androidx.core.content.edit
import com.habitrpg.android.habitica.BuildConfig
import com.habitrpg.android.habitica.R
import com.habitrpg.android.habitica.helpers.KeyHelper
/**
* The configuration of the host<br></br>
@ -16,11 +17,10 @@ import com.habitrpg.android.habitica.R
class HostConfig {
var address: String
var port: String
var api: String
var user: String
var apiKey: String
var userID: String
constructor(sharedPreferences: SharedPreferences, context: Context) {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
constructor(sharedPreferences: SharedPreferences, keyHelper: KeyHelper?, context: Context) {
this.port = BuildConfig.PORT
if (BuildConfig.DEBUG) {
this.address = BuildConfig.BASE_URL
@ -32,19 +32,41 @@ class HostConfig {
this.address = context.getString(R.string.base_url)
}
}
this.api = prefs.getString("APIToken", null) ?: ""
this.user = prefs.getString(context.getString(R.string.SP_userID), null) ?: ""
this.userID = sharedPreferences.getString(context.getString(R.string.SP_userID), null) ?: ""
this.apiKey = loadAPIKey(sharedPreferences, keyHelper)
}
private fun loadAPIKey(sharedPreferences: SharedPreferences, keyHelper: KeyHelper?): String {
return if (sharedPreferences.contains(userID)) {
val encryptedKey = sharedPreferences.getString(userID, null)
if (encryptedKey?.isNotBlank() == true) {
keyHelper?.decrypt(encryptedKey)
} else {
""
}
} else {
val key = sharedPreferences.getString("APIToken", null)
if (key?.isNotBlank() == true) {
val encryptedKey = keyHelper?.encrypt(key)
sharedPreferences.edit {
putString(userID, encryptedKey)
remove("APIToken")
}
}
key
} ?: ""
}
constructor(address: String, port: String, api: String, user: String) {
this.address = address
this.port = port
this.api = api
this.user = user
this.apiKey = api
this.userID = user
}
fun hasAuthentication(): Boolean {
return user.isNotEmpty() && api.isNotEmpty()
return userID.isNotEmpty() && apiKey.isNotEmpty()
}
}

View file

@ -90,9 +90,9 @@ class ApiClientImpl//private OnHabitsAPIResult mResultListener;
this.popupNotificationsManager.setApiClient(this)
HabiticaBaseApplication.component?.inject(this)
crashlyticsProxy.setUserIdentifier(this.hostConfig.user)
crashlyticsProxy.setUserName(this.hostConfig.user)
Amplitude.getInstance().userId = this.hostConfig.user
crashlyticsProxy.setUserIdentifier(this.hostConfig.userID)
crashlyticsProxy.setUserName(this.hostConfig.userID)
Amplitude.getInstance().userId = this.hostConfig.userID
buildRetrofit()
}
@ -115,8 +115,8 @@ class ApiClientImpl//private OnHabitsAPIResult mResultListener;
var builder: Request.Builder = original.newBuilder()
if (this.hostConfig.hasAuthentication()) {
builder = builder
.header("x-api-key", this.hostConfig.api)
.header("x-api-user", this.hostConfig.user)
.header("x-api-key", this.hostConfig.apiKey)
.header("x-api-user", this.hostConfig.userID)
}
builder = builder.header("x-client", "habitica-android")
.header("x-user-timezoneOffset", timezoneOffset.toString())
@ -256,7 +256,7 @@ class ApiClientImpl//private OnHabitsAPIResult mResultListener;
}
override fun hasAuthenticationKeys(): Boolean {
return this.hostConfig.user.isNotEmpty() && hostConfig.api.isNotEmpty()
return this.hostConfig.userID.isNotEmpty() && hostConfig.apiKey.isNotEmpty()
}
private fun showConnectionProblemDialog(resourceMessageString: Int) {
@ -282,11 +282,11 @@ class ApiClientImpl//private OnHabitsAPIResult mResultListener;
}
override fun updateAuthenticationCredentials(userID: String?, apiToken: String?) {
this.hostConfig.user = userID ?: ""
this.hostConfig.api = apiToken ?: ""
crashlyticsProxy.setUserIdentifier(this.hostConfig.user)
crashlyticsProxy.setUserName(this.hostConfig.user)
Amplitude.getInstance().userId = this.hostConfig.user
this.hostConfig.userID = userID ?: ""
this.hostConfig.apiKey = apiToken ?: ""
crashlyticsProxy.setUserIdentifier(this.hostConfig.userID)
crashlyticsProxy.setUserName(this.hostConfig.userID)
Amplitude.getInstance().userId = this.hostConfig.userID
}
override fun setLanguageCode(languageCode: String) {

View file

@ -2,7 +2,7 @@ package com.habitrpg.android.habitica.extensions
import android.content.DialogInterface
import com.habitrpg.android.habitica.R
import com.habitrpg.android.habitica.ui.views.HabiticaAlertDialog
import com.habitrpg.android.habitica.ui.views.dialogs.HabiticaAlertDialog
fun HabiticaAlertDialog.addOkButton(isPrimary: Boolean = true, listener: ((HabiticaAlertDialog, Int) -> Unit)? = null) {
this.addButton(R.string.ok, isPrimary, listener)

View file

@ -0,0 +1,245 @@
package com.habitrpg.android.habitica.helpers
import android.content.Context
import android.content.SharedPreferences
import android.os.Build
import android.security.KeyPairGeneratorSpec
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import android.util.Base64
import androidx.core.content.edit
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.IOException
import java.io.UnsupportedEncodingException
import java.math.BigInteger
import java.security.*
import java.util.*
import javax.crypto.*
import javax.crypto.spec.GCMParameterSpec
import javax.crypto.spec.SecretKeySpec
import javax.security.auth.x500.X500Principal
import javax.security.cert.CertificateException
class KeyHelper @Throws(NoSuchPaddingException::class, NoSuchProviderException::class, NoSuchAlgorithmException::class, InvalidAlgorithmParameterException::class, KeyStoreException::class, CertificateException::class, IOException::class)
constructor(ctx: Context, var sharedPreferences: SharedPreferences, var keyStore: KeyStore) {
private val aesKeyFromKS: Key?
@Throws(NoSuchProviderException::class, NoSuchAlgorithmException::class, InvalidAlgorithmParameterException::class, KeyStoreException::class, CertificateException::class, IOException::class, UnrecoverableKeyException::class)
get() {
keyStore.load(null)
return keyStore.getKey(KEY_ALIAS, null) as? SecretKey
}
init {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
this.generateEncryptKey(ctx)
}
this.generateRandomIV(sharedPreferences)
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
try {
this.generateAESKey()
} catch (e: Exception) {
e.printStackTrace()
}
}
}
@Throws(NoSuchProviderException::class, NoSuchAlgorithmException::class, InvalidAlgorithmParameterException::class, KeyStoreException::class, CertificateException::class, IOException::class)
private fun generateEncryptKey(ctx: Context) {
keyStore.load(null)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (!keyStore.containsAlias(KEY_ALIAS)) {
val keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, AndroidKeyStore)
keyGenerator.init(
KeyGenParameterSpec.Builder(KEY_ALIAS,
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.setRandomizedEncryptionRequired(false)
.build())
keyGenerator.generateKey()
}
} else {
if (!keyStore.containsAlias(KEY_ALIAS)) {
// Generate a key pair for encryption
val start = Calendar.getInstance()
val end = Calendar.getInstance()
end.add(Calendar.YEAR, 30)
val spec = KeyPairGeneratorSpec.Builder(ctx)
.setAlias(KEY_ALIAS)
.setSubject(X500Principal("CN=$KEY_ALIAS"))
.setSerialNumber(BigInteger.TEN)
.setStartDate(start.time)
.setEndDate(end.time)
.build()
val kpg = KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_RSA, AndroidKeyStore)
kpg.initialize(spec)
kpg.generateKeyPair()
}
}
}
@Throws(Exception::class)
private fun rsaEncrypt(secret: ByteArray): ByteArray {
val privateKeyEntry = keyStore.getEntry(KEY_ALIAS, null) as? KeyStore.PrivateKeyEntry
// Encrypt the text
val inputCipher = Cipher.getInstance(RSA_MODE, "AndroidOpenSSL")
inputCipher.init(Cipher.ENCRYPT_MODE, privateKeyEntry?.certificate?.publicKey)
val outputStream = ByteArrayOutputStream()
val cipherOutputStream = CipherOutputStream(outputStream, inputCipher)
cipherOutputStream.write(secret)
cipherOutputStream.close()
return outputStream.toByteArray()
}
@Throws(Exception::class)
private fun rsaDecrypt(encrypted: ByteArray): ByteArray {
val privateKeyEntry = keyStore.getEntry(KEY_ALIAS, null) as? KeyStore.PrivateKeyEntry
val output = Cipher.getInstance(RSA_MODE, "AndroidOpenSSL")
output.init(Cipher.DECRYPT_MODE, privateKeyEntry?.privateKey)
val cipherInputStream = CipherInputStream(
ByteArrayInputStream(encrypted), output)
return cipherInputStream.readBytes()
}
@Throws(Exception::class)
private fun generateAESKey() {
var enryptedKeyB64 = sharedPreferences.getString(ENCRYPTED_KEY, null)
if (enryptedKeyB64 == null) {
val key = ByteArray(16)
val secureRandom = SecureRandom()
secureRandom.nextBytes(key)
val encryptedKey = rsaEncrypt(key)
enryptedKeyB64 = Base64.encodeToString(encryptedKey, Base64.DEFAULT)
sharedPreferences.edit {
putString(ENCRYPTED_KEY, enryptedKeyB64)
}
}
}
@Throws(Exception::class)
private fun getSecretKey(): Key {
val enryptedKeyB64 = sharedPreferences.getString(ENCRYPTED_KEY, null)
val encryptedKey = Base64.decode(enryptedKeyB64, Base64.DEFAULT)
val key = rsaDecrypt(encryptedKey)
return SecretKeySpec(key, "AES")
}
@Throws(NoSuchAlgorithmException::class, NoSuchPaddingException::class, NoSuchProviderException::class, BadPaddingException::class, IllegalBlockSizeException::class, UnsupportedEncodingException::class)
fun encrypt(input: String): String {
val c: Cipher
val publicIV = sharedPreferences.getString(PUBLIC_IV, null)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
c = Cipher.getInstance(AES_MODE_M)
try {
c.init(Cipher.ENCRYPT_MODE, aesKeyFromKS, GCMParameterSpec(128, Base64.decode(publicIV, Base64.DEFAULT)))
} catch (e: Exception) {
e.printStackTrace()
}
} else {
c = Cipher.getInstance(AES_MODE_M)
try {
c.init(Cipher.ENCRYPT_MODE, getSecretKey(), GCMParameterSpec(128, Base64.decode(publicIV, Base64.DEFAULT)))
} catch (e: Exception) {
e.printStackTrace()
}
}
val encodedBytes = c.doFinal(input.toByteArray(charset("UTF-8")))
return Base64.encodeToString(encodedBytes, Base64.DEFAULT)
}
@Throws(NoSuchAlgorithmException::class, NoSuchPaddingException::class, NoSuchProviderException::class, BadPaddingException::class, IllegalBlockSizeException::class, UnsupportedEncodingException::class)
fun decrypt(encrypted: String): String {
val c: Cipher
val publicIV = sharedPreferences.getString(PUBLIC_IV, null)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
c = Cipher.getInstance(AES_MODE_M)
try {
c.init(Cipher.DECRYPT_MODE, aesKeyFromKS, GCMParameterSpec(128, Base64.decode(publicIV, Base64.DEFAULT)))
} catch (e: Exception) {
e.printStackTrace()
}
} else {
c = Cipher.getInstance(AES_MODE_M)
try {
c.init(Cipher.DECRYPT_MODE, getSecretKey(), GCMParameterSpec(128, Base64.decode(publicIV, Base64.DEFAULT)))
} catch (e: Exception) {
e.printStackTrace()
}
}
val decodedValue = Base64.decode(encrypted.toByteArray(charset("UTF-8")), Base64.DEFAULT)
val decryptedVal = c.doFinal(decodedValue)
return String(decryptedVal)
}
private fun generateRandomIV(sharedPreferences: SharedPreferences) {
val publicIV = sharedPreferences.getString(PUBLIC_IV, null)
if (publicIV == null) {
val random = SecureRandom()
val generated = random.generateSeed(12)
val generatedIVstr = Base64.encodeToString(generated, Base64.DEFAULT)
sharedPreferences.edit {
putString(PUBLIC_IV, generatedIVstr)
}
}
}
private fun getStringFromSharedPrefs(key: String): String? {
return sharedPreferences.getString(key, null)
}
companion object {
private const val RSA_MODE = "RSA/ECB/PKCS1Padding"
private const val AES_MODE_M = "AES/GCM/NoPadding"
private const val KEY_ALIAS = "KEY"
private const val AndroidKeyStore = "AndroidKeyStore"
const val ENCRYPTED_KEY = "ENCRYPTED_KEY"
const val PUBLIC_IV = "PUBLIC_IV"
private var keyHelper: KeyHelper? = null
fun getInstance(ctx: Context, sharedPreferences: SharedPreferences, keyStore: KeyStore): KeyHelper? {
if (keyHelper == null) {
try {
keyHelper = KeyHelper(ctx, sharedPreferences, keyStore)
} catch (e: NoSuchPaddingException) {
e.printStackTrace()
} catch (e: NoSuchProviderException) {
e.printStackTrace()
} catch (e: NoSuchAlgorithmException) {
e.printStackTrace()
} catch (e: InvalidAlgorithmParameterException) {
e.printStackTrace()
} catch (e: KeyStoreException) {
e.printStackTrace()
} catch (e: CertificateException) {
e.printStackTrace()
} catch (e: IOException) {
e.printStackTrace()
}
}
return keyHelper
}
}
}

View file

@ -12,7 +12,7 @@ import com.habitrpg.android.habitica.helpers.SoundManager
import com.habitrpg.android.habitica.models.user.Stats
import com.habitrpg.android.habitica.models.user.User
import com.habitrpg.android.habitica.ui.AvatarView
import com.habitrpg.android.habitica.ui.views.HabiticaAlertDialog
import com.habitrpg.android.habitica.ui.views.dialogs.HabiticaAlertDialog
import com.habitrpg.android.habitica.ui.views.HabiticaIconsHelper
import io.reactivex.Flowable
import io.reactivex.functions.Consumer

View file

@ -7,9 +7,11 @@ import com.habitrpg.android.habitica.api.HostConfig;
import com.habitrpg.android.habitica.api.MaintenanceApiService;
import com.habitrpg.android.habitica.data.ApiClient;
import com.habitrpg.android.habitica.data.implementation.ApiClientImpl;
import com.habitrpg.android.habitica.helpers.KeyHelper;
import com.habitrpg.android.habitica.helpers.PopupNotificationsManager;
import com.habitrpg.android.habitica.proxy.CrashlyticsProxy;
import javax.annotation.Nullable;
import javax.inject.Singleton;
import dagger.Module;
@ -23,8 +25,8 @@ public class ApiModule {
@Provides
@Singleton
public HostConfig providesHostConfig(SharedPreferences sharedPreferences, Context context) {
return new HostConfig(sharedPreferences, context);
public HostConfig providesHostConfig(SharedPreferences sharedPreferences, @Nullable KeyHelper keyHelper, Context context) {
return new HostConfig(sharedPreferences, keyHelper, context);
}
@Provides

View file

@ -4,6 +4,7 @@ import android.app.Application;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.res.Resources;
import androidx.preference.PreferenceManager;
import com.habitrpg.android.habitica.R;
@ -14,12 +15,17 @@ import com.habitrpg.android.habitica.executors.PostExecutionThread;
import com.habitrpg.android.habitica.executors.ThreadExecutor;
import com.habitrpg.android.habitica.executors.UIThread;
import com.habitrpg.android.habitica.helpers.AppConfigManager;
import com.habitrpg.android.habitica.helpers.KeyHelper;
import com.habitrpg.android.habitica.helpers.SoundFileLoader;
import com.habitrpg.android.habitica.helpers.SoundManager;
import com.habitrpg.android.habitica.helpers.TaskAlarmManager;
import com.habitrpg.android.habitica.helpers.TaskFilterHelper;
import com.habitrpg.android.habitica.helpers.notifications.PushNotificationManager;
import java.security.KeyStore;
import java.security.KeyStoreException;
import javax.annotation.Nullable;
import javax.inject.Named;
import javax.inject.Singleton;
@ -49,6 +55,26 @@ public class AppModule {
return PreferenceManager.getDefaultSharedPreferences(context);
}
@Provides
@Nullable
KeyStore provideKeyStore() {
try {
return KeyStore.getInstance("AndroidKeyStore");
} catch (KeyStoreException e) {
e.printStackTrace();
}
return null;
}
@Provides
@Nullable
KeyHelper provideKeyHelper(Context context, SharedPreferences sharedPreferences, @Nullable KeyStore keyStore) {
if (keyStore == null) {
return null;
}
return KeyHelper.Companion.getInstance(context, sharedPreferences, keyStore);
}
@Provides
@Named(NAMED_USER_ID)
public String providesUserID(SharedPreferences sharedPreferences) {

View file

@ -28,7 +28,7 @@ import android.util.Log;
import androidx.fragment.app.Fragment;
import com.habitrpg.android.habitica.ui.views.HabiticaAlertDialog;
import com.habitrpg.android.habitica.ui.views.dialogs.HabiticaAlertDialog;
import java.util.Arrays;
import java.util.Collection;

View file

@ -11,7 +11,7 @@ import com.habitrpg.android.habitica.HabiticaBaseApplication
import com.habitrpg.android.habitica.components.AppComponent
import com.habitrpg.android.habitica.events.ShowConnectionProblemEvent
import com.habitrpg.android.habitica.helpers.LanguageHelper
import com.habitrpg.android.habitica.ui.views.HabiticaAlertDialog
import com.habitrpg.android.habitica.ui.views.dialogs.HabiticaAlertDialog
import io.reactivex.disposables.CompositeDisposable
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe

View file

@ -27,7 +27,7 @@ import com.habitrpg.android.habitica.models.user.User
import com.habitrpg.android.habitica.modules.AppModule
import com.habitrpg.android.habitica.ui.adapter.social.challenges.ChallengeTasksRecyclerViewAdapter
import com.habitrpg.android.habitica.ui.helpers.bindView
import com.habitrpg.android.habitica.ui.views.HabiticaAlertDialog
import com.habitrpg.android.habitica.ui.views.dialogs.HabiticaAlertDialog
import com.habitrpg.android.habitica.ui.views.HabiticaIconsHelper
import io.reactivex.Flowable
import io.reactivex.functions.Consumer

View file

@ -20,7 +20,7 @@ import com.habitrpg.android.habitica.helpers.RxErrorHandler
import com.habitrpg.android.habitica.models.user.*
import com.habitrpg.android.habitica.ui.AvatarView
import com.habitrpg.android.habitica.ui.helpers.bindView
import com.habitrpg.android.habitica.ui.views.HabiticaAlertDialog
import com.habitrpg.android.habitica.ui.views.dialogs.HabiticaAlertDialog
import com.habitrpg.android.habitica.ui.views.HabiticaIconsHelper
import io.reactivex.functions.Consumer
import javax.inject.Inject

View file

@ -32,7 +32,7 @@ import com.habitrpg.android.habitica.ui.helpers.DataBindingUtils
import com.habitrpg.android.habitica.ui.helpers.MarkdownParser
import com.habitrpg.android.habitica.ui.helpers.bindView
import com.habitrpg.android.habitica.ui.helpers.dismissKeyboard
import com.habitrpg.android.habitica.ui.views.HabiticaAlertDialog
import com.habitrpg.android.habitica.ui.views.dialogs.HabiticaAlertDialog
import com.habitrpg.android.habitica.ui.views.HabiticaSnackbar
import com.habitrpg.android.habitica.ui.views.HabiticaSnackbar.SnackbarDisplayType
import io.reactivex.Flowable

View file

@ -24,7 +24,7 @@ import com.habitrpg.android.habitica.proxy.CrashlyticsProxy
import com.habitrpg.android.habitica.ui.AvatarView
import com.habitrpg.android.habitica.ui.helpers.bindOptionalView
import com.habitrpg.android.habitica.ui.helpers.bindView
import com.habitrpg.android.habitica.ui.views.HabiticaAlertDialog
import com.habitrpg.android.habitica.ui.views.dialogs.HabiticaAlertDialog
import com.habitrpg.android.habitica.ui.views.social.UsernameLabel
import com.habitrpg.android.habitica.ui.views.subscriptions.SubscriptionOptionView
import io.reactivex.functions.Consumer

View file

@ -44,12 +44,13 @@ import com.habitrpg.android.habitica.extensions.addCloseButton
import com.habitrpg.android.habitica.extensions.addOkButton
import com.habitrpg.android.habitica.extensions.notNull
import com.habitrpg.android.habitica.helpers.AmplitudeManager
import com.habitrpg.android.habitica.helpers.KeyHelper
import com.habitrpg.android.habitica.helpers.RxErrorHandler
import com.habitrpg.android.habitica.models.auth.UserAuthResponse
import com.habitrpg.android.habitica.prefs.scanner.IntentIntegrator
import com.habitrpg.android.habitica.ui.helpers.bindView
import com.habitrpg.android.habitica.ui.helpers.dismissKeyboard
import com.habitrpg.android.habitica.ui.views.HabiticaAlertDialog
import com.habitrpg.android.habitica.ui.views.dialogs.HabiticaAlertDialog
import com.habitrpg.android.habitica.ui.views.login.LockableScrollView
import com.habitrpg.android.habitica.ui.views.login.LoginBackgroundView
import io.reactivex.Flowable
@ -75,6 +76,9 @@ class LoginActivity : BaseActivity(), Consumer<UserAuthResponse> {
lateinit var hostConfig: HostConfig
@Inject
internal lateinit var userRepository: UserRepository
@Inject
@JvmField
var keyHelper: KeyHelper? = null
private var isRegistering: Boolean = false
private var isShowingForm: Boolean = false
@ -302,7 +306,11 @@ class LoginActivity : BaseActivity(), Consumer<UserAuthResponse> {
val prefs = PreferenceManager.getDefaultSharedPreferences(this)
prefs.edit {
putString(getString(R.string.SP_address), adr)
putString(getString(R.string.SP_APIToken), key)
if (keyHelper != null) {
putString(user, keyHelper?.encrypt(key))
} else {
putString(getString(R.string.SP_APIToken), key)
}
putString(getString(R.string.SP_userID), user)
}
startMainActivity()
@ -337,7 +345,11 @@ class LoginActivity : BaseActivity(), Consumer<UserAuthResponse> {
private fun saveTokens(api: String, user: String) {
this.apiClient.updateAuthenticationCredentials(user, api)
sharedPrefs.edit {
putString(getString(R.string.SP_APIToken), api)
if (keyHelper != null) {
putString(user, keyHelper?.encrypt(api))
} else {
putString(getString(R.string.SP_APIToken), api)
}
putString(getString(R.string.SP_userID), user)
}
}

View file

@ -58,7 +58,7 @@ import com.habitrpg.android.habitica.ui.helpers.DataBindingUtils
import com.habitrpg.android.habitica.ui.helpers.KeyboardUtil
import com.habitrpg.android.habitica.ui.helpers.bindOptionalView
import com.habitrpg.android.habitica.ui.helpers.bindView
import com.habitrpg.android.habitica.ui.views.HabiticaAlertDialog
import com.habitrpg.android.habitica.ui.views.dialogs.HabiticaAlertDialog
import com.habitrpg.android.habitica.ui.views.HabiticaIconsHelper
import com.habitrpg.android.habitica.ui.views.HabiticaSnackbar
import com.habitrpg.android.habitica.ui.views.HabiticaSnackbar.SnackbarDisplayType
@ -206,7 +206,7 @@ open class MainActivity : BaseActivity(), TutorialView.OnTutorialReaction {
val px = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 16f, resources.displayMetrics)
avatarWithBars.setPadding(px.toInt(), statusBarHeight, px.toInt(), 0)
compositeSubscription.add(userRepository.getUser(hostConfig.user)
compositeSubscription.add(userRepository.getUser(hostConfig.userID)
.subscribe(Consumer { newUser ->
this@MainActivity.user = newUser
this@MainActivity.setUserData()

View file

@ -75,7 +75,7 @@ class SetupActivity : BaseActivity(), ViewPager.OnPageChangeListener {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
compositeSubscription.add(userRepository.getUser(hostConfig.user)
compositeSubscription.add(userRepository.getUser(hostConfig.userID)
.subscribe(Consumer { this.onUserReceived(it) }, RxErrorHandler.handleEmptyError()))
val additionalData = HashMap<String, Any>()

View file

@ -34,7 +34,7 @@ import com.habitrpg.android.habitica.models.tasks.HabitResetOption
import com.habitrpg.android.habitica.models.tasks.Task
import com.habitrpg.android.habitica.models.user.Stats
import com.habitrpg.android.habitica.ui.helpers.bindView
import com.habitrpg.android.habitica.ui.views.HabiticaAlertDialog
import com.habitrpg.android.habitica.ui.views.dialogs.HabiticaAlertDialog
import com.habitrpg.android.habitica.ui.views.tasks.form.*
import io.reactivex.functions.Consumer
import io.realm.RealmList

View file

@ -12,6 +12,7 @@ import com.habitrpg.android.habitica.models.QuestAchievement
import com.habitrpg.android.habitica.ui.helpers.DataBindingUtils
import com.habitrpg.android.habitica.ui.helpers.bindOptionalView
import com.habitrpg.android.habitica.ui.helpers.bindView
import com.habitrpg.android.habitica.ui.views.dialogs.AchievementDetailDialog
class AchievementsAdapter: RecyclerView.Adapter<RecyclerView.ViewHolder>() {
@ -71,7 +72,7 @@ class AchievementsAdapter: RecyclerView.Adapter<RecyclerView.ViewHolder>() {
}
}
class AchievementViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) {
class AchievementViewHolder(itemView: View): RecyclerView.ViewHolder(itemView), View.OnClickListener {
private var achievement: Achievement? = null
private val achievementContainer: ViewGroup? by bindOptionalView(R.id.achievement_container)
@ -80,6 +81,10 @@ class AchievementsAdapter: RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private val achievementTitleView: TextView by bindView(R.id.achievement_title)
private val achievementDescriptionView: TextView? by bindOptionalView(R.id.achievement_description)
init {
itemView.setOnClickListener(this)
}
fun bind(achievement: Achievement) {
this.achievement = achievement
val iconName = if (achievement.earned) {
@ -98,6 +103,12 @@ class AchievementsAdapter: RecyclerView.Adapter<RecyclerView.ViewHolder>() {
}
achievementContainer?.clipToOutline = true
}
override fun onClick(v: View?) {
achievement?.let {
AchievementDetailDialog(it, itemView.context).show()
}
}
}
class QuestAchievementViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) {

View file

@ -14,7 +14,7 @@ import com.habitrpg.android.habitica.models.inventory.Customization
import com.habitrpg.android.habitica.models.inventory.CustomizationSet
import com.habitrpg.android.habitica.ui.helpers.DataBindingUtils
import com.habitrpg.android.habitica.ui.helpers.bindView
import com.habitrpg.android.habitica.ui.views.HabiticaAlertDialog
import com.habitrpg.android.habitica.ui.views.dialogs.HabiticaAlertDialog
import com.habitrpg.android.habitica.ui.views.HabiticaIconsHelper
import io.reactivex.BackpressureStrategy
import io.reactivex.Flowable

View file

@ -18,7 +18,7 @@ import com.habitrpg.android.habitica.ui.AvatarView
import com.habitrpg.android.habitica.ui.activities.MainActivity
import com.habitrpg.android.habitica.ui.helpers.bindView
import com.habitrpg.android.habitica.ui.viewHolders.SectionViewHolder
import com.habitrpg.android.habitica.ui.views.HabiticaAlertDialog
import com.habitrpg.android.habitica.ui.views.dialogs.HabiticaAlertDialog
class AchievementProfileAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {

View file

@ -30,7 +30,7 @@ class AchievementsFragment: BaseMainFragment(), SwipeRefreshLayout.OnRefreshList
private var menuID: Int = 0
private lateinit var adapter: AchievementsAdapter
private val layoutManager = GridLayoutManager(activity, 2)
private var useGridLayout = true
private var useGridLayout = false
set(value) {
field = value
adapter.useGridLayout = value
@ -48,6 +48,9 @@ class AchievementsFragment: BaseMainFragment(), SwipeRefreshLayout.OnRefreshList
hidesToolbar = true
super.onCreateView(inflater, container, savedInstanceState)
adapter = AchievementsAdapter()
onRefresh()
return inflater.inflate(R.layout.fragment_refresh_recyclerview, container, false)
}

View file

@ -18,7 +18,7 @@ import com.habitrpg.android.habitica.helpers.UserStatComputer
import com.habitrpg.android.habitica.models.user.Stats
import com.habitrpg.android.habitica.models.user.User
import com.habitrpg.android.habitica.modules.AppModule
import com.habitrpg.android.habitica.ui.views.HabiticaAlertDialog
import com.habitrpg.android.habitica.ui.views.dialogs.HabiticaAlertDialog
import com.habitrpg.android.habitica.ui.views.HabiticaIconsHelper
import com.habitrpg.android.habitica.ui.views.stats.BulkAllocateStatsDialog
import io.reactivex.functions.Consumer

View file

@ -25,7 +25,7 @@ import com.habitrpg.android.habitica.ui.activities.GiftIAPActivity
import com.habitrpg.android.habitica.ui.helpers.bindOptionalView
import com.habitrpg.android.habitica.ui.helpers.bindView
import com.habitrpg.android.habitica.ui.helpers.dismissKeyboard
import com.habitrpg.android.habitica.ui.views.HabiticaAlertDialog
import com.habitrpg.android.habitica.ui.views.dialogs.HabiticaAlertDialog
import com.habitrpg.android.habitica.ui.views.HabiticaIconsHelper
import com.habitrpg.android.habitica.ui.views.subscriptions.SubscriptionDetailsView
import com.habitrpg.android.habitica.ui.views.subscriptions.SubscriptionOptionView

View file

@ -20,7 +20,7 @@ import com.habitrpg.android.habitica.modules.AppModule
import com.habitrpg.android.habitica.ui.AvatarView
import com.habitrpg.android.habitica.ui.fragments.BaseMainFragment
import com.habitrpg.android.habitica.ui.helpers.dismissKeyboard
import com.habitrpg.android.habitica.ui.views.HabiticaAlertDialog
import com.habitrpg.android.habitica.ui.views.dialogs.HabiticaAlertDialog
import com.habitrpg.android.habitica.ui.views.social.UsernameLabel
import io.reactivex.functions.Consumer
import io.realm.RealmResults

View file

@ -27,7 +27,7 @@ import com.habitrpg.android.habitica.ui.helpers.DataBindingUtils
import com.habitrpg.android.habitica.ui.helpers.MarkdownParser
import com.habitrpg.android.habitica.ui.helpers.bindOptionalView
import com.habitrpg.android.habitica.ui.helpers.resetViews
import com.habitrpg.android.habitica.ui.views.HabiticaAlertDialog
import com.habitrpg.android.habitica.ui.views.dialogs.HabiticaAlertDialog
import io.reactivex.functions.Consumer
import javax.inject.Inject
import javax.inject.Named

View file

@ -26,7 +26,7 @@ import com.habitrpg.android.habitica.models.social.Group
import com.habitrpg.android.habitica.models.user.User
import com.habitrpg.android.habitica.modules.AppModule
import com.habitrpg.android.habitica.ui.fragments.BaseFragment
import com.habitrpg.android.habitica.ui.views.HabiticaAlertDialog
import com.habitrpg.android.habitica.ui.views.dialogs.HabiticaAlertDialog
import com.habitrpg.android.habitica.ui.views.social.UsernameLabel
import io.reactivex.functions.Consumer
import kotlinx.android.synthetic.main.fragment_tavern_detail.*

View file

@ -29,7 +29,7 @@ import com.habitrpg.android.habitica.ui.activities.FullProfileActivity
import com.habitrpg.android.habitica.ui.fragments.BaseMainFragment
import com.habitrpg.android.habitica.ui.helpers.MarkdownParser
import com.habitrpg.android.habitica.ui.helpers.bindView
import com.habitrpg.android.habitica.ui.views.HabiticaAlertDialog
import com.habitrpg.android.habitica.ui.views.dialogs.HabiticaAlertDialog
import com.habitrpg.android.habitica.ui.views.HabiticaEmojiTextView
import com.habitrpg.android.habitica.ui.views.HabiticaIconsHelper
import com.habitrpg.android.habitica.ui.views.social.UsernameLabel

View file

@ -23,7 +23,7 @@ import com.habitrpg.android.habitica.ui.fragments.BaseFragment
import com.habitrpg.android.habitica.ui.fragments.inventory.items.ItemRecyclerFragment
import com.habitrpg.android.habitica.ui.helpers.*
import com.habitrpg.android.habitica.ui.viewmodels.PartyViewModel
import com.habitrpg.android.habitica.ui.views.HabiticaAlertDialog
import com.habitrpg.android.habitica.ui.views.dialogs.HabiticaAlertDialog
import com.habitrpg.android.habitica.ui.views.social.OldQuestProgressView
import io.reactivex.functions.Consumer
import javax.inject.Inject

View file

@ -22,7 +22,7 @@ import com.habitrpg.android.habitica.ui.helpers.bindView
import com.habitrpg.android.habitica.ui.helpers.resetViews
import com.habitrpg.android.habitica.ui.viewmodels.GroupViewType
import com.habitrpg.android.habitica.ui.viewmodels.PartyViewModel
import com.habitrpg.android.habitica.ui.views.HabiticaAlertDialog
import com.habitrpg.android.habitica.ui.views.dialogs.HabiticaAlertDialog
import io.reactivex.functions.Consumer
import java.util.*

View file

@ -0,0 +1,33 @@
package com.habitrpg.android.habitica.ui.views.dialogs
import android.content.Context
import android.view.LayoutInflater
import android.widget.TextView
import com.facebook.drawee.view.SimpleDraweeView
import com.habitrpg.android.habitica.R
import com.habitrpg.android.habitica.extensions.addCloseButton
import com.habitrpg.android.habitica.models.Achievement
import com.habitrpg.android.habitica.ui.helpers.DataBindingUtils
class AchievementDetailDialog(val achievement: Achievement, context: Context): HabiticaAlertDialog(context) {
private var iconView: SimpleDraweeView?
private var descriptionView: TextView?
init {
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as? LayoutInflater
val view = inflater?.inflate(R.layout.dialog_achievement_detail, null)
iconView = view?.findViewById(R.id.icon_view)
descriptionView = view?.findViewById(R.id.description_view)
setAdditionalContentView(view)
setTitle(achievement.title)
descriptionView?.text = achievement.text
val iconName = if (achievement.earned) {
achievement.icon + "2x"
} else {
"achievement-unearned2x"
}
DataBindingUtils.loadImage(iconView, iconName)
addCloseButton(true)
}
}

View file

@ -1,4 +1,4 @@
package com.habitrpg.android.habitica.ui.views
package com.habitrpg.android.habitica.ui.views.dialogs
import android.content.Context
import android.view.LayoutInflater

View file

@ -5,7 +5,7 @@ import android.view.LayoutInflater
import android.widget.ImageView
import android.widget.TextView
import com.habitrpg.android.habitica.R
import com.habitrpg.android.habitica.ui.views.HabiticaAlertDialog
import com.habitrpg.android.habitica.ui.views.dialogs.HabiticaAlertDialog
/**
* Created by phillip on 27.09.17.

View file

@ -25,6 +25,7 @@ import com.habitrpg.android.habitica.models.shops.ShopItem
import com.habitrpg.android.habitica.models.user.User
import com.habitrpg.android.habitica.ui.helpers.bindView
import com.habitrpg.android.habitica.ui.views.*
import com.habitrpg.android.habitica.ui.views.dialogs.HabiticaAlertDialog
import com.habitrpg.android.habitica.ui.views.insufficientCurrency.InsufficientGemsDialog
import com.habitrpg.android.habitica.ui.views.insufficientCurrency.InsufficientGoldDialog
import com.habitrpg.android.habitica.ui.views.insufficientCurrency.InsufficientHourglassesDialog

View file

@ -28,6 +28,7 @@ import com.habitrpg.android.habitica.ui.helpers.DataBindingUtils
import com.habitrpg.android.habitica.ui.helpers.MarkdownParser
import com.habitrpg.android.habitica.ui.helpers.bindView
import com.habitrpg.android.habitica.ui.views.*
import com.habitrpg.android.habitica.ui.views.dialogs.HabiticaAlertDialog
import io.reactivex.Observable
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.functions.Consumer

View file

@ -18,8 +18,8 @@ import com.habitrpg.android.habitica.helpers.RxErrorHandler
import com.habitrpg.android.habitica.models.tasks.ChecklistItem
import com.habitrpg.android.habitica.models.tasks.Task
import com.habitrpg.android.habitica.ui.helpers.bindColor
import com.habitrpg.android.habitica.ui.views.HabiticaAlertDialog
import com.habitrpg.android.habitica.ui.views.HabiticaEmojiTextView
import com.habitrpg.android.habitica.ui.views.dialogs.HabiticaAlertDialog
import io.reactivex.Observable
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.functions.Consumer