diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index c9df76c2d2..828a76bab9 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -97,12 +97,12 @@ + android:windowSoftInputMode="adjustNothing" /> + android:windowSoftInputMode="adjustNothing" /> - userActivities - .firstOrNull() - ?.pins - ?.let(StepikUtil::getCurrentStreak) - .toMaybe() - } + .flatMapSingleElement(userActivityRepository::getUserActivitySummary) + .map { it.recentStrike } .subscribeOn(backgroundScheduler) .observeOn(mainScheduler) .subscribeBy( diff --git a/app/src/main/java/org/stepic/droid/di/NotificationModule.kt b/app/src/main/java/org/stepic/droid/di/NotificationModule.kt index e98925a84b..3fa3e9f5f2 100644 --- a/app/src/main/java/org/stepic/droid/di/NotificationModule.kt +++ b/app/src/main/java/org/stepic/droid/di/NotificationModule.kt @@ -19,6 +19,7 @@ import org.stepik.android.view.purchase_notification.notification.PurchaseNotifi import org.stepik.android.view.splash.notification.RemindRegistrationNotificationDelegate import org.stepik.android.view.splash.notification.RetentionNotificationDelegate import org.stepik.android.view.streak.notification.StreakNotificationDelegate +import org.stepik.android.view.streak.notification.StreakNotificationScheduler @Module(includes = [CourseDataModule::class]) interface NotificationModule { @@ -54,7 +55,10 @@ interface NotificationModule { @IntoSet fun provideStreakNotificationDelegate(streakNotificationDelegate: StreakNotificationDelegate): NotificationDelegate + @Binds + fun bindStreakNotificationScheduler(streakNotificationDelegate: StreakNotificationDelegate): StreakNotificationScheduler + @Binds @IntoSet fun providePurchaseNotificationDelegate(purchaseNotificationDelegate: PurchaseNotificationDelegate): NotificationDelegate -} \ No newline at end of file +} diff --git a/app/src/main/java/org/stepic/droid/ui/adapters/SocialAuthAdapter.java b/app/src/main/java/org/stepic/droid/ui/adapters/SocialAuthAdapter.java index f93780b806..73e65cda2c 100644 --- a/app/src/main/java/org/stepic/droid/ui/adapters/SocialAuthAdapter.java +++ b/app/src/main/java/org/stepic/droid/ui/adapters/SocialAuthAdapter.java @@ -16,28 +16,12 @@ import kotlin.jvm.functions.Function1; public class SocialAuthAdapter extends RecyclerView.Adapter implements OnItemClickListener { - private SocialNetwork[] socialList; - private Function1 onSocialItemClick; + private final SocialNetwork[] socialList; + private final Function1 onSocialItemClick; - private State state; - - public enum State { - EXPANDED(4), NORMAL(3); - - public final int multiplier; - State(int multiplier) { - this.multiplier = multiplier; - } - } - - public SocialAuthAdapter(Function1 onSocialItemClick, State state) { + public SocialAuthAdapter(Function1 onSocialItemClick) { this.onSocialItemClick = onSocialItemClick; socialList = SocialNetwork.values(); - if (state == null) { - this.state = State.NORMAL; - } else { - this.state = state; - } } @@ -56,7 +40,7 @@ public void onBindViewHolder(SocialViewHolder holder, int position) { @Override public int getItemCount() { - return state.multiplier; + return socialList.length; } @Override @@ -64,24 +48,6 @@ public void onItemClick(int position) { onSocialItemClick.invoke(socialList[position]); } - public void showMore() { - int start = getItemCount(); - state = State.EXPANDED; - int end = getItemCount(); - notifyItemRangeInserted(start, end - start); - } - - public void showLess() { - int end = getItemCount(); - state = State.NORMAL; - int start = getItemCount(); - notifyItemRangeRemoved(start, end - start); - } - - public State getState() { - return state; - } - static class SocialViewHolder extends RecyclerView.ViewHolder { private final ImageView imageView; diff --git a/app/src/main/java/org/stepik/android/data/user_activity/repository/UserActivityRepositoryImpl.kt b/app/src/main/java/org/stepik/android/data/user_activity/repository/UserActivityRepositoryImpl.kt index 833d23739f..9f83580a19 100644 --- a/app/src/main/java/org/stepik/android/data/user_activity/repository/UserActivityRepositoryImpl.kt +++ b/app/src/main/java/org/stepik/android/data/user_activity/repository/UserActivityRepositoryImpl.kt @@ -4,6 +4,7 @@ import io.reactivex.Single import org.stepik.android.data.user_activity.source.UserActivityRemoteDataSource import org.stepik.android.domain.user_activity.repository.UserActivityRepository import org.stepik.android.model.user.UserActivity +import org.stepik.android.model.user.UserActivitySummary import javax.inject.Inject class UserActivityRepositoryImpl @@ -14,4 +15,7 @@ constructor( override fun getUserActivities(userId: Long): Single> = userActivityRemoteDataSource.getUserActivities(userId) + + override fun getUserActivitySummary(userId: Long): Single = + userActivityRemoteDataSource.getUserActivitySummary(userId) } \ No newline at end of file diff --git a/app/src/main/java/org/stepik/android/data/user_activity/source/UserActivityRemoteDataSource.kt b/app/src/main/java/org/stepik/android/data/user_activity/source/UserActivityRemoteDataSource.kt index b5f81241c1..c919a95cef 100644 --- a/app/src/main/java/org/stepik/android/data/user_activity/source/UserActivityRemoteDataSource.kt +++ b/app/src/main/java/org/stepik/android/data/user_activity/source/UserActivityRemoteDataSource.kt @@ -2,7 +2,10 @@ package org.stepik.android.data.user_activity.source import io.reactivex.Single import org.stepik.android.model.user.UserActivity +import org.stepik.android.model.user.UserActivitySummary interface UserActivityRemoteDataSource { fun getUserActivities(userId: Long): Single> + + fun getUserActivitySummary(userId: Long): Single } \ No newline at end of file diff --git a/app/src/main/java/org/stepik/android/domain/profile_activities/interactor/ProfileActivitiesInteractor.kt b/app/src/main/java/org/stepik/android/domain/profile_activities/interactor/ProfileActivitiesInteractor.kt index 35cd0b1e34..64c9a0f9f7 100644 --- a/app/src/main/java/org/stepik/android/domain/profile_activities/interactor/ProfileActivitiesInteractor.kt +++ b/app/src/main/java/org/stepik/android/domain/profile_activities/interactor/ProfileActivitiesInteractor.kt @@ -3,10 +3,8 @@ package org.stepik.android.domain.profile_activities.interactor import io.reactivex.Single import org.stepik.android.domain.profile_activities.model.ProfileActivitiesData import org.stepik.android.domain.user_activity.repository.UserActivityRepository -import org.stepik.android.model.user.UserActivity -import ru.nobird.android.domain.rx.first +import org.stepik.android.model.user.UserActivitySummary import javax.inject.Inject -import kotlin.math.max class ProfileActivitiesInteractor @Inject @@ -15,28 +13,14 @@ constructor( ) { fun getProfileActivities(userId: Long): Single = userActivityRepository - .getUserActivities(userId) - .first() + .getUserActivitySummary(userId) .map(::mapToProfileActivities) - private fun mapToProfileActivities(userActivity: UserActivity): ProfileActivitiesData { - var streak = 0 - var maxStreak = 0 - var buffer = 0 - - for (i in 0..userActivity.pins.size) { - val pin = userActivity.pins.getOrElse(i) { 0 } - if (pin > 0) { - buffer++ - } else { - maxStreak = max(maxStreak, buffer) - if (buffer == i || buffer == i - 1) { // first is solved today, second not solved - streak = buffer - } - buffer = 0 - } - } - - return ProfileActivitiesData(userActivity.pins, streak, maxStreak, isSolvedToday = userActivity.pins[0] > 0) - } + private fun mapToProfileActivities(userActivitySummary: UserActivitySummary): ProfileActivitiesData = + ProfileActivitiesData( + pins = userActivitySummary.pins, + streak = userActivitySummary.recentStrike, + maxStreak = userActivitySummary.maxStrike, + isSolvedToday = userActivitySummary.solvedToday > 0 + ) } \ No newline at end of file diff --git a/app/src/main/java/org/stepik/android/domain/streak/interactor/StreakInteractor.kt b/app/src/main/java/org/stepik/android/domain/streak/interactor/StreakInteractor.kt index f1a56cff4b..ebc3edb36c 100644 --- a/app/src/main/java/org/stepik/android/domain/streak/interactor/StreakInteractor.kt +++ b/app/src/main/java/org/stepik/android/domain/streak/interactor/StreakInteractor.kt @@ -2,10 +2,8 @@ package org.stepik.android.domain.streak.interactor import io.reactivex.Maybe import org.stepic.droid.preferences.SharedPreferenceHelper -import org.stepic.droid.util.StepikUtil -import ru.nobird.android.domain.rx.toMaybe import org.stepik.android.domain.user_activity.repository.UserActivityRepository -import org.stepik.android.view.streak.notification.StreakNotificationDelegate +import org.stepik.android.view.streak.notification.StreakNotificationScheduler import javax.inject.Inject class StreakInteractor @@ -13,7 +11,7 @@ class StreakInteractor constructor( private val userActivityRepository: UserActivityRepository, private val sharedPreferenceHelper: SharedPreferenceHelper, - private val streakNotificationDelegate: StreakNotificationDelegate + private val streakNotificationScheduler: StreakNotificationScheduler ) { fun needShowStreakDialog(): Boolean = @@ -24,14 +22,13 @@ constructor( fun onNeedShowStreak(): Maybe = Maybe .fromCallable { sharedPreferenceHelper.profile?.id } - .flatMapSingleElement { userActivityRepository.getUserActivities(it) } - .flatMap { it.firstOrNull()?.pins.toMaybe() } - .map { StepikUtil.getCurrentStreak(it) } + .flatMapSingleElement { userActivityRepository.getUserActivitySummary(it) } + .map { it.recentStrike } fun setStreakTime(timeIntervalCode: Int) { sharedPreferenceHelper.isStreakNotificationEnabled = true sharedPreferenceHelper.timeNotificationCode = timeIntervalCode - streakNotificationDelegate.scheduleStreakNotification() + streakNotificationScheduler.scheduleStreakNotification() } fun wasStreakDialogSeenOnHomeScreen(): Boolean = @@ -49,4 +46,4 @@ constructor( private fun isAuthResponseFromStore(): Boolean = sharedPreferenceHelper.authResponseFromStore != null -} \ No newline at end of file +} diff --git a/app/src/main/java/org/stepik/android/domain/user_activity/repository/UserActivityRepository.kt b/app/src/main/java/org/stepik/android/domain/user_activity/repository/UserActivityRepository.kt index cb9b854929..8a03bac2f8 100644 --- a/app/src/main/java/org/stepik/android/domain/user_activity/repository/UserActivityRepository.kt +++ b/app/src/main/java/org/stepik/android/domain/user_activity/repository/UserActivityRepository.kt @@ -2,7 +2,10 @@ package org.stepik.android.domain.user_activity.repository import io.reactivex.Single import org.stepik.android.model.user.UserActivity +import org.stepik.android.model.user.UserActivitySummary interface UserActivityRepository { fun getUserActivities(userId: Long): Single> + + fun getUserActivitySummary(userId: Long): Single } \ No newline at end of file diff --git a/app/src/main/java/org/stepik/android/remote/user_activity/UserActivityRemoteDataSourceImpl.kt b/app/src/main/java/org/stepik/android/remote/user_activity/UserActivityRemoteDataSourceImpl.kt index 0b4381d85d..d2fb9d2ee0 100644 --- a/app/src/main/java/org/stepik/android/remote/user_activity/UserActivityRemoteDataSourceImpl.kt +++ b/app/src/main/java/org/stepik/android/remote/user_activity/UserActivityRemoteDataSourceImpl.kt @@ -4,7 +4,9 @@ import io.reactivex.Single import io.reactivex.functions.Function import org.stepik.android.data.user_activity.source.UserActivityRemoteDataSource import org.stepik.android.model.user.UserActivity +import org.stepik.android.model.user.UserActivitySummary import org.stepik.android.remote.user_activity.model.UserActivityResponse +import org.stepik.android.remote.user_activity.model.UserActivitySummaryResponse import org.stepik.android.remote.user_activity.service.UserActivityService import javax.inject.Inject @@ -16,8 +18,16 @@ constructor( private val userActivityResponseMapper = Function>(UserActivityResponse::userActivities) + private val userActivitySummaryResponseMapper = + Function { it.userActivitySummaries.first() } + override fun getUserActivities(userId: Long): Single> = userActivityService .getUserActivitiesReactive(userId) .map(userActivityResponseMapper) + + override fun getUserActivitySummary(userId: Long): Single = + userActivityService + .getUserActivitySummaryReactive(userId) + .map(userActivitySummaryResponseMapper) } \ No newline at end of file diff --git a/app/src/main/java/org/stepik/android/remote/user_activity/model/UserActivitySummaryResponse.kt b/app/src/main/java/org/stepik/android/remote/user_activity/model/UserActivitySummaryResponse.kt new file mode 100644 index 0000000000..00e698bcf3 --- /dev/null +++ b/app/src/main/java/org/stepik/android/remote/user_activity/model/UserActivitySummaryResponse.kt @@ -0,0 +1,13 @@ +package org.stepik.android.remote.user_activity.model + +import com.google.gson.annotations.SerializedName +import org.stepik.android.model.Meta +import org.stepik.android.model.user.UserActivitySummary +import org.stepik.android.remote.base.model.MetaResponse + +class UserActivitySummaryResponse( + @SerializedName("meta") + override val meta: Meta, + @SerializedName("user-activity-summaries") + val userActivitySummaries: List +) : MetaResponse \ No newline at end of file diff --git a/app/src/main/java/org/stepik/android/remote/user_activity/service/UserActivityService.kt b/app/src/main/java/org/stepik/android/remote/user_activity/service/UserActivityService.kt index f49734d387..28190596e0 100644 --- a/app/src/main/java/org/stepik/android/remote/user_activity/service/UserActivityService.kt +++ b/app/src/main/java/org/stepik/android/remote/user_activity/service/UserActivityService.kt @@ -2,10 +2,14 @@ package org.stepik.android.remote.user_activity.service import io.reactivex.Single import org.stepik.android.remote.user_activity.model.UserActivityResponse +import org.stepik.android.remote.user_activity.model.UserActivitySummaryResponse import retrofit2.http.GET import retrofit2.http.Path interface UserActivityService { @GET("api/user-activities/{userId}") fun getUserActivitiesReactive(@Path("userId") userId: Long): Single + + @GET("api/user-activity-summaries/{userId}") + fun getUserActivitySummaryReactive(@Path("userId") userId: Long): Single } \ No newline at end of file diff --git a/app/src/main/java/org/stepik/android/view/auth/extension/AuthKeyboardInsetsAnimationExtension.kt b/app/src/main/java/org/stepik/android/view/auth/extension/AuthKeyboardInsetsAnimationExtension.kt new file mode 100644 index 0000000000..50293132a1 --- /dev/null +++ b/app/src/main/java/org/stepik/android/view/auth/extension/AuthKeyboardInsetsAnimationExtension.kt @@ -0,0 +1,200 @@ +package org.stepik.android.view.auth.extension + +import android.view.View +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsAnimationCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.doOnLayout +import org.stepik.android.view.auth.ui.view.AuthScrollView + +fun View.addAuthKeyboardInsetsAnimation( + contentView: View, + logoView: View, + titleView: View, + keyboardPinnedView: View? = null +) { + AuthKeyboardInsetsAnimationDelegate( + rootView = this, + contentView = contentView, + logoView = logoView, + titleView = titleView, + keyboardPinnedView = keyboardPinnedView + ).bind() +} + +private class AuthKeyboardInsetsAnimationDelegate( + private val rootView: View, + private val contentView: View, + private val logoView: View, + private val titleView: View, + private val keyboardPinnedView: View? +) { + private var isImeAnimationRunning = false + private var lastImeBottomInset = 0 + private var isImeVisible = false + + private var isInitialStateCaptured = false + + private var logoHiddenTranslationY = 0f + private var titleHiddenTranslationY = 0f + private var contentHiddenTranslationY = 0f + + private var imeAnimationLowerBound = 0 + private var imeAnimationUpperBound = 0 + + private var initialRootPaddingLeft = 0 + private var initialRootPaddingTop = 0 + private var initialRootPaddingRight = 0 + private var initialRootPaddingBottom = 0 + private var lastSystemBarsBottomInset = 0 + + fun bind() { + (rootView as? AuthScrollView)?.isFocusScrollEnabled = false + + ViewCompat.setOnApplyWindowInsetsListener(rootView) { _, insets -> + val systemBarsInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars()) + val imeBottomInset = getImeBottomInset(insets) + isImeVisible = insets.isImeVisible() + lastImeBottomInset = imeBottomInset + lastSystemBarsBottomInset = systemBarsInsets.bottom + + if (isInitialStateCaptured) { + applyInsetsPadding(systemBarsInsets) + } + + if (!isImeAnimationRunning) { + setKeyboardProgress(if (isImeVisible) 1f else 0f) + } + insets + } + + ViewCompat.setWindowInsetsAnimationCallback( + rootView, + object : WindowInsetsAnimationCompat.Callback(DISPATCH_MODE_STOP) { + override fun onStart( + animation: WindowInsetsAnimationCompat, + bounds: WindowInsetsAnimationCompat.BoundsCompat + ): WindowInsetsAnimationCompat.BoundsCompat { + if (animation.isImeInsetsAnimation()) { + isImeAnimationRunning = true + captureHiddenTranslations() + imeAnimationLowerBound = bounds.lowerBound.bottom + imeAnimationUpperBound = bounds.upperBound.bottom + } + return bounds + } + + override fun onProgress( + insets: WindowInsetsCompat, + runningAnimations: MutableList + ): WindowInsetsCompat { + val imeAnimation = runningAnimations.firstOrNull { it.isImeInsetsAnimation() } + if (imeAnimation != null) { + val systemBarsInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars()) + val imeBottomInset = getImeBottomInset(insets) + val animationProgress = imeAnimation.interpolatedFraction.coerceIn(0f, 1f) + lastImeBottomInset = imeBottomInset + lastSystemBarsBottomInset = systemBarsInsets.bottom + + if (isInitialStateCaptured) { + applyInsetsPadding(systemBarsInsets) + } + setKeyboardProgress(resolveKeyboardProgress(imeBottomInset, animationProgress)) + } + + return insets + } + + override fun onEnd(animation: WindowInsetsAnimationCompat) { + if (animation.isImeInsetsAnimation()) { + ViewCompat.getRootWindowInsets(rootView)?.let { insets -> + val systemBarsInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars()) + isImeVisible = insets.isImeVisible() + lastImeBottomInset = getImeBottomInset(insets) + lastSystemBarsBottomInset = systemBarsInsets.bottom + } + isImeAnimationRunning = false + setKeyboardProgress(if (isImeVisible) 1f else 0f) + ViewCompat.requestApplyInsets(rootView) + } + } + } + ) + + rootView.doOnLayout { + captureInitialState() + ViewCompat.requestApplyInsets(rootView) + } + } + + private fun captureInitialState() { + if (isInitialStateCaptured) { + return + } + + initialRootPaddingLeft = rootView.paddingLeft + initialRootPaddingTop = rootView.paddingTop + initialRootPaddingRight = rootView.paddingRight + initialRootPaddingBottom = rootView.paddingBottom + + captureHiddenTranslations() + + isInitialStateCaptured = true + } + + private fun captureHiddenTranslations() { + logoHiddenTranslationY = -(logoView.top + logoView.height).toFloat() + titleHiddenTranslationY = -(titleView.top + titleView.height).toFloat() + contentHiddenTranslationY = -contentView.top.toFloat() + } + + private fun setKeyboardProgress(progress: Float) { + if (!isInitialStateCaptured) { + return + } + + val normalizedProgress = progress.coerceIn(0f, 1f) + val headerVisibilityProgress = 1f - normalizedProgress + + logoView.alpha = headerVisibilityProgress + titleView.alpha = headerVisibilityProgress + + logoView.visibility = View.VISIBLE + titleView.visibility = View.VISIBLE + + logoView.translationY = logoHiddenTranslationY * normalizedProgress + titleView.translationY = titleHiddenTranslationY * normalizedProgress + contentView.translationY = contentHiddenTranslationY * normalizedProgress + keyboardPinnedView?.translationY = -(lastImeBottomInset - lastSystemBarsBottomInset).coerceAtLeast(0).toFloat() + } + + private fun resolveKeyboardProgress( + imeBottomInset: Int, + fallbackAnimationProgress: Float + ): Float { + val imeAnimationRange = imeAnimationUpperBound - imeAnimationLowerBound + return if (imeAnimationRange > 0) { + ((imeBottomInset - imeAnimationLowerBound).toFloat() / imeAnimationRange).coerceIn(0f, 1f) + } else { + fallbackAnimationProgress.coerceIn(0f, 1f) + } + } + + private fun applyInsetsPadding(systemBarsInsets: androidx.core.graphics.Insets) { + rootView.setPadding( + initialRootPaddingLeft + systemBarsInsets.left, + initialRootPaddingTop + systemBarsInsets.top, + initialRootPaddingRight + systemBarsInsets.right, + initialRootPaddingBottom + systemBarsInsets.bottom + ) + } + + private fun getImeBottomInset(insets: WindowInsetsCompat): Int = + insets.getInsets(WindowInsetsCompat.Type.ime()).bottom + + private fun WindowInsetsCompat.isImeVisible(): Boolean = + isVisible(WindowInsetsCompat.Type.ime()) || getImeBottomInset(this) > 0 + + private fun WindowInsetsAnimationCompat.isImeInsetsAnimation(): Boolean = + typeMask and WindowInsetsCompat.Type.ime() != 0 +} diff --git a/app/src/main/java/org/stepik/android/view/auth/model/SocialNetwork.kt b/app/src/main/java/org/stepik/android/view/auth/model/SocialNetwork.kt index fb59c57dcf..67aaa930f3 100644 --- a/app/src/main/java/org/stepik/android/view/auth/model/SocialNetwork.kt +++ b/app/src/main/java/org/stepik/android/view/auth/model/SocialNetwork.kt @@ -15,6 +15,5 @@ enum class SocialNetwork( GOOGLE("google", R.drawable.ic_login_social_google), VK("vk", R.drawable.ic_login_social_vk, isNeedUseAccessTokenInsteadOfCode = true), // FACEBOOK("facebook", R.drawable.ic_login_social_fb, isNeedUseAccessTokenInsteadOfCode = true), - TWITTER("twitter", R.drawable.ic_login_social_twitter), GITHUB("github", R.drawable.ic_login_social_github) } \ No newline at end of file diff --git a/app/src/main/java/org/stepik/android/view/auth/ui/activity/CredentialAuthActivity.kt b/app/src/main/java/org/stepik/android/view/auth/ui/activity/CredentialAuthActivity.kt index 068051829e..39bf319669 100644 --- a/app/src/main/java/org/stepik/android/view/auth/ui/activity/CredentialAuthActivity.kt +++ b/app/src/main/java/org/stepik/android/view/auth/ui/activity/CredentialAuthActivity.kt @@ -12,6 +12,8 @@ import android.view.inputmethod.EditorInfo import androidx.activity.viewModels import androidx.appcompat.app.AppCompatDelegate import androidx.core.content.res.ResourcesCompat +import androidx.core.view.WindowCompat +import androidx.core.view.WindowInsetsCompat import androidx.core.view.isVisible import androidx.fragment.app.DialogFragment import androidx.lifecycle.ViewModelProvider @@ -24,13 +26,13 @@ import org.stepic.droid.databinding.ActivityAuthCredentialBinding import org.stepic.droid.model.Credentials import org.stepic.droid.ui.activities.SmartLockActivityBase import org.stepic.droid.ui.dialogs.LoadingProgressDialogFragment -import org.stepic.droid.ui.util.setOnKeyboardOpenListener import org.stepic.droid.util.ProgressHelper import org.stepic.droid.util.toBundle import org.stepik.android.domain.auth.model.LoginFailType import org.stepik.android.model.Course import org.stepik.android.presentation.auth.CredentialAuthPresenter import org.stepik.android.presentation.auth.CredentialAuthView +import org.stepik.android.view.auth.extension.addAuthKeyboardInsetsAnimation import org.stepik.android.view.auth.extension.getMessageFor import org.stepik.android.view.auth.model.AutoAuth import org.stepik.android.view.base.ui.span.TypefaceSpanCompat @@ -73,9 +75,11 @@ class CredentialAuthActivity : SmartLockActivityBase(), CredentialAuthView { private val progressDialogFragment: DialogFragment = LoadingProgressDialogFragment.newInstance() + private var keyboardTargetField: View? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + WindowCompat.setDecorFitsSystemWindows(window, false) setContentView(R.layout.activity_auth_credential) injectComponent() @@ -149,13 +153,12 @@ class CredentialAuthActivity : SmartLockActivityBase(), CredentialAuthView { initGoogleApiClient() - setOnKeyboardOpenListener(binding.rootView, { - binding.stepikLogo.isVisible = false - binding.signInText.isVisible = false - }, { - binding.stepikLogo.isVisible = true - binding.signInText.isVisible = true - }) + binding.loginRootView.addAuthKeyboardInsetsAnimation( + contentView = binding.container, + logoView = binding.stepikLogo, + titleView = binding.signInText, + keyboardPinnedView = binding.bottomButtons + ) if (savedInstanceState == null) { setData(intent) @@ -184,7 +187,7 @@ class CredentialAuthActivity : SmartLockActivityBase(), CredentialAuthView { val signInWithPasswordSuffix = getString(R.string.sign_in_with_password_suffix) val spannableSignIn = SpannableString(signInString + signInWithPasswordSuffix) - val typeface = ResourcesCompat.getFont(this, R.font.roboto_medium) + val typeface = ResourcesCompat.getFont(this, R.font.roboto_bold) spannableSignIn.setSpan(TypefaceSpanCompat(typeface), 0, signInString.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) @@ -196,6 +199,13 @@ class CredentialAuthActivity : SmartLockActivityBase(), CredentialAuthView { setData(intent) } + override fun onWindowFocusChanged(hasFocus: Boolean) { + super.onWindowFocusChanged(hasFocus) + if (hasFocus) { + showKeyboardForTargetField() + } + } + private fun setData(intent: Intent) { val autoAuth = intent .getSerializableExtra(EXTRA_AUTO_AUTH) as? AutoAuth @@ -207,16 +217,39 @@ class CredentialAuthActivity : SmartLockActivityBase(), CredentialAuthView { binding.loginField.setText(email) binding.passwordField.setText(password) - when { + val fieldToFocus = when { email == null -> - binding.loginField.requestFocus() + binding.loginField password == null -> - binding.passwordField.requestFocus() + binding.passwordField + + else -> + null } + fieldToFocus?.requestFocus() if (autoAuth != AutoAuth.NONE) { + keyboardTargetField = null submit(autoAuth) + } else { + keyboardTargetField = fieldToFocus + showKeyboardForTargetField() + } + } + + private fun showKeyboardForTargetField() { + val targetField = keyboardTargetField ?: return + if (!window.decorView.hasWindowFocus()) { + return + } + + targetField.post { + targetField.requestFocus() + + keyboardTargetField = null + WindowCompat.getInsetsController(window, targetField) + .show(WindowInsetsCompat.Type.ime()) } } diff --git a/app/src/main/java/org/stepik/android/view/auth/ui/activity/RegistrationActivity.kt b/app/src/main/java/org/stepik/android/view/auth/ui/activity/RegistrationActivity.kt index bd8cf71bf8..ab6bcd386d 100644 --- a/app/src/main/java/org/stepik/android/view/auth/ui/activity/RegistrationActivity.kt +++ b/app/src/main/java/org/stepik/android/view/auth/ui/activity/RegistrationActivity.kt @@ -14,6 +14,7 @@ import android.view.inputmethod.EditorInfo import androidx.activity.viewModels import androidx.appcompat.app.AppCompatDelegate import androidx.core.content.res.ResourcesCompat +import androidx.core.view.WindowCompat import androidx.core.view.isVisible import androidx.core.widget.CompoundButtonCompat import androidx.fragment.app.DialogFragment @@ -26,7 +27,6 @@ import org.stepic.droid.base.App import org.stepic.droid.databinding.ActivityRegistrationBinding import org.stepic.droid.ui.activities.SmartLockActivityBase import org.stepic.droid.ui.dialogs.LoadingProgressDialogFragment -import org.stepic.droid.ui.util.setOnKeyboardOpenListener import org.stepic.droid.ui.util.snackbar import org.stepic.droid.util.ProgressHelper import org.stepic.droid.util.ValidatorUtil @@ -41,6 +41,7 @@ import org.stepik.android.model.Course import org.stepik.android.model.user.RegistrationCredentials import org.stepik.android.presentation.auth.RegistrationPresenter import org.stepik.android.presentation.auth.RegistrationView +import org.stepik.android.view.auth.extension.addAuthKeyboardInsetsAnimation import org.stepik.android.view.auth.model.AutoAuth import org.stepik.android.view.base.ui.span.TypefaceSpanCompat import ru.nobird.android.view.base.ui.extension.hideKeyboard @@ -97,6 +98,7 @@ class RegistrationActivity : SmartLockActivityBase(), RegistrationView { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + WindowCompat.setDecorFitsSystemWindows(window, false) setContentView(R.layout.activity_registration) injectComponent() @@ -187,13 +189,11 @@ class RegistrationActivity : SmartLockActivityBase(), RegistrationView { setSignUpButtonState() - setOnKeyboardOpenListener(binding.rootView, { - binding.stepikLogo.isVisible = false - binding.signUpText.isVisible = false - }, { - binding.stepikLogo.isVisible = true - binding.signUpText.isVisible = true - }) + binding.registerRootView.addAuthKeyboardInsetsAnimation( + contentView = binding.container, + logoView = binding.stepikLogo, + titleView = binding.signUpText + ) } private fun injectComponent() { @@ -225,7 +225,7 @@ class RegistrationActivity : SmartLockActivityBase(), RegistrationView { val signUpSuffix = getString(R.string.sign_up_with_email_suffix) val spannableSignIn = SpannableString(signUpString + signUpSuffix) - val typeface = ResourcesCompat.getFont(this, R.font.roboto_medium) + val typeface = ResourcesCompat.getFont(this, R.font.roboto_bold) spannableSignIn.setSpan(TypefaceSpanCompat(typeface), 0, signUpString.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) diff --git a/app/src/main/java/org/stepik/android/view/auth/ui/activity/SocialAuthActivity.kt b/app/src/main/java/org/stepik/android/view/auth/ui/activity/SocialAuthActivity.kt index 93b5e319c2..786a352292 100644 --- a/app/src/main/java/org/stepik/android/view/auth/ui/activity/SocialAuthActivity.kt +++ b/app/src/main/java/org/stepik/android/view/auth/ui/activity/SocialAuthActivity.kt @@ -11,7 +11,7 @@ import androidx.core.content.res.ResourcesCompat import androidx.core.view.isVisible import androidx.fragment.app.DialogFragment import androidx.lifecycle.ViewModelProvider -import androidx.recyclerview.widget.GridLayoutManager +import androidx.recyclerview.widget.LinearLayoutManager import dev.androidbroadcast.vbpd.viewBinding import com.google.android.gms.auth.api.Auth import com.google.android.gms.common.api.GoogleApiClient @@ -20,7 +20,6 @@ import com.vk.api.sdk.auth.VKAccessToken import com.vk.api.sdk.auth.VKAuthCallback import com.vk.api.sdk.auth.VKScope import com.vk.api.sdk.exceptions.VKApiCodes -import jp.wasabeef.recyclerview.animators.FadeInDownAnimator import org.stepic.droid.R import org.stepic.droid.analytic.Analytic import org.stepic.droid.analytic.experiments.DeferredAuthSplitTest @@ -55,7 +54,6 @@ class SocialAuthActivity : SmartLockActivityBase(), SocialAuthView, SocialAuthCo companion object { private const val REQUEST_CODE_GOOGLE_SIGN_IN = 7007 - private const val KEY_SOCIAL_ADAPTER_STATE = "social_adapter_state_key" private const val KEY_SELECTED_SOCIAL_TYPE = "selected_social_type" private const val EXTRA_WAS_LOGOUT_KEY = "wasLogoutKey" @@ -135,12 +133,7 @@ class SocialAuthActivity : SmartLockActivityBase(), SocialAuthView, SocialAuthCo initGoogleApiClient(true) { showNetworkError() } - val recyclerState = savedInstanceState?.getSerializable(KEY_SOCIAL_ADAPTER_STATE) - if (recyclerState is SocialAuthAdapter.State) { - initSocialRecycler(recyclerState) - } else { - initSocialRecycler() - } + initSocialRecycler() selectedSocialType = savedInstanceState?.getSerializable(KEY_SELECTED_SOCIAL_TYPE) as? SocialNetwork @@ -148,7 +141,7 @@ class SocialAuthActivity : SmartLockActivityBase(), SocialAuthView, SocialAuthCo val signInWithSocial = getString(R.string.sign_in_with_social_suffix) val spannableSignIn = SpannableString(signInString + signInWithSocial) - val typefaceSpan = TypefaceSpanCompat(ResourcesCompat.getFont(this, R.font.roboto_medium)) + val typefaceSpan = TypefaceSpanCompat(ResourcesCompat.getFont(this, R.font.roboto_bold)) spannableSignIn.setSpan(typefaceSpan, 0, signInString.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) @@ -208,31 +201,10 @@ class SocialAuthActivity : SmartLockActivityBase(), SocialAuthView, SocialAuthCo super.onPause() } - private fun initSocialRecycler(state: SocialAuthAdapter.State = SocialAuthAdapter.State.NORMAL) { - binding.socialListRecyclerView.layoutManager = GridLayoutManager(this, 3) - - binding.socialListRecyclerView.itemAnimator = FadeInDownAnimator() - .apply { - removeDuration = 0 - } - - val adapter = SocialAuthAdapter(this::onSocialItemClicked, state) - binding.showMore.setOnClickListener { - binding.showMore.isVisible = false - binding.showLess.isVisible = true - adapter.showMore() - } - - binding.showLess.setOnClickListener { - binding.showLess.isVisible = false - binding.showMore.isVisible = true - adapter.showLess() - } + private fun initSocialRecycler() { + binding.socialListRecyclerView.layoutManager = LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false) - binding.showLess.isVisible = state == SocialAuthAdapter.State.EXPANDED - binding.showMore.isVisible = state == SocialAuthAdapter.State.NORMAL - - binding.socialListRecyclerView.adapter = adapter + binding.socialListRecyclerView.adapter = SocialAuthAdapter(this::onSocialItemClicked) } private fun onSocialItemClicked(type: SocialNetwork) { @@ -391,10 +363,6 @@ class SocialAuthActivity : SmartLockActivityBase(), SocialAuthView, SocialAuthCo } override fun onSaveInstanceState(outState: Bundle) { - val adapter = binding.socialListRecyclerView.adapter - if (adapter is SocialAuthAdapter) { - outState.putSerializable(KEY_SOCIAL_ADAPTER_STATE, adapter.state) - } selectedSocialType?.let { outState.putSerializable(KEY_SELECTED_SOCIAL_TYPE, it) } super.onSaveInstanceState(outState) } diff --git a/app/src/main/java/org/stepik/android/view/auth/ui/view/AuthScrollView.kt b/app/src/main/java/org/stepik/android/view/auth/ui/view/AuthScrollView.kt new file mode 100644 index 0000000000..e950e91dc7 --- /dev/null +++ b/app/src/main/java/org/stepik/android/view/auth/ui/view/AuthScrollView.kt @@ -0,0 +1,23 @@ +package org.stepik.android.view.auth.ui.view + +import android.content.Context +import android.graphics.Rect +import android.util.AttributeSet +import android.widget.ScrollView + +class AuthScrollView +@JvmOverloads +constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : ScrollView(context, attrs, defStyleAttr) { + var isFocusScrollEnabled = true + + override fun computeScrollDeltaToGetChildRectOnScreen(rect: Rect): Int = + if (isFocusScrollEnabled) { + super.computeScrollDeltaToGetChildRectOnScreen(rect) + } else { + 0 + } +} diff --git a/app/src/main/java/org/stepik/android/view/streak/notification/StreakNotificationDelegate.kt b/app/src/main/java/org/stepik/android/view/streak/notification/StreakNotificationDelegate.kt index 5d7599686e..ad2bdedb11 100644 --- a/app/src/main/java/org/stepik/android/view/streak/notification/StreakNotificationDelegate.kt +++ b/app/src/main/java/org/stepik/android/view/streak/notification/StreakNotificationDelegate.kt @@ -9,7 +9,6 @@ import org.stepic.droid.core.ScreenManager import org.stepic.droid.preferences.SharedPreferenceHelper import org.stepic.droid.util.AppConstants import org.stepic.droid.util.DateTimeHelper -import org.stepic.droid.util.StepikUtil import org.stepik.android.domain.base.analytic.BUNDLEABLE_ANALYTIC_EVENT import org.stepik.android.domain.base.analytic.toBundle import org.stepik.android.domain.streak.analytic.StreakNotificationClicked @@ -36,7 +35,7 @@ constructor( private val sharedPreferenceHelper: SharedPreferenceHelper, private val notificationHelper: NotificationHelper, stepikNotificationManager: StepikNotificationManager -) : NotificationDelegate("show_streak_notification", stepikNotificationManager) { +) : NotificationDelegate("show_streak_notification", stepikNotificationManager), StreakNotificationScheduler { companion object { const val STREAK_NOTIFICATION_CLICKED = "streak_notification_clicked" private const val STREAK_NOTIFICATION_ID = 3214L @@ -48,23 +47,35 @@ constructor( val numberOfStreakNotifications = sharedPreferenceHelper.numberOfStreakNotifications if (numberOfStreakNotifications < AppConstants.MAX_NUMBER_OF_NOTIFICATION_STREAK) { try { - val pins: ArrayList = userActivityRepository.getUserActivities(sharedPreferenceHelper.profile?.id ?: throw Exception("User is not auth")) + val userId = sharedPreferenceHelper.profile?.id + ?: throw Exception("User is not auth") + val userActivitySummary = userActivityRepository + .getUserActivitySummary(userId) .blockingGet() - .firstOrNull() - ?.pins!! - val (currentStreak, isSolvedToday) = StepikUtil.getCurrentStreakExtended(pins) - if (currentStreak <= 0) { - analytic.reportEvent(Analytic.Streak.GET_ZERO_STREAK_NOTIFICATION) - showNotificationWithoutStreakInfo(StreakNotificationType.ZERO) - } else { - // if current streak is > 0 -> streaks works! -> continue send it - // it will reset before sending, after sending it will be incremented - sharedPreferenceHelper.resetNumberOfStreakNotifications() - if (isSolvedToday) { - showNotificationStreakImprovement(currentStreak) - } else { - showNotificationWithStreakCallToAction(currentStreak) + + val notificationType = getStreakNotificationType( + userActivitySummary.recentStrike, + userActivitySummary.solvedToday + ) + + when (notificationType) { + StreakNotificationType.ZERO -> { + analytic.reportEvent(Analytic.Streak.GET_ZERO_STREAK_NOTIFICATION) + showNotificationWithoutStreakInfo(StreakNotificationType.ZERO) } + + StreakNotificationType.SOLVED_TODAY -> { + sharedPreferenceHelper.resetNumberOfStreakNotifications() + showNotificationStreakImprovement(userActivitySummary.recentStrike) + } + + StreakNotificationType.NOT_SOLVED_TODAY -> { + sharedPreferenceHelper.resetNumberOfStreakNotifications() + showNotificationWithStreakCallToAction(userActivitySummary.recentStrike) + } + + StreakNotificationType.NO_INTERNET -> + Unit } } catch (exception: Exception) { // no internet || cant get streaks -> show some notification without streak information. @@ -81,7 +92,7 @@ constructor( } } - fun scheduleStreakNotification() { + override fun scheduleStreakNotification() { if (sharedPreferenceHelper.isStreakNotificationEnabled) { // plan new alarm val hour = sharedPreferenceHelper.timeNotificationCode @@ -157,4 +168,14 @@ constructor( .createStreakNotificationIntent(context, StreakNotificationDismissed(notificationType.type).toBundle()) return PendingIntentCompat.getBroadcast(context, 0, deleteIntent, PendingIntent.FLAG_CANCEL_CURRENT) } -} \ No newline at end of file +} + +internal fun getStreakNotificationType(recentStrike: Int, solvedToday: Int): StreakNotificationType = + when { + recentStrike <= 0 -> + StreakNotificationType.ZERO + solvedToday > 0 -> + StreakNotificationType.SOLVED_TODAY + else -> + StreakNotificationType.NOT_SOLVED_TODAY + } diff --git a/app/src/main/java/org/stepik/android/view/streak/notification/StreakNotificationScheduler.kt b/app/src/main/java/org/stepik/android/view/streak/notification/StreakNotificationScheduler.kt new file mode 100644 index 0000000000..63a1c68e3c --- /dev/null +++ b/app/src/main/java/org/stepik/android/view/streak/notification/StreakNotificationScheduler.kt @@ -0,0 +1,5 @@ +package org.stepik.android.view.streak.notification + +interface StreakNotificationScheduler { + fun scheduleStreakNotification() +} diff --git a/app/src/main/res/drawable/ic_login_social_twitter.xml b/app/src/main/res/drawable/ic_login_social_twitter.xml deleted file mode 100644 index b415802f33..0000000000 --- a/app/src/main/res/drawable/ic_login_social_twitter.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_login_social_vk.xml b/app/src/main/res/drawable/ic_login_social_vk.xml index 537a2fa567..6c7ea7dfe4 100644 --- a/app/src/main/res/drawable/ic_login_social_vk.xml +++ b/app/src/main/res/drawable/ic_login_social_vk.xml @@ -1,6 +1,15 @@ - - + + + + diff --git a/app/src/main/res/layout/activity_auth_credential.xml b/app/src/main/res/layout/activity_auth_credential.xml index 7eee035810..6f89e1836a 100644 --- a/app/src/main/res/layout/activity_auth_credential.xml +++ b/app/src/main/res/layout/activity_auth_credential.xml @@ -1,5 +1,5 @@ - + app:layout_constraintTop_toBottomOf="@id/stepikLogo" + app:layout_constraintBottom_toTopOf="@id/bottomButtons" + app:layout_constraintVertical_bias="0" + android:layout_marginTop="32dp" + android:layout_marginBottom="16dp"> @@ -185,4 +185,4 @@ - \ No newline at end of file + diff --git a/app/src/main/res/layout/activity_auth_social.xml b/app/src/main/res/layout/activity_auth_social.xml index 4273f96c62..929d9785d6 100644 --- a/app/src/main/res/layout/activity_auth_social.xml +++ b/app/src/main/res/layout/activity_auth_social.xml @@ -44,6 +44,16 @@ android:focusableInTouchMode="false" android:src="@drawable/ic_stepik_logotype_square_black" /> + + + app:layout_constraintTop_toBottomOf="@id/authTopSpacing"> @@ -114,31 +120,11 @@ android:layout_height="wrap_content" android:overScrollMode="never" android:layout_gravity="center_horizontal" + android:layout_marginTop="8dp" tools:listitem="@layout/item_social" - app:layoutManager="androidx.recyclerview.widget.GridLayoutManager" tools:itemCount="3" /> -