diff --git a/Habitica/build.gradle b/Habitica/build.gradle index f6551fab6..e3dadc526 100644 --- a/Habitica/build.gradle +++ b/Habitica/build.gradle @@ -50,9 +50,8 @@ dependencies { implementation 'com.google.dagger:dagger:2.39.1' kapt 'com.google.dagger:dagger-compiler:2.39.1' compileOnly 'javax.annotation:javax.annotation-api:1.3.2' - compileOnly 'com.github.pengrad:jdk9-deps:1.0' //App Compatibility and Material Design - implementation 'androidx.appcompat:appcompat:1.3.1' + implementation 'androidx.appcompat:appcompat:1.4.0' implementation 'com.google.android.material:material:1.4.0' implementation 'androidx.recyclerview:recyclerview:1.2.1' implementation "androidx.preference:preference-ktx:1.1.1" @@ -103,14 +102,13 @@ dependencies { //Leak Detection debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.7' //Push Notifications - implementation platform('com.google.firebase:firebase-bom:28.3.0') - implementation 'com.google.firebase:firebase-crashlytics' + implementation platform('com.google.firebase:firebase-bom:29.0.0') + implementation 'com.google.firebase:firebase-crashlytics-ktx' implementation 'com.google.firebase:firebase-core' - implementation 'com.google.firebase:firebase-messaging' - implementation 'com.google.firebase:firebase-config' - implementation 'com.google.firebase:firebase-perf' + implementation 'com.google.firebase:firebase-messaging-ktx' + implementation 'com.google.firebase:firebase-config-ktx' + implementation 'com.google.firebase:firebase-perf-ktx' implementation 'com.google.android.gms:play-services-auth:19.2.0' - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.5.31" implementation 'com.nex3z:flow-layout:1.2.2' implementation 'androidx.core:core-ktx:1.7.0' @@ -119,9 +117,9 @@ dependencies { implementation "androidx.lifecycle:lifecycle-common-java8:2.4.0" implementation 'androidx.navigation:navigation-fragment-ktx:2.3.5' implementation 'androidx.navigation:navigation-ui-ktx:2.3.5' - implementation "androidx.fragment:fragment-ktx:1.3.6" - implementation "androidx.paging:paging-runtime-ktx:3.0.1" implementation 'com.plattysoft.leonids:LeonidsLib:1.3.2' + implementation "androidx.fragment:fragment-ktx:1.4.0" + implementation "androidx.paging:paging-runtime-ktx:3.1.0" implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2' @@ -153,8 +151,8 @@ android { buildConfigField "String", "TESTING_LEVEL", "\"production\"" resConfigs "en", "bg", "de", "en-rGB", "es", "fr", "hr-rHR", "in", "it", "iw", "ja", "ko", "lt", "nl", "pl", "pt-rBR", "pt-rPT", "ru", "tr", "zh", "zh-rTW" - versionCode 3093 - versionName "3.4.1.1" + versionCode 3102 + versionName "3.4.2" targetSdkVersion 31 diff --git a/Habitica/res/layout/dialog_edittext_add_local_auth.xml b/Habitica/res/layout/dialog_edittext_add_local_auth.xml index ee1b6e33d..0b71217b3 100644 --- a/Habitica/res/layout/dialog_edittext_add_local_auth.xml +++ b/Habitica/res/layout/dialog_edittext_add_local_auth.xml @@ -1,55 +1,28 @@ - + - - - + - - - + - - + android:inputType="textPassword" + app:hint="@string/new_password_repeat" + android:layout_marginTop="@dimen/content_section_spacing" /> \ No newline at end of file diff --git a/Habitica/res/layout/dialog_edittext_change_pw.xml b/Habitica/res/layout/dialog_edittext_change_pw.xml index 4a15a7ac5..12a142026 100644 --- a/Habitica/res/layout/dialog_edittext_change_pw.xml +++ b/Habitica/res/layout/dialog_edittext_change_pw.xml @@ -1,43 +1,27 @@ - - - - + - - - + - - + android:inputType="textPassword" + app:hint="@string/new_password_repeat" + android:layout_marginTop="@dimen/content_section_spacing" /> \ No newline at end of file diff --git a/Habitica/res/layout/dialog_edittext_confirm_pw.xml b/Habitica/res/layout/dialog_edittext_confirm_pw.xml index dc9d6c843..a74c31d89 100644 --- a/Habitica/res/layout/dialog_edittext_confirm_pw.xml +++ b/Habitica/res/layout/dialog_edittext_confirm_pw.xml @@ -1,32 +1,20 @@ - - - - + - - + android:inputType="textPassword" + app:hint="@string/password" + android:layout_marginTop="@dimen/content_section_spacing" /> \ No newline at end of file diff --git a/Habitica/res/layout/preference_child_summary_danger.xml b/Habitica/res/layout/preference_child_summary_danger.xml new file mode 100644 index 000000000..7f8cba022 --- /dev/null +++ b/Habitica/res/layout/preference_child_summary_danger.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/Habitica/res/layout/preference_child_summary_error.xml b/Habitica/res/layout/preference_child_summary_error.xml index 1fb6acc58..5f29f5888 100644 --- a/Habitica/res/layout/preference_child_summary_error.xml +++ b/Habitica/res/layout/preference_child_summary_error.xml @@ -38,6 +38,6 @@ android:layout_below="@android:id/title" android:maxLines="4" android:textAppearance="?android:attr/textAppearanceSmall" - android:textColor="@color/red_10" /> + android:textColor="@color/text_red" /> \ No newline at end of file diff --git a/Habitica/res/layout/validating_edit_text.xml b/Habitica/res/layout/validating_edit_text.xml new file mode 100644 index 000000000..b08fcc8cf --- /dev/null +++ b/Habitica/res/layout/validating_edit_text.xml @@ -0,0 +1,31 @@ + + + + + + + \ No newline at end of file diff --git a/Habitica/res/values-night/colors.xml b/Habitica/res/values-night/colors.xml index cabdf4adc..d2adefd8b 100644 --- a/Habitica/res/values-night/colors.xml +++ b/Habitica/res/values-night/colors.xml @@ -10,7 +10,7 @@ @color/gray_10 @color/brand_400 @color/brand_500 - @color/red_10 + @color/red_100 @color/orange_100 @color/yellow_100 @color/green_100 diff --git a/Habitica/res/values-ru/strings.xml b/Habitica/res/values-ru/strings.xml index 481ca6593..e079882f7 100644 --- a/Habitica/res/values-ru/strings.xml +++ b/Habitica/res/values-ru/strings.xml @@ -1147,7 +1147,7 @@ Вы уверены, что хотите покинуть команду\? Жалуйтесь ** только ** на те публикации, которые нарушают [Принципы сообщества] (https://habitica.com/static/community-guidelines) и / или [Условия предоставления услуг] (https://habitica.com/static/terms ). Ложное сообщение о нарушении может привести к наказанию. Выберите ниже набор самоцветов, который хотите подарить! - Доступно для %@ + Доступно для %s Обновить содержимое Вы не сможете снова присоединиться к этой команде, пока вас не пригласят. Вы хотите продолжить участвовать в испытании, покинув команду\? diff --git a/Habitica/res/values/attrs.xml b/Habitica/res/values/attrs.xml index ad92dd442..e09e02790 100644 --- a/Habitica/res/values/attrs.xml +++ b/Habitica/res/values/attrs.xml @@ -147,4 +147,8 @@ + + + + diff --git a/Habitica/res/values/colors.xml b/Habitica/res/values/colors.xml index cbaae2796..56665d057 100644 --- a/Habitica/res/values/colors.xml +++ b/Habitica/res/values/colors.xml @@ -161,7 +161,7 @@ @color/gray_700 @color/brand_300 @color/brand_400 - @color/red_10 + @color/maroon_100 @color/orange_10 @color/yellow_10 @color/green_10 diff --git a/Habitica/res/values/dimens.xml b/Habitica/res/values/dimens.xml index 23b956352..68a3250b4 100644 --- a/Habitica/res/values/dimens.xml +++ b/Habitica/res/values/dimens.xml @@ -53,7 +53,7 @@ 16sp 14sp 2dp - 84dp + 88dp 120dp 20dp 68dp diff --git a/Habitica/res/values/strings.xml b/Habitica/res/values/strings.xml index 26103d02d..adb4f95f7 100644 --- a/Habitica/res/values/strings.xml +++ b/Habitica/res/values/strings.xml @@ -1196,4 +1196,12 @@ Connect Disconnect Add + Copy Token. Be careful, this is a password! + Added %s authentication + Copied %s to clipboard + Disconnected %s + Password saved + Add Email and Password + Password needs to be typed correctly twice + Invalid Email address diff --git a/Habitica/res/xml/preferences_fragment.xml b/Habitica/res/xml/preferences_fragment.xml index aed0579de..ceadf3bb3 100644 --- a/Habitica/res/xml/preferences_fragment.xml +++ b/Habitica/res/xml/preferences_fragment.xml @@ -63,23 +63,20 @@ android:title="@string/public_profile" android:key="public_profile" android:layout="@layout/preference_category"> - - - + + android:layout="@layout/preference_child_summary_danger" /> + android:layout="@layout/preference_child_summary_danger" /> @@ -122,13 +121,9 @@ tools:title="Change Class" android:layout="@layout/preference_child_summary" app:isPreferenceVisible="false"/> - + android:layout="@layout/preference_child_summary_danger"/> diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/api/GSonFactoryCreator.java b/Habitica/src/main/java/com/habitrpg/android/habitica/api/GSonFactoryCreator.java index 68871196b..8d7a7649a 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/api/GSonFactoryCreator.java +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/api/GSonFactoryCreator.java @@ -28,6 +28,7 @@ import com.habitrpg.android.habitica.models.user.OwnedMount; import com.habitrpg.android.habitica.models.user.OwnedPet; import com.habitrpg.android.habitica.models.user.Purchases; import com.habitrpg.android.habitica.models.user.User; +import com.habitrpg.android.habitica.models.user.auth.SocialAuthentication; import com.habitrpg.android.habitica.utils.AchievementListDeserializer; import com.habitrpg.android.habitica.utils.BooleanAsIntAdapter; import com.habitrpg.android.habitica.utils.ChallengeDeserializer; @@ -51,6 +52,7 @@ import com.habitrpg.android.habitica.utils.QuestCollectDeserializer; import com.habitrpg.android.habitica.utils.QuestDeserializer; import com.habitrpg.android.habitica.utils.QuestDropItemsListSerialization; import com.habitrpg.android.habitica.utils.SkillDeserializer; +import com.habitrpg.android.habitica.utils.SocialAuthenticationDeserializer; import com.habitrpg.android.habitica.utils.TaskListDeserializer; import com.habitrpg.android.habitica.utils.TaskSerializer; import com.habitrpg.android.habitica.utils.TaskTagDeserializer; @@ -84,7 +86,6 @@ public class GSonFactoryCreator { Type ownedMountListType = new TypeToken>() {}.getType(); Type achievementsListType = new TypeToken>() {}.getType(); - Gson gson = new GsonBuilder() .registerTypeAdapter(taskTagClassListType, new TaskTagDeserializer()) .registerTypeAdapter(Boolean.class, new BooleanAsIntAdapter()) @@ -117,6 +118,7 @@ public class GSonFactoryCreator { .registerTypeAdapter(WorldState.class, new WorldStateSerialization()) .registerTypeAdapter(FindUsernameResult.class, new FindUsernameResultDeserializer()) .registerTypeAdapter(Notification.class, new NotificationDeserializer()) + .registerTypeAdapter(SocialAuthentication.class, new SocialAuthenticationDeserializer()) .setDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") .create(); return GsonConverterFactory.create(gson); diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/events/ShowSnackbarEvent.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/events/ShowSnackbarEvent.kt index 6f9a15ab7..cf1b44af8 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/events/ShowSnackbarEvent.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/events/ShowSnackbarEvent.kt @@ -3,16 +3,23 @@ package com.habitrpg.android.habitica.events import android.graphics.drawable.Drawable import android.view.View import com.habitrpg.android.habitica.ui.views.HabiticaSnackbar.SnackbarDisplayType +import org.greenrobot.eventbus.EventBus /** * Created by phillip on 26.06.17. */ class ShowSnackbarEvent { - constructor(title: String, type: SnackbarDisplayType) { + constructor(title: String?, type: SnackbarDisplayType) { this.title = title this.type = type } + constructor(title: String?, text: String?, type: SnackbarDisplayType) { + this.title = title + this.text = text + this.type = type + } + constructor() var leftImage: Drawable? = null @@ -23,4 +30,8 @@ class ShowSnackbarEvent { var rightIcon: Drawable? = null var rightTextColor = 0 var rightText: String? = null + + fun post() { + EventBus.getDefault().post(this) + } } diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/extensions/PendingIntent-Extensions.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/extensions/PendingIntent-Extensions.kt new file mode 100644 index 000000000..b0840558d --- /dev/null +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/extensions/PendingIntent-Extensions.kt @@ -0,0 +1,13 @@ +package com.habitrpg.android.habitica.extensions + +import android.app.PendingIntent +import android.content.Context +import android.os.Build + +fun withImmutableFlag(flags: Int): Int { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + flags + PendingIntent.FLAG_IMMUTABLE + } else { + flags + } +} \ No newline at end of file diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/TaskAlarmManager.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/TaskAlarmManager.kt index 017bd9563..07b5fd08f 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/TaskAlarmManager.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/TaskAlarmManager.kt @@ -7,6 +7,7 @@ import android.content.Intent import android.os.Build import androidx.preference.PreferenceManager import com.habitrpg.android.habitica.data.TaskRepository +import com.habitrpg.android.habitica.extensions.withImmutableFlag import com.habitrpg.android.habitica.models.tasks.RemindersItem import com.habitrpg.android.habitica.models.tasks.Task import com.habitrpg.android.habitica.receivers.NotificationPublisher @@ -93,13 +94,13 @@ class TaskAlarmManager(private var context: Context, private var taskRepository: val intentId = remindersItem.id?.hashCode() ?: 0 and 0xfffffff // Cancel alarm if already exists - val previousSender = PendingIntent.getBroadcast(context, intentId, intent, PendingIntent.FLAG_NO_CREATE) + val previousSender = PendingIntent.getBroadcast(context, intentId, intent, withImmutableFlag(PendingIntent.FLAG_NO_CREATE)) if (previousSender != null) { previousSender.cancel() am?.cancel(previousSender) } - val sender = PendingIntent.getBroadcast(context, intentId, intent, PendingIntent.FLAG_CANCEL_CURRENT) + val sender = PendingIntent.getBroadcast(context, intentId, intent, withImmutableFlag(PendingIntent.FLAG_CANCEL_CURRENT)) setAlarm(context, cal.timeInMillis, sender) } @@ -108,7 +109,7 @@ class TaskAlarmManager(private var context: Context, private var taskRepository: val intent = Intent(context, TaskReceiver::class.java) intent.action = remindersItem.id val intentId = remindersItem.id?.hashCode() ?: 0 and 0xfffffff - val sender = PendingIntent.getBroadcast(context, intentId, intent, PendingIntent.FLAG_UPDATE_CURRENT) + val sender = PendingIntent.getBroadcast(context, intentId, intent, withImmutableFlag(PendingIntent.FLAG_UPDATE_CURRENT)) val am = context.getSystemService(Context.ALARM_SERVICE) as? AlarmManager sender.cancel() am?.cancel(sender) @@ -140,13 +141,13 @@ class TaskAlarmManager(private var context: Context, private var taskRepository: notificationIntent.putExtra(NotificationPublisher.CHECK_DAILIES, false) val alarmManager = context?.getSystemService(Context.ALARM_SERVICE) as? AlarmManager - val previousSender = PendingIntent.getBroadcast(context, 0, notificationIntent, PendingIntent.FLAG_NO_CREATE) + val previousSender = PendingIntent.getBroadcast(context, 0, notificationIntent, withImmutableFlag(PendingIntent.FLAG_NO_CREATE)) if (previousSender != null) { previousSender.cancel() alarmManager?.cancel(previousSender) } - val pendingIntent = PendingIntent.getBroadcast(context, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT) + val pendingIntent = PendingIntent.getBroadcast(context, 0, notificationIntent, withImmutableFlag(PendingIntent.FLAG_UPDATE_CURRENT)) if (context != null) { setAlarm(context, triggerTime, pendingIntent) @@ -157,7 +158,7 @@ class TaskAlarmManager(private var context: Context, private var taskRepository: fun removeDailyReminder(context: Context?) { val notificationIntent = Intent(context, NotificationPublisher::class.java) val alarmManager = context?.getSystemService(Context.ALARM_SERVICE) as? AlarmManager - val displayIntent = PendingIntent.getBroadcast(context, 0, notificationIntent, 0) + val displayIntent = PendingIntent.getBroadcast(context, 0, notificationIntent, withImmutableFlag(0)) alarmManager?.cancel(displayIntent) } diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/notifications/GroupActivityNotification.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/notifications/GroupActivityNotification.kt index 2b4db0b6e..d74f03049 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/notifications/GroupActivityNotification.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/notifications/GroupActivityNotification.kt @@ -11,6 +11,7 @@ import androidx.core.app.Person import androidx.core.app.RemoteInput import androidx.core.os.bundleOf import com.habitrpg.android.habitica.R +import com.habitrpg.android.habitica.extensions.withImmutableFlag import com.habitrpg.android.habitica.receivers.LocalNotificationActionReceiver import com.habitrpg.android.habitica.ui.helpers.EmojiParser import java.text.SimpleDateFormat @@ -75,7 +76,7 @@ class GroupActivityNotification(context: Context, identifier: String?) : Habitic PendingIntent.getBroadcast( context, groupID.hashCode(), intent, - PendingIntent.FLAG_UPDATE_CURRENT + withImmutableFlag(PendingIntent.FLAG_UPDATE_CURRENT) ) val action: NotificationCompat.Action = diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/notifications/GuildInviteLocalNotification.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/notifications/GuildInviteLocalNotification.kt index 80c5e0421..e830c1833 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/notifications/GuildInviteLocalNotification.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/notifications/GuildInviteLocalNotification.kt @@ -4,6 +4,7 @@ import android.app.PendingIntent import android.content.Context import android.content.Intent import com.habitrpg.android.habitica.R +import com.habitrpg.android.habitica.extensions.withImmutableFlag import com.habitrpg.android.habitica.receivers.LocalNotificationActionReceiver /** @@ -29,7 +30,7 @@ class GuildInviteLocalNotification(context: Context, identifier: String?) : Habi context, groupID.hashCode(), acceptInviteIntent, - PendingIntent.FLAG_UPDATE_CURRENT + withImmutableFlag(PendingIntent.FLAG_UPDATE_CURRENT) ) notificationBuilder.addAction(0, "Accept", pendingIntentAccept) @@ -41,7 +42,7 @@ class GuildInviteLocalNotification(context: Context, identifier: String?) : Habi context, groupID.hashCode() + 1, rejectInviteIntent, - PendingIntent.FLAG_UPDATE_CURRENT + withImmutableFlag(PendingIntent.FLAG_UPDATE_CURRENT) ) notificationBuilder.addAction(0, "Reject", pendingIntentReject) } diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/notifications/HabiticaLocalNotification.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/notifications/HabiticaLocalNotification.kt index f44f7d0b8..3724a31f2 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/notifications/HabiticaLocalNotification.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/notifications/HabiticaLocalNotification.kt @@ -8,6 +8,7 @@ import androidx.annotation.CallSuper import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import com.habitrpg.android.habitica.R +import com.habitrpg.android.habitica.extensions.withImmutableFlag import com.habitrpg.android.habitica.ui.activities.MainActivity import java.util.* @@ -63,7 +64,7 @@ abstract class HabiticaLocalNotification(protected var context: Context, protect context, 3000, intent, - PendingIntent.FLAG_UPDATE_CURRENT + withImmutableFlag(PendingIntent.FLAG_UPDATE_CURRENT) ) notificationBuilder.setContentIntent(pendingIntent) } diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/notifications/PartyInviteLocalNotification.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/notifications/PartyInviteLocalNotification.kt index 21c0cc3f7..3e393b3a4 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/notifications/PartyInviteLocalNotification.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/notifications/PartyInviteLocalNotification.kt @@ -4,6 +4,7 @@ import android.app.PendingIntent import android.content.Context import android.content.Intent import com.habitrpg.android.habitica.R +import com.habitrpg.android.habitica.extensions.withImmutableFlag import com.habitrpg.android.habitica.receivers.LocalNotificationActionReceiver /** @@ -24,7 +25,7 @@ class PartyInviteLocalNotification(context: Context, identifier: String?) : Habi context, groupID.hashCode(), acceptInviteIntent, - PendingIntent.FLAG_UPDATE_CURRENT + withImmutableFlag(PendingIntent.FLAG_UPDATE_CURRENT) ) notificationBuilder.addAction(0, context.getString(R.string.accept), pendingIntentAccept) @@ -36,7 +37,7 @@ class PartyInviteLocalNotification(context: Context, identifier: String?) : Habi context, groupID.hashCode() + 1, rejectInviteIntent, - PendingIntent.FLAG_UPDATE_CURRENT + withImmutableFlag(PendingIntent.FLAG_UPDATE_CURRENT) ) notificationBuilder.addAction(0, context.getString(R.string.reject), pendingIntentReject) } diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/notifications/QuestInviteLocalNotification.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/notifications/QuestInviteLocalNotification.kt index 1c3bb5906..cdfe782d4 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/notifications/QuestInviteLocalNotification.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/notifications/QuestInviteLocalNotification.kt @@ -3,6 +3,7 @@ package com.habitrpg.android.habitica.helpers.notifications import android.app.PendingIntent import android.content.Context import android.content.Intent +import android.os.Build import com.habitrpg.android.habitica.R import com.habitrpg.android.habitica.receivers.LocalNotificationActionReceiver @@ -22,11 +23,16 @@ class QuestInviteLocalNotification(context: Context, identifier: String?) : Habi val acceptInviteIntent = Intent(context, LocalNotificationActionReceiver::class.java) acceptInviteIntent.action = res.getString(R.string.accept_quest_invite) acceptInviteIntent.putExtra("NOTIFICATION_ID", notificationId) + val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + PendingIntent.FLAG_UPDATE_CURRENT + PendingIntent.FLAG_IMMUTABLE + } else { + PendingIntent.FLAG_UPDATE_CURRENT + } val pendingIntentAccept = PendingIntent.getBroadcast( context, 3001, acceptInviteIntent, - PendingIntent.FLAG_UPDATE_CURRENT + flags ) notificationBuilder.addAction(0, "Accept", pendingIntentAccept) @@ -37,7 +43,7 @@ class QuestInviteLocalNotification(context: Context, identifier: String?) : Habi context, 2001, rejectInviteIntent, - PendingIntent.FLAG_UPDATE_CURRENT + flags ) notificationBuilder.addAction(0, "Reject", pendingIntentReject) } diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/notifications/ReceivedPrivateMessageLocalNotification.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/notifications/ReceivedPrivateMessageLocalNotification.kt index 1f432b1d1..98d2af601 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/notifications/ReceivedPrivateMessageLocalNotification.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/notifications/ReceivedPrivateMessageLocalNotification.kt @@ -10,6 +10,7 @@ import androidx.core.app.NotificationCompat import androidx.core.app.RemoteInput import androidx.core.os.bundleOf import com.habitrpg.android.habitica.R +import com.habitrpg.android.habitica.extensions.withImmutableFlag import com.habitrpg.android.habitica.receivers.LocalNotificationActionReceiver import com.habitrpg.android.habitica.ui.helpers.EmojiParser @@ -74,7 +75,7 @@ class ReceivedPrivateMessageLocalNotification(context: Context, identifier: Stri PendingIntent.getBroadcast( context, senderID.hashCode(), intent, - PendingIntent.FLAG_UPDATE_CURRENT + withImmutableFlag(PendingIntent.FLAG_UPDATE_CURRENT) ) val action: NotificationCompat.Action = diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/models/user/Authentication.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/models/user/Authentication.kt index b5dc64574..038efbccf 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/models/user/Authentication.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/models/user/Authentication.kt @@ -30,11 +30,11 @@ open class Authentication : RealmObject(), BaseObject { var facebookAuthentication: SocialAuthentication? = null val hasGoogleAuth: Boolean - get() = googleAuthentication?.emails?.isEmpty() == false + get() = googleAuthentication?.emails?.isEmpty() != true val hasAppleAuth: Boolean - get() = appleAuthentication?.emails?.isEmpty() == false + get() = appleAuthentication?.emails?.isEmpty() != true val hasFacebookAuth: Boolean - get() = facebookAuthentication?.emails?.isEmpty() == false + get() = facebookAuthentication?.emails?.isEmpty() != true var timestamps: AuthenticationTimestamps? = null } diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/receivers/NotificationPublisher.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/receivers/NotificationPublisher.kt index 5c3ba5c20..a82c14919 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/receivers/NotificationPublisher.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/receivers/NotificationPublisher.kt @@ -14,6 +14,7 @@ import com.habitrpg.android.habitica.HabiticaBaseApplication import com.habitrpg.android.habitica.R import com.habitrpg.android.habitica.data.TaskRepository import com.habitrpg.android.habitica.data.UserRepository +import com.habitrpg.android.habitica.extensions.withImmutableFlag import com.habitrpg.android.habitica.helpers.RxErrorHandler import com.habitrpg.android.habitica.helpers.TaskAlarmManager import com.habitrpg.android.habitica.models.tasks.Task @@ -126,7 +127,7 @@ class NotificationPublisher : BroadcastReceiver() { val intent = PendingIntent.getActivity( thisContext, 0, - notificationIntent, 0 + notificationIntent, withImmutableFlag(0) ) builder.setContentIntent(intent) diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/receivers/TaskReceiver.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/receivers/TaskReceiver.kt index 2425fd7f9..2a0813640 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/receivers/TaskReceiver.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/receivers/TaskReceiver.kt @@ -12,6 +12,7 @@ import androidx.core.app.NotificationManagerCompat import com.habitrpg.android.habitica.HabiticaBaseApplication import com.habitrpg.android.habitica.R import com.habitrpg.android.habitica.data.TaskRepository +import com.habitrpg.android.habitica.extensions.withImmutableFlag import com.habitrpg.android.habitica.helpers.RxErrorHandler import com.habitrpg.android.habitica.helpers.TaskAlarmManager import com.habitrpg.android.habitica.models.tasks.Task @@ -61,7 +62,7 @@ class TaskReceiver : BroadcastReceiver() { HLogger.log(LogLevel.INFO, this::javaClass.name, "Create Notification") intent.putExtra("notificationIdentifier", "task_reminder") - val pendingIntent = PendingIntent.getActivity(context, System.currentTimeMillis().toInt(), intent, 0) + val pendingIntent = PendingIntent.getActivity(context, System.currentTimeMillis().toInt(), intent, withImmutableFlag(0)) val soundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION) var notificationBuilder = NotificationCompat.Builder(context, "default") @@ -88,7 +89,7 @@ class TaskReceiver : BroadcastReceiver() { context, task.id.hashCode(), completeIntent, - PendingIntent.FLAG_UPDATE_CURRENT + withImmutableFlag(PendingIntent.FLAG_UPDATE_CURRENT) ) notificationBuilder.addAction(0, context.getString(R.string.complete), pendingIntentComplete) } diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/activities/LoginActivity.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/activities/LoginActivity.kt index fd66dfb5b..482e30389 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/activities/LoginActivity.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/activities/LoginActivity.kt @@ -33,6 +33,7 @@ import com.habitrpg.android.habitica.extensions.addOkButton import com.habitrpg.android.habitica.extensions.updateStatusBarColor import com.habitrpg.android.habitica.helpers.* import com.habitrpg.android.habitica.models.auth.UserAuthResponse +import com.habitrpg.android.habitica.models.user.User import com.habitrpg.android.habitica.ui.helpers.dismissKeyboard import com.habitrpg.android.habitica.ui.viewmodels.AuthenticationViewModel import com.habitrpg.android.habitica.ui.views.dialogs.HabiticaAlertDialog @@ -229,9 +230,17 @@ class LoginActivity : BaseActivity() { } private fun handleAuthResponse(response: UserAuthResponse) { + viewModel.handleAuthResponse(response) + compositeSubscription.add(userRepository.retrieveUser(true) + .subscribe({ + handleAuthResponse(it, response.newUser) + }, RxErrorHandler.handleEmptyError()) + ) + } + + private fun handleAuthResponse(user: User, isNew: Boolean) { hideProgress() dismissKeyboard() - viewModel.handleAuthResponse(response) if (isRegistering) { FirebaseAnalytics.getInstance(this).logEvent("user_registered", null) @@ -240,7 +249,7 @@ class LoginActivity : BaseActivity() { userRepository.retrieveUser(withTasks = true, forced = true) .subscribe( { - if (response.newUser) { + if (isNew) { this.startSetupActivity() } else { this.startMainActivity() @@ -288,8 +297,8 @@ class LoginActivity : BaseActivity() { private val pickAccountResult = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { if (it.resultCode == Activity.RESULT_OK) { viewModel.googleEmail = it?.data?.getStringExtra(AccountManager.KEY_ACCOUNT_NAME) - viewModel.handleGoogleLoginResult(this, recoverFromPlayServicesErrorResult) { - handleAuthResponse(it) + viewModel.handleGoogleLoginResult(this, recoverFromPlayServicesErrorResult) { user, isNew -> + handleAuthResponse(user, isNew) } } } @@ -297,8 +306,8 @@ class LoginActivity : BaseActivity() { private val recoverFromPlayServicesErrorResult = registerForActivityResult( ActivityResultContracts.StartActivityForResult()) { if (it.resultCode != Activity.RESULT_CANCELED) { - viewModel.handleGoogleLoginResult(this, null) { - handleAuthResponse(it) + viewModel.handleGoogleLoginResult(this, null) { user, isNew -> + handleAuthResponse(user, isNew) } } } diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/preferences/AccountPreferenceFragment.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/preferences/AccountPreferenceFragment.kt index 39c62ddb6..f98319fb4 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/preferences/AccountPreferenceFragment.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/preferences/AccountPreferenceFragment.kt @@ -8,28 +8,37 @@ import android.content.SharedPreferences import android.os.Bundle import android.text.InputType import android.view.View +import android.view.ViewGroup import android.widget.EditText import android.widget.LinearLayout -import android.widget.Toast +import android.widget.TextView import androidx.activity.result.contract.ActivityResultContracts import androidx.core.content.ContextCompat +import androidx.core.util.PatternsCompat +import androidx.core.widget.addTextChangedListener +import androidx.core.widget.doAfterTextChanged import androidx.preference.EditTextPreference import androidx.preference.Preference -import androidx.preference.PreferenceCategory import com.google.android.material.textfield.TextInputLayout import com.habitrpg.android.habitica.HabiticaBaseApplication import com.habitrpg.android.habitica.R import com.habitrpg.android.habitica.api.HostConfig import com.habitrpg.android.habitica.data.ApiClient +import com.habitrpg.android.habitica.events.ShowSnackbarEvent import com.habitrpg.android.habitica.extensions.addCancelButton import com.habitrpg.android.habitica.extensions.dpToPx import com.habitrpg.android.habitica.extensions.layoutInflater import com.habitrpg.android.habitica.helpers.RxErrorHandler import com.habitrpg.android.habitica.models.user.User +import com.habitrpg.android.habitica.ui.activities.FixCharacterValuesActivity +import com.habitrpg.android.habitica.ui.helpers.KeyboardUtil import com.habitrpg.android.habitica.ui.viewmodels.AuthenticationViewModel import com.habitrpg.android.habitica.ui.views.ExtraLabelPreference +import com.habitrpg.android.habitica.ui.views.HabiticaSnackbar +import com.habitrpg.android.habitica.ui.views.ValidatingEditText import com.habitrpg.android.habitica.ui.views.dialogs.HabiticaAlertDialog import com.habitrpg.android.habitica.ui.views.dialogs.HabiticaProgressDialog +import org.greenrobot.eventbus.EventBus import javax.inject.Inject class AccountPreferenceFragment: BasePreferencesFragment(), @@ -52,8 +61,6 @@ class AccountPreferenceFragment: BasePreferencesFragment(), super.onCreate(savedInstanceState) viewModel = AuthenticationViewModel() findPreference("confirm_username")?.isVisible = user?.flags?.verifiedUsername == false - - viewModel.setupFacebookLogin { viewModel.handleAuthResponse(it) } } override fun setupPreferences() { @@ -74,12 +81,12 @@ class AccountPreferenceFragment: BasePreferencesFragment(), private fun updateUserFields() { val user = user ?: return configurePreference(findPreference("username"), user.authentication?.localAuthentication?.username) - configurePreference(findPreference("email"), user.authentication?.localAuthentication?.email) + configurePreference(findPreference("email"), user.authentication?.localAuthentication?.email ?: getString(R.string.not_set)) findPreference("confirm_username")?.isVisible = user.flags?.verifiedUsername != true val passwordPref = findPreference("password") if (user.authentication?.hasPassword == true) { - passwordPref?.summary = "··········" + passwordPref?.summary = "✴✴✴✴✴✴✴✴✴✴✴" passwordPref?.extraText = getString(R.string.change_password) } else { passwordPref?.summary = getString(R.string.not_set) @@ -87,16 +94,17 @@ class AccountPreferenceFragment: BasePreferencesFragment(), } val googlePref = findPreference("google_auth") if (user.authentication?.hasGoogleAuth == true) { - googlePref?.summary = user.authentication?.googleAuthentication?.emails?.first() + googlePref?.summary = user.authentication?.googleAuthentication?.emails?.firstOrNull() googlePref?.extraText = getString(R.string.disconnect) googlePref?.extraTextColor = context?.let { ContextCompat.getColor(it, R.color.text_red) } } else { googlePref?.summary = getString(R.string.not_connected) googlePref?.extraText = getString(R.string.connect) + googlePref?.extraTextColor = context?.let { ContextCompat.getColor(it, R.color.text_ternary) } } val applePref = findPreference("apple_auth") - if (user.authentication?.hasGoogleAuth == true) { - applePref?.summary = user.authentication?.appleAuthentication?.emails?.first() + if (user.authentication?.hasAppleAuth == true) { + applePref?.summary = user.authentication?.appleAuthentication?.emails?.firstOrNull() applePref?.extraText = getString(R.string.disconnect) applePref?.extraTextColor = context?.let { ContextCompat.getColor(it, R.color.text_red) } } else { @@ -104,7 +112,7 @@ class AccountPreferenceFragment: BasePreferencesFragment(), } val facebookPref = findPreference("facebook_auth") if (user.authentication?.hasFacebookAuth == true) { - facebookPref?.summary = user.authentication?.facebookAuthentication?.emails?.first() + facebookPref?.summary = user.authentication?.facebookAuthentication?.emails?.firstOrNull() facebookPref?.extraText = getString(R.string.disconnect) facebookPref?.extraTextColor = context?.let { ContextCompat.getColor(it, R.color.text_red) } } else { @@ -127,42 +135,38 @@ class AccountPreferenceFragment: BasePreferencesFragment(), preference?.summary = value } - override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String) { - val profileCategory = findPreference("profile") as? PreferenceCategory - configurePreference(profileCategory?.findPreference(key), sharedPreferences?.getString(key, "")) - if (sharedPreferences != null) { - val newValue = sharedPreferences.getString(key, "") ?: return - when (key) { - "display_name" -> updateUser("profile.name", newValue, user?.profile?.name) - "photo_url" -> updateUser("profile.imageUrl", newValue, user?.profile?.imageUrl) - "about" -> updateUser("profile.blurb", newValue, user?.profile?.blurb) - } - } - } - override fun onPreferenceTreeClick(preference: Preference?): Boolean { when(preference?.key) { "username" -> showLoginNameDialog() "confirm_username" -> showConfirmUsernameDialog() - "email" -> showEmailDialog() + "email" -> { + if (user?.authentication?.hasPassword == true) { + showEmailDialog() + } else { + showAddPasswordDialog(true) + } + } "password" -> { if (user?.authentication?.hasPassword == true) { showChangePasswordDialog() } else { - showAddPasswordDialog() + showAddPasswordDialog(true) } } "UserID" -> { copyValue(getString(R.string.SP_userID), user?.id) return true } - "ApiToken" -> { + "APIToken" -> { copyValue(getString(R.string.SP_APIToken_title), hostConfig.apiKey) return true } + "display_name" -> updateUser("profile.name", user?.profile?.name, getString(R.string.display_name)) + "photo_url" -> updateUser("profile.imageUrl", user?.profile?.imageUrl, getString(R.string.photo_url)) + "about" -> updateUser("profile.blurb", user?.profile?.blurb, getString(R.string.about)) "google_auth" -> { if (user?.authentication?.hasGoogleAuth == true) { - apiClient.disconnectSocial("google").subscribe({}, RxErrorHandler.handleEmptyError()) + disconnect("google", "Google") } else { activity?.let { viewModel.handleGoogleLogin(it, pickAccountResult) @@ -171,7 +175,7 @@ class AccountPreferenceFragment: BasePreferencesFragment(), } "apple_auth" -> { if (user?.authentication?.hasAppleAuth == true) { - apiClient.disconnectSocial("apple").subscribe({}, RxErrorHandler.handleEmptyError()) + disconnect("apple", "Apple") } else { viewModel.connectApple(parentFragmentManager) { viewModel.handleAuthResponse(it) @@ -180,17 +184,33 @@ class AccountPreferenceFragment: BasePreferencesFragment(), } "facebook_auth" -> { if (user?.authentication?.hasFacebookAuth == true) { - apiClient.disconnectSocial("facebook").subscribe({}, RxErrorHandler.handleEmptyError()) - } else { - viewModel.handleFacebookLogin(this) + disconnect("facebook", "Facebook") } } "reset_account" -> showAccountResetConfirmation() "delete_account" -> showAccountDeleteConfirmation() + "fixCharacterValues" -> { + val intent = Intent(activity, FixCharacterValuesActivity::class.java) + activity?.startActivity(intent) + } } return super.onPreferenceTreeClick(preference) } + private fun disconnect(network: String, networkName: String) { + context?.let { context -> + val dialog = HabiticaAlertDialog(context) + dialog.setTitle(R.string.are_you_sure) + dialog.addButton(R.string.disconnect, true) { _, _ -> + apiClient.disconnectSocial(network) + .flatMap { userRepository.retrieveUser(true, true) } + .subscribe({ displayDisconnectSuccess(networkName) }, RxErrorHandler.handleEmptyError()) + } + dialog.addCancelButton() + dialog.show() + } + } + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) viewModel.onActivityResult(requestCode, resultCode, data) { @@ -202,47 +222,71 @@ class AccountPreferenceFragment: BasePreferencesFragment(), if (it.resultCode == Activity.RESULT_OK) { viewModel.googleEmail = it?.data?.getStringExtra(AccountManager.KEY_ACCOUNT_NAME) activity?.let { it1 -> - viewModel.handleGoogleLoginResult(it1, recoverFromPlayServicesErrorResult) { - viewModel.handleAuthResponse(it) + viewModel.handleGoogleLoginResult(it1, recoverFromPlayServicesErrorResult) { _, _ -> + displayAuthenticationSuccess(getString(R.string.google)) } } } } + private fun displayAuthenticationSuccess(network: String) { + ShowSnackbarEvent(null, context?.getString(R.string.added_social_auth, network), HabiticaSnackbar.SnackbarDisplayType.SUCCESS).post() + } + + private fun displayDisconnectSuccess(network: String) { + val event = ShowSnackbarEvent() + event.text = context?.getString(R.string.removed_social_auth, network) + event.type = HabiticaSnackbar.SnackbarDisplayType.SUCCESS + EventBus.getDefault().post(event) + } + private val recoverFromPlayServicesErrorResult = registerForActivityResult( ActivityResultContracts.StartActivityForResult()) { if (it.resultCode != Activity.RESULT_CANCELED) { activity?.let { it1 -> - viewModel.handleGoogleLoginResult(it1, null) { - viewModel.handleAuthResponse(it) + viewModel.handleGoogleLoginResult(it1, null) { _, _ -> + displayAuthenticationSuccess(getString(R.string.google)) } } } } - private fun updateUser(path: String, newValue: String, oldValue: String?) { - if (newValue != oldValue) { - compositeSubscription.add(userRepository.updateUser(path, newValue).subscribe({}, RxErrorHandler.handleEmptyError())) + private fun updateUser(path: String, value: String?, title: String) { + showSingleEntryDialog(value, title) { + if (value != it) { + userRepository.updateUser(path, it ?: "") + .subscribe({}, RxErrorHandler.handleEmptyError()) + } } } private fun showChangePasswordDialog() { val inflater = context?.layoutInflater val view = inflater?.inflate(R.layout.dialog_edittext_change_pw, null) - val oldPasswordEditText = view?.findViewById(R.id.editText) - val passwordEditText = view?.findViewById(R.id.passwordEditText) - val passwordRepeatEditText = view?.findViewById(R.id.passwordRepeatEditText) + val oldPasswordEditText = view?.findViewById(R.id.old_password_edit_text) + val passwordEditText = view?.findViewById(R.id.new_password_edit_text) + passwordEditText?.validator = { (it?.length ?: 0) >= 8 } + passwordEditText?.errorText = getString(R.string.password_too_short, 8) + val passwordRepeatEditText = view?.findViewById(R.id.new_password_repeat_edit_text) + passwordRepeatEditText?.validator = { it == passwordEditText?.text } + passwordRepeatEditText?.errorText = getString(R.string.password_not_matching) context?.let { context -> val dialog = HabiticaAlertDialog(context) dialog.setTitle(R.string.change_password) - dialog.addButton(R.string.change, true) { _, _ -> - userRepository.updatePassword(oldPasswordEditText?.text.toString(), passwordEditText?.text.toString(), passwordRepeatEditText?.text.toString()) + dialog.addButton(R.string.change, true, false, false) { dialog, _ -> + KeyboardUtil.dismissKeyboard(activity) + if (passwordEditText?.isValid != true || passwordRepeatEditText?.isValid != true) return@addButton + userRepository.updatePassword(oldPasswordEditText?.text ?: "", + passwordEditText.text ?: "", + passwordRepeatEditText.text ?: "") + .flatMap { userRepository.retrieveUser(true, true) } .subscribe( { - Toast.makeText(activity, R.string.password_changed, Toast.LENGTH_SHORT).show() + ShowSnackbarEvent(null, context.getString(R.string.password_changed), HabiticaSnackbar.SnackbarDisplayType.SUCCESS).post() }, RxErrorHandler.handleEmptyError() ) + dialog.dismiss() } dialog.addCancelButton() dialog.setAdditionalContentView(view) @@ -251,25 +295,39 @@ class AccountPreferenceFragment: BasePreferencesFragment(), } } - private fun showAddPasswordDialog() { + private fun showAddPasswordDialog(showEmail: Boolean) { val inflater = context?.layoutInflater - val view = inflater?.inflate(R.layout.dialog_edittext_change_pw, null) - val oldPasswordEditText = view?.findViewById(R.id.editText) - oldPasswordEditText?.visibility = View.GONE - val passwordEditText = view?.findViewById(R.id.passwordEditText) - val passwordRepeatEditText = view?.findViewById(R.id.passwordRepeatEditText) + val view = inflater?.inflate(R.layout.dialog_edittext_add_local_auth, null) + val emailEditText = view?.findViewById(R.id.email_edit_text) + emailEditText?.visibility = if (showEmail) View.VISIBLE else View.GONE + emailEditText?.validator = { PatternsCompat.EMAIL_ADDRESS.matcher(it ?: "").matches() } + emailEditText?.errorText = getString(R.string.email_invalid) + val passwordEditText = view?.findViewById(R.id.password_edit_text) + passwordEditText?.validator = { (it?.length ?: 0) >= 8 } + passwordEditText?.errorText = getString(R.string.password_too_short, 8) + val passwordRepeatEditText = view?.findViewById(R.id.password_repeat_edit_text) + passwordRepeatEditText?.validator = { it == passwordEditText?.text } + passwordRepeatEditText?.errorText = getString(R.string.password_not_matching) context?.let { context -> val dialog = HabiticaAlertDialog(context) - dialog.setTitle(R.string.add_password) - dialog.addButton(R.string.add, true) { _, _ -> - val email = user?.authentication?.findFirstSocialEmail() - apiClient.registerUser(user?.username ?: "", email ?: "", passwordEditText?.text.toString(), passwordRepeatEditText?.text.toString()) + if (showEmail) { + dialog.setTitle(R.string.add_email_and_password) + } else { + dialog.setTitle(R.string.add_password) + } + dialog.addButton(R.string.add, true, false, false) { dialog, _ -> + KeyboardUtil.dismissKeyboard(activity) + if (emailEditText?.isValid != true || passwordEditText?.isValid != true || passwordRepeatEditText?.isValid != true) return@addButton + val email = if (showEmail) emailEditText.text else user?.authentication?.findFirstSocialEmail() + apiClient.registerUser(user?.username ?: "", email ?: "", passwordEditText.text ?: "", passwordRepeatEditText?.text ?: "") + .flatMap { userRepository.retrieveUser(true, true) } .subscribe( { - Toast.makeText(activity, R.string.password_changed, Toast.LENGTH_SHORT).show() + ShowSnackbarEvent(null, context.getString(R.string.password_added), HabiticaSnackbar.SnackbarDisplayType.SUCCESS).post() }, RxErrorHandler.handleEmptyError() ) + dialog.dismiss() } dialog.addCancelButton() dialog.setAdditionalContentView(view) @@ -281,21 +339,27 @@ class AccountPreferenceFragment: BasePreferencesFragment(), private fun showEmailDialog() { val inflater = context?.layoutInflater val view = inflater?.inflate(R.layout.dialog_edittext_confirm_pw, null) - val emailEditText = view?.findViewById(R.id.editText) - emailEditText?.setText(user?.authentication?.localAuthentication?.email) + val emailEditText = view?.findViewById(R.id.email_edit_text) + emailEditText?.text = user?.authentication?.localAuthentication?.email + emailEditText?.validator = { PatternsCompat.EMAIL_ADDRESS.matcher(it ?: "").matches() } + emailEditText?.errorText = getString(R.string.email_invalid) view?.findViewById(R.id.input_layout)?.hint = context?.getString(R.string.email) - val passwordEditText = view?.findViewById(R.id.passwordEditText) + val passwordEditText = view?.findViewById(R.id.password_edit_text) context?.let { context -> val dialog = HabiticaAlertDialog(context) dialog.setTitle(R.string.change_email) - dialog.addButton(R.string.change, true) { _, _ -> - userRepository.updateEmail(emailEditText?.text.toString(), passwordEditText?.text.toString()) + dialog.addButton(R.string.change, true, false, false) { dialog, _ -> + KeyboardUtil.dismissKeyboard(activity) + if (passwordEditText?.isValid != true || emailEditText?.isValid != true) return@addButton + userRepository.updateEmail(emailEditText.text.toString(), passwordEditText.text.toString()) + .flatMap { userRepository.retrieveUser(true, true) } .subscribe( { - configurePreference(findPreference("email"), emailEditText?.text.toString()) + configurePreference(findPreference("email"), emailEditText.text.toString()) }, RxErrorHandler.handleEmptyError() ) + dialog.dismiss() } dialog.addCancelButton() dialog.setAdditionalContentView(view) @@ -305,22 +369,23 @@ class AccountPreferenceFragment: BasePreferencesFragment(), } private fun showLoginNameDialog() { + showSingleEntryDialog(user?.username, getString(R.string.username)) { + userRepository.updateLoginName(it ?: "") + .subscribe({}, RxErrorHandler.handleEmptyError()) + } + } + + private fun showSingleEntryDialog(value: String?, title: String, onChange: (String?) -> Unit) { val inflater = context?.layoutInflater val view = inflater?.inflate(R.layout.dialog_edittext, null) - val loginNameEditText = view?.findViewById(R.id.editText) - loginNameEditText?.setText(user?.authentication?.localAuthentication?.username) - view?.findViewById(R.id.input_layout)?.hint = context?.getString(R.string.username) + val editText = view?.findViewById(R.id.editText) + editText?.setText(value) + view?.findViewById(R.id.input_layout)?.hint = title context?.let { context -> val dialog = HabiticaAlertDialog(context) - dialog.setTitle(R.string.change_username) + dialog.setTitle(title) dialog.addButton(R.string.save, true) { _, _ -> - userRepository.updateLoginName(loginNameEditText?.text.toString()) - .subscribe( - { - configurePreference(findPreference("username"), loginNameEditText?.text.toString()) - }, - RxErrorHandler.handleEmptyError() - ) + onChange(editText?.text?.toString()) } dialog.addCancelButton() dialog.setAdditionalContentView(view) @@ -408,6 +473,12 @@ class AccountPreferenceFragment: BasePreferencesFragment(), private fun copyValue(name: String, value: CharSequence?) { ClipData.newPlainText(name, value) - Toast.makeText(activity, "Copied $name to clipboard.", Toast.LENGTH_SHORT).show() + val event = ShowSnackbarEvent() + event.text = context?.getString(R.string.copied_to_clipboard, name) + event.type = HabiticaSnackbar.SnackbarDisplayType.SUCCESS + EventBus.getDefault().post(event) + } + + override fun onSharedPreferenceChanged(p0: SharedPreferences?, p1: String?) { } } \ No newline at end of file diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/preferences/PreferencesFragment.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/preferences/PreferencesFragment.kt index 60bfb6b01..671f5b264 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/preferences/PreferencesFragment.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/preferences/PreferencesFragment.kt @@ -17,6 +17,7 @@ import com.habitrpg.android.habitica.R import com.habitrpg.android.habitica.data.ApiClient import com.habitrpg.android.habitica.data.ContentRepository import com.habitrpg.android.habitica.events.ShowSnackbarEvent +import com.habitrpg.android.habitica.extensions.addCancelButton import com.habitrpg.android.habitica.helpers.* import com.habitrpg.android.habitica.helpers.notifications.PushNotificationManager import com.habitrpg.android.habitica.models.user.User @@ -26,6 +27,7 @@ import com.habitrpg.android.habitica.ui.activities.FixCharacterValuesActivity import com.habitrpg.android.habitica.ui.activities.MainActivity import com.habitrpg.android.habitica.ui.activities.PrefsActivity import com.habitrpg.android.habitica.ui.views.HabiticaSnackbar +import com.habitrpg.android.habitica.ui.views.dialogs.HabiticaAlertDialog import org.greenrobot.eventbus.EventBus import java.util.* import javax.inject.Inject @@ -109,8 +111,7 @@ class PreferencesFragment : BasePreferencesFragment(), SharedPreferences.OnShare override fun onPreferenceTreeClick(preference: Preference): Boolean { when (preference.key) { "logout" -> { - context?.let { HabiticaBaseApplication.logout(it) } - activity?.finish() + logout() } "choose_class" -> { val bundle = Bundle() @@ -148,19 +149,28 @@ class PreferencesFragment : BasePreferencesFragment(), SharedPreferences.OnShare RxErrorHandler.handleEmptyError() ) } - "fixCharacterValues" -> { - val intent = Intent(activity, FixCharacterValuesActivity::class.java) - activity?.startActivity(intent) - } } return super.onPreferenceTreeClick(preference) } + private fun logout() { + context?.let { context -> + val dialog = HabiticaAlertDialog(context) + dialog.setTitle(R.string.are_you_sure) + dialog.addButton(R.string.logout, true) { _, _ -> + HabiticaBaseApplication.logout(context) + activity?.finish() + } + dialog.addCancelButton() + dialog.show() + } + } + private val classSelectionResult = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { userRepository.retrieveUser(true, forced = true) } - override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) { + override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String?) { when (key) { "use_reminder" -> { val useReminder = sharedPreferences.getBoolean(key, false) diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/viewmodels/AuthenticationViewModel.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/viewmodels/AuthenticationViewModel.kt index 63b0da2b6..b7fb3f59e 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/viewmodels/AuthenticationViewModel.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/viewmodels/AuthenticationViewModel.kt @@ -9,7 +9,6 @@ import android.os.Build import androidx.activity.result.ActivityResultLauncher import androidx.core.content.edit import androidx.fragment.app.Fragment -import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentManager import com.facebook.AccessToken import com.facebook.CallbackManager @@ -32,12 +31,14 @@ import com.habitrpg.android.habitica.HabiticaBaseApplication import com.habitrpg.android.habitica.R import com.habitrpg.android.habitica.api.HostConfig import com.habitrpg.android.habitica.data.ApiClient +import com.habitrpg.android.habitica.data.UserRepository import com.habitrpg.android.habitica.extensions.addCloseButton import com.habitrpg.android.habitica.helpers.KeyHelper import com.habitrpg.android.habitica.helpers.RxErrorHandler import com.habitrpg.android.habitica.helpers.SignInWithAppleResult import com.habitrpg.android.habitica.helpers.SignInWithAppleService import com.habitrpg.android.habitica.models.auth.UserAuthResponse +import com.habitrpg.android.habitica.models.user.User import com.habitrpg.android.habitica.proxy.AnalyticsManager import com.habitrpg.android.habitica.ui.views.dialogs.HabiticaAlertDialog import com.willowtreeapps.signinwithapplebutton.SignInWithAppleConfiguration @@ -52,6 +53,8 @@ class AuthenticationViewModel() { @Inject internal lateinit var apiClient: ApiClient @Inject + internal lateinit var userRepository: UserRepository + @Inject internal lateinit var sharedPrefs: SharedPreferences @Inject internal lateinit var hostConfig: HostConfig @@ -168,10 +171,11 @@ class AuthenticationViewModel() { fun handleGoogleLoginResult( activity: Activity, recoverFromPlayServicesErrorResult: ActivityResultLauncher?, - onSuccess: (UserAuthResponse) -> Unit + onSuccess: (User, Boolean) -> Unit ) { val scopesString = Scopes.PROFILE + " " + Scopes.EMAIL val scopes = "oauth2:$scopesString" + var newUser = false compositeSubscription.add( Flowable.defer { try { @@ -187,9 +191,14 @@ class AuthenticationViewModel() { } .subscribeOn(Schedulers.io()) .flatMap { token -> apiClient.connectSocial("google", googleEmail ?: "", token) } + .doOnNext { + newUser = it.newUser + handleAuthResponse(it) + } + .flatMap { userRepository.retrieveUser(true, true) } .subscribe( { - onSuccess(it) + onSuccess(it, newUser) }, { throwable -> if (recoverFromPlayServicesErrorResult == null) return@subscribe @@ -211,26 +220,24 @@ class AuthenticationViewModel() { recoverFromPlayServicesErrorResult: ActivityResultLauncher ) { if (e is GooglePlayServicesAvailabilityException) { - // The Google Play services APK is old, disabled, or not present. - // Show a dialog created by Google Play services that allows - // the user to update the APK - val statusCode = e - .connectionStatusCode GoogleApiAvailability.getInstance() - @Suppress("DEPRECATION") GooglePlayServicesUtil.showErrorDialogFragment( - statusCode, + e.connectionStatusCode, activity, + null, AuthenticationViewModel.REQUEST_CODE_RECOVER_FROM_PLAY_SERVICES_ERROR ) { } + return } else if (e is UserRecoverableAuthException) { // Unable to authenticate, such as when the user has not yet granted // the app access to the account, but the user can fix this. // Forward the user to an activity in Google Play services. val intent = e.intent recoverFromPlayServicesErrorResult.launch(intent) + return } + } @@ -257,7 +264,6 @@ class AuthenticationViewModel() { } HabiticaBaseApplication.reloadUserComponent() - } @Throws(Exception::class) @@ -276,7 +282,7 @@ class AuthenticationViewModel() { putString(user, encryptedKey) } else { // Something might have gone wrong with encryption, so fall back to this. - putString("ApiToken", api) + putString("APIToken", api) } } } diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/ValidatingEditText.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/ValidatingEditText.kt new file mode 100644 index 000000000..c81274863 --- /dev/null +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/ValidatingEditText.kt @@ -0,0 +1,55 @@ +package com.habitrpg.android.habitica.ui.views + +import android.content.Context +import android.text.InputType +import android.util.AttributeSet +import android.view.View +import android.view.ViewGroup +import android.widget.LinearLayout +import com.habitrpg.android.habitica.R +import com.habitrpg.android.habitica.databinding.ValidatingEditTextBinding +import com.habitrpg.android.habitica.extensions.layoutInflater + +class ValidatingEditText @JvmOverloads constructor( + context: Context, attrs: AttributeSet? = null +) : LinearLayout(context, attrs) { + + private var binding: ValidatingEditTextBinding = ValidatingEditTextBinding.inflate(context.layoutInflater, this) + + var text: String? + get() = binding.editText.text?.toString() + set(value) = binding.editText.setText(value) + var errorText: String? + get() = binding.errorText.text?.toString() + set(value) { + binding.errorText.text = value + } + + var validator: ((String?) -> Boolean)? = null + + val isValid: Boolean + get() = validator?.invoke(text) == true + + init { + orientation = VERTICAL + context.theme?.obtainStyledAttributes( + attrs, + R.styleable.ValidatingEditText, + 0, 0 + )?.let { attributes -> + binding.inputLayout.hint = attributes.getString(R.styleable.ValidatingEditText_hint) + binding.editText.inputType = attributes.getInt(R.styleable.ValidatingEditText_android_inputType, InputType.TYPE_CLASS_TEXT) + } + + + binding.editText.setOnFocusChangeListener { _, isEditing -> + if (isEditing) return@setOnFocusChangeListener + if (validator?.invoke(text) == true) { + binding.errorText.visibility = View.GONE + } else { + binding.errorText.visibility = View.VISIBLE + } + (this.parent as? ViewGroup)?.forceLayout() + } + } +} \ No newline at end of file diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/utils/SocialAuthenticationDeserializer.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/utils/SocialAuthenticationDeserializer.kt new file mode 100644 index 000000000..1024d8bd5 --- /dev/null +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/utils/SocialAuthenticationDeserializer.kt @@ -0,0 +1,29 @@ +package com.habitrpg.android.habitica.utils + +import com.google.gson.JsonDeserializationContext +import com.google.gson.JsonDeserializer +import com.google.gson.JsonElement +import com.google.gson.JsonParseException +import com.habitrpg.android.habitica.extensions.getAsString +import com.habitrpg.android.habitica.models.TutorialStep +import com.habitrpg.android.habitica.models.user.auth.SocialAuthentication +import java.lang.reflect.Type + +class SocialAuthenticationDeserializer : JsonDeserializer { + @Throws(JsonParseException::class) + override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): SocialAuthentication { + val authentication = SocialAuthentication() + val obj = json.asJsonObject + if (obj.has("emails")) { + val emailJson = obj.getAsJsonArray("emails") + for (entry in emailJson) { + if (entry.isJsonPrimitive) { + authentication.emails.add(entry.asString) + } else if (entry.isJsonObject) { + authentication.emails.add(entry.asJsonObject.getAsString("value")) + } + } + } + return authentication + } +} diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/widget/AvatarStatsWidgetFactory.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/widget/AvatarStatsWidgetFactory.kt index 0344488ce..fa8842ea7 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/widget/AvatarStatsWidgetFactory.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/widget/AvatarStatsWidgetFactory.kt @@ -169,7 +169,6 @@ class AvatarStatsWidgetFactory( } } - override fun getLoadingView() = RemoteViews(context.packageName, R.layout.widget_avatar_stats) override fun getViewTypeCount(): Int { diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/widget/AvatarStatsWidgetProvider.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/widget/AvatarStatsWidgetProvider.kt index 25af7aec0..ad0f810c7 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/widget/AvatarStatsWidgetProvider.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/widget/AvatarStatsWidgetProvider.kt @@ -9,6 +9,7 @@ import android.os.Bundle import android.widget.RemoteViews import com.habitrpg.android.habitica.HabiticaBaseApplication import com.habitrpg.android.habitica.R +import com.habitrpg.android.habitica.extensions.withImmutableFlag import com.habitrpg.android.habitica.ui.activities.MainActivity class AvatarStatsWidgetProvider : BaseWidgetProvider() { @@ -37,7 +38,7 @@ class AvatarStatsWidgetProvider : BaseWidgetProvider() { intent.data = Uri.parse(intent.toUri(Intent.URI_INTENT_SCHEME)) val openAppIntent = Intent(context.applicationContext, MainActivity::class.java) - val openApp = PendingIntent.getActivity(context, 0, openAppIntent, PendingIntent.FLAG_UPDATE_CURRENT) + val openApp = PendingIntent.getActivity(context, 0, openAppIntent, withImmutableFlag(PendingIntent.FLAG_UPDATE_CURRENT)) val remoteViews = RemoteViews(context.packageName, R.layout.widget_main_avatar_stats) remoteViews.setRemoteAdapter(R.id.widget_avatar_list, intent) diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/widget/BaseWidgetProvider.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/widget/BaseWidgetProvider.kt index ecaa6e5b4..b29cc969c 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/widget/BaseWidgetProvider.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/widget/BaseWidgetProvider.kt @@ -39,8 +39,6 @@ abstract class BaseWidgetProvider : AppWidgetProvider() { protected var context: Context? = null - - override fun onAppWidgetOptionsChanged(context: Context, appWidgetManager: AppWidgetManager, appWidgetId: Int, newOptions: Bundle) { this.context = context val options = appWidgetManager.getAppWidgetOptions(appWidgetId) diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/widget/HabitButtonWidgetService.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/widget/HabitButtonWidgetService.kt index f6a6f70d0..9e2a0c23a 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/widget/HabitButtonWidgetService.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/widget/HabitButtonWidgetService.kt @@ -15,6 +15,7 @@ import androidx.core.content.ContextCompat import com.habitrpg.android.habitica.HabiticaBaseApplication import com.habitrpg.android.habitica.R import com.habitrpg.android.habitica.data.TaskRepository +import com.habitrpg.android.habitica.extensions.withImmutableFlag import com.habitrpg.android.habitica.helpers.RxErrorHandler import com.habitrpg.android.habitica.models.responses.TaskDirection import com.habitrpg.android.habitica.models.tasks.Task @@ -107,7 +108,7 @@ class HabitButtonWidgetService : Service() { taskIntent.putExtra(HabitButtonWidgetProvider.TASK_DIRECTION, direction) return PendingIntent.getBroadcast( context, widgetId + direction.hashCode(), taskIntent, - PendingIntent.FLAG_UPDATE_CURRENT + withImmutableFlag(PendingIntent.FLAG_UPDATE_CURRENT) ) } } diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/widget/TaskListWidgetProvider.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/widget/TaskListWidgetProvider.kt index 689a718e3..fab2dd367 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/widget/TaskListWidgetProvider.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/widget/TaskListWidgetProvider.kt @@ -10,6 +10,7 @@ import android.widget.RemoteViews import com.habitrpg.android.habitica.HabiticaBaseApplication import com.habitrpg.android.habitica.R import com.habitrpg.android.habitica.data.TaskRepository +import com.habitrpg.android.habitica.extensions.withImmutableFlag import com.habitrpg.android.habitica.helpers.RxErrorHandler import com.habitrpg.android.habitica.ui.activities.MainActivity import javax.inject.Inject @@ -80,7 +81,7 @@ abstract class TaskListWidgetProvider : BaseWidgetProvider() { // if the user click on the title: open App val openAppIntent = Intent(context.applicationContext, MainActivity::class.java) - val openApp = PendingIntent.getActivity(context, 0, openAppIntent, PendingIntent.FLAG_UPDATE_CURRENT) + val openApp = PendingIntent.getActivity(context, 0, openAppIntent, withImmutableFlag(PendingIntent.FLAG_UPDATE_CURRENT)) rv.setOnClickPendingIntent(R.id.widget_title, openApp) val taskIntent = Intent(context, providerClass) @@ -89,7 +90,7 @@ abstract class TaskListWidgetProvider : BaseWidgetProvider() { intent.data = Uri.parse(intent.toUri(Intent.URI_INTENT_SCHEME)) val toastPendingIntent = PendingIntent.getBroadcast( context, 0, taskIntent, - PendingIntent.FLAG_UPDATE_CURRENT + withImmutableFlag(PendingIntent.FLAG_UPDATE_CURRENT) ) rv.setPendingIntentTemplate(R.id.list_view, toastPendingIntent) diff --git a/build.gradle b/build.gradle index ffe6ad3b3..51a01feef 100644 --- a/build.gradle +++ b/build.gradle @@ -13,7 +13,7 @@ buildscript { classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8' classpath 'com.google.gms:google-services:4.3.10' classpath 'com.google.firebase:firebase-crashlytics-gradle:2.8.0' - classpath "io.realm:realm-gradle-plugin:10.8.0" + classpath "io.realm:realm-gradle-plugin:10.8.1" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "io.gitlab.arturbosch.detekt:detekt-gradle-plugin:1.18.1" classpath "androidx.navigation:navigation-safe-args-gradle-plugin:2.3.5" diff --git a/shared/build.gradle b/shared/build.gradle index 411baeefb..bd0129fec 100644 --- a/shared/build.gradle +++ b/shared/build.gradle @@ -3,12 +3,11 @@ apply plugin: 'kotlin-multiplatform' apply plugin: 'kotlin-kapt' android { - compileSdkVersion 30 + compileSdkVersion 31 defaultConfig { - minSdkVersion 21 - targetSdkVersion 30 + targetSdkVersion 31 } buildTypes { release {