diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 2a3e44b21e..dd1dd015b7 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -361,6 +361,16 @@ + + + + + + diff --git a/app/src/main/java/org/stepic/droid/adaptive/ui/fragments/AdaptiveRatingFragment.kt b/app/src/main/java/org/stepic/droid/adaptive/ui/fragments/AdaptiveRatingFragment.kt index c82432a8d7..6698c5c839 100644 --- a/app/src/main/java/org/stepic/droid/adaptive/ui/fragments/AdaptiveRatingFragment.kt +++ b/app/src/main/java/org/stepic/droid/adaptive/ui/fragments/AdaptiveRatingFragment.kt @@ -13,6 +13,7 @@ import kotlinx.android.synthetic.main.fragment_adaptive_rating.* import kotlinx.android.synthetic.main.error_no_connection_with_button.* import org.stepic.droid.R import org.stepic.droid.adaptive.ui.adapters.AdaptiveRatingAdapter +import org.stepic.droid.analytic.AmplitudeAnalytic import org.stepic.droid.base.App import org.stepic.droid.base.FragmentBase import org.stepic.droid.core.presenters.AdaptiveRatingPresenter @@ -67,6 +68,13 @@ class AdaptiveRatingFragment: FragmentBase(), AdaptiveRatingView { tryAgain.setOnClickListener { adaptiveRatingPresenter.retry() } } + override fun setUserVisibleHint(isVisibleToUser: Boolean) { + super.setUserVisibleHint(isVisibleToUser) + if (isVisibleToUser) { + analytic.reportAmplitudeEvent(AmplitudeAnalytic.Adaptive.RATING_OPENED, mapOf(AmplitudeAnalytic.Adaptive.Params.COURSE to courseId.toString())) + } + } + override fun onLoading() { error.visibility = View.GONE progress.visibility = View.VISIBLE diff --git a/app/src/main/java/org/stepic/droid/analytic/AmplitudeAnalytic.kt b/app/src/main/java/org/stepic/droid/analytic/AmplitudeAnalytic.kt index ffced7f890..5bdbcbc8c0 100644 --- a/app/src/main/java/org/stepic/droid/analytic/AmplitudeAnalytic.kt +++ b/app/src/main/java/org/stepic/droid/analytic/AmplitudeAnalytic.kt @@ -2,13 +2,13 @@ package org.stepic.droid.analytic interface AmplitudeAnalytic { object Properties { - const val STEPIK_ID = "stepik_id" - const val SUBMISSIONS_COUNT = "submissions_count" - const val COURSES_COUNT = "courses_count" - const val SCREEN_ORIENTATION = "screen_orientation" - const val APPLICATION_ID = "application_id" - const val PUSH_PERMISSION = "push_permission" - const val STREAKS_NOTIFICATIONS_ENABLED = "streaks_notifications_enabled" + const val STEPIK_ID = "stepik_id" + const val SUBMISSIONS_COUNT = "submissions_count" + const val COURSES_COUNT = "courses_count" + const val SCREEN_ORIENTATION = "screen_orientation" + const val APPLICATION_ID = "application_id" + const val PUSH_PERMISSION = "push_permission" + const val STREAKS_NOTIFICATIONS_ENABLED = "streaks_notifications_enabled" } object Launch { @@ -137,4 +137,17 @@ interface AmplitudeAnalytic { const val LEVEL = "achievement_level" } } + + object ProfileEdit { + const val SCREEN_OPENED = "Profile edit screen opened" + const val SAVED = "Profile edit saved" + } + + object Adaptive { + const val RATING_OPENED = "Adaptive rating opened" + + object Params { + const val COURSE = "course" + } + } } \ No newline at end of file diff --git a/app/src/main/java/org/stepic/droid/analytic/experiments/CommentsTooltipSplitTest.kt b/app/src/main/java/org/stepic/droid/analytic/experiments/CommentsTooltipSplitTest.kt new file mode 100644 index 0000000000..1c897ed455 --- /dev/null +++ b/app/src/main/java/org/stepic/droid/analytic/experiments/CommentsTooltipSplitTest.kt @@ -0,0 +1,25 @@ +package org.stepic.droid.analytic.experiments + +import org.stepic.droid.analytic.Analytic +import org.stepic.droid.preferences.SharedPreferenceHelper +import javax.inject.Inject + +class CommentsTooltipSplitTest +@Inject +constructor( + analytics: Analytic, + sharedPreferenceHelper: SharedPreferenceHelper +) : SplitTest( + analytics, + sharedPreferenceHelper, + + name = "comments", + groups = Group.values() +) { + enum class Group( + val isCommentsToolTipEnabled: Boolean + ) : SplitTest.Group { + Control(isCommentsToolTipEnabled = false), + TooltipEnabled(isCommentsToolTipEnabled = true) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/stepic/droid/base/StepBaseFragment.java b/app/src/main/java/org/stepic/droid/base/StepBaseFragment.java index 4338cfb157..de39d8e343 100644 --- a/app/src/main/java/org/stepic/droid/base/StepBaseFragment.java +++ b/app/src/main/java/org/stepic/droid/base/StepBaseFragment.java @@ -3,6 +3,8 @@ import android.os.Bundle; import android.support.annotation.Nullable; import android.support.v4.app.DialogFragment; +import android.support.v4.widget.NestedScrollView; +import android.view.Gravity; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; @@ -14,36 +16,50 @@ import org.stepic.droid.analytic.Analytic; import org.stepic.droid.core.commentcount.contract.CommentCountListener; import org.stepic.droid.core.presenters.AnonymousPresenter; +import org.stepic.droid.core.presenters.CommentsBannerPresenter; import org.stepic.droid.core.presenters.RouteStepPresenter; import org.stepic.droid.core.presenters.contracts.AnonymousView; +import org.stepic.droid.core.presenters.contracts.CommentsView; import org.stepic.droid.core.presenters.contracts.RouteStepView; import org.stepic.droid.persistence.model.StepPersistentWrapper; -import org.stepic.droid.ui.custom.StepTextWrapper; -import org.stepik.android.model.Lesson; -import org.stepik.android.model.Section; -import org.stepik.android.model.Step; -import org.stepik.android.model.Unit; import org.stepic.droid.storage.operations.DatabaseFacade; +import org.stepic.droid.ui.custom.StepTextWrapper; import org.stepic.droid.ui.dialogs.LoadingProgressDialogFragment; import org.stepic.droid.ui.dialogs.StepShareDialogFragment; +import org.stepic.droid.ui.util.PopupHelper; import org.stepic.droid.util.AppConstants; +import org.stepic.droid.util.DisplayUtils; import org.stepic.droid.util.ProgressHelper; import org.stepic.droid.web.StepResponse; +import org.stepik.android.model.Lesson; +import org.stepik.android.model.Section; +import org.stepik.android.model.Step; +import org.stepik.android.model.Unit; +import org.stepik.android.view.ui.listener.FragmentViewPagerScrollStateListener; import java.lang.ref.WeakReference; import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; import javax.inject.Inject; import butterknife.BindView; +import io.reactivex.Completable; +import io.reactivex.Observable; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.CompositeDisposable; +import io.reactivex.subjects.BehaviorSubject; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; +import static org.stepic.droid.util.RxUtilKt.zip; public abstract class StepBaseFragment extends FragmentBase implements RouteStepView, AnonymousView, - CommentCountListener { + CommentsView, + CommentCountListener, + FragmentViewPagerScrollStateListener { @BindView(R.id.open_comments_text) protected TextView textForComment; @@ -69,6 +85,10 @@ public abstract class StepBaseFragment extends FragmentBase @BindView(R.id.previous_lesson_view) protected View previousLessonView; + @BindView(R.id.rootScrollView) + @Nullable + protected NestedScrollView nestedScrollView; + protected StepPersistentWrapper stepWrapper; protected Step step; protected Lesson lesson; @@ -92,9 +112,20 @@ public abstract class StepBaseFragment extends FragmentBase @Inject AnonymousPresenter anonymousPresenter; + @Inject + CommentsBannerPresenter commentsBannerPresenter; + @Inject Client commentCountListenerClient; + private CompositeDisposable uiCompositeDisposable = new CompositeDisposable(); + + private BehaviorSubject fragmentVisibilitySubject = + BehaviorSubject.create(); + + private BehaviorSubject commentsVisibilitySubject = + BehaviorSubject.createDefault(false); + @Override protected void injectComponent() { App.Companion @@ -133,6 +164,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { commentCountListenerClient.subscribe(this); routeStepPresenter.attachView(this); anonymousPresenter.attachView(this); + commentsBannerPresenter.attachView(this); anonymousPresenter.checkForAnonymous(); if (unit != null) { nextLessonView.setOnClickListener(new View.OnClickListener() { @@ -152,11 +184,36 @@ public void onClick(View view) { routeStepPresenter.checkStepForFirst(step.getId(), lesson, unit); routeStepPresenter.checkStepForLast(step.getId(), lesson, unit); } - } protected abstract void attachStepTextWrapper(); protected abstract void detachStepTextWrapper(); + @Override + public void showCommentsBanner() { + Observable visibilityObservable = + fragmentVisibilitySubject.filter(state -> state == ScrollState.ACTIVE); + + Observable commentsObservable = + commentsVisibilitySubject.filter(isVisible -> isVisible); + + uiCompositeDisposable.add(zip(visibilityObservable, commentsObservable) + .firstElement() + .ignoreElement() + .subscribe(() -> { + View view = nestedScrollView.findViewById(R.id.open_comments_text); + PopupHelper.INSTANCE.showPopupAnchoredToView( + getContext(), + view, + getString(R.string.step_comment_tooltip), + PopupHelper.PopupTheme.DARK_ABOVE, + true, + Gravity.TOP, + true, + true + ); + commentsBannerPresenter.onBannerShown(section.getCourse()); + })); + } @Override public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { @@ -168,6 +225,22 @@ public void onClick(View v) { getScreenManager().showLaunchScreen(getActivity()); } }); + nestedScrollView.setOnScrollChangeListener(new NestedScrollView.OnScrollChangeListener() { + @Override + public void onScrollChange(NestedScrollView nestedScrollView, int scrollX, int scrollY, int oldScrollX, int oldScrollY) { + if (scrollY == (nestedScrollView.getChildAt(0).getMeasuredHeight() - nestedScrollView.getMeasuredHeight())) { + commentsVisibilitySubject.onNext(DisplayUtils.isVisible(nestedScrollView, textForComment)); + } + } + }); + } + + @Override + public void setUserVisibleHint(boolean isVisibleToUser) { + super.setUserVisibleHint(isVisibleToUser); + if (isVisibleToUser) { + checkCommentsBanner(); + } } private void updateCommentState() { @@ -193,7 +266,6 @@ public void onClick(View v) { } } }); - int discussionCount = step.getDiscussionsCount(); if (discussionCount > 0) { textForComment.setText(App.Companion.getAppContext().getResources().getQuantityString(R.plurals.open_comments, discussionCount, discussionCount)); @@ -231,8 +303,10 @@ public void onDestroyView() { routeStepPresenter.detachView(this); commentCountListenerClient.unsubscribe(this); anonymousPresenter.detachView(this); + commentsBannerPresenter.detachView(this); nextLessonView.setOnClickListener(null); previousLessonView.setOnClickListener(null); + uiCompositeDisposable.clear(); super.onDestroyView(); } @@ -359,4 +433,26 @@ public void onFailure(Call call, Throwable t) { } } + + @Override + public void onViewPagerScrollStateChanged(ScrollState scrollState) { + changeVisibilitySubjects(scrollState); + } + + private void changeVisibilitySubjects(ScrollState scrollState) { + fragmentVisibilitySubject.onNext(scrollState); + if (scrollState == ScrollState.ACTIVE && nestedScrollView != null) { + commentsVisibilitySubject.onNext(DisplayUtils.isVisible(nestedScrollView, textForComment)); + } + } + + private void checkCommentsBanner() { + uiCompositeDisposable.add(Completable.timer(3, TimeUnit.SECONDS, AndroidSchedulers.mainThread()) + .subscribe(() -> { + if (section != null && step.getDiscussionsCount() > 0) { + changeVisibilitySubjects(ScrollState.ACTIVE); + commentsBannerPresenter.fetchCommentsBanner(section.getCourse()); + } + })); + } } diff --git a/app/src/main/java/org/stepic/droid/core/ScreenManager.java b/app/src/main/java/org/stepic/droid/core/ScreenManager.java index 506d66d979..5b84244f7c 100644 --- a/app/src/main/java/org/stepic/droid/core/ScreenManager.java +++ b/app/src/main/java/org/stepic/droid/core/ScreenManager.java @@ -21,6 +21,7 @@ import org.stepic.droid.ui.fragments.CommentsFragment; import org.stepic.droid.web.ViewAssignment; import org.stepik.android.model.Tag; +import org.stepik.android.model.user.Profile; import org.stepik.android.view.course.routing.CourseScreenTab; import org.stepik.android.view.routing.deeplink.BranchRoute; import org.stepik.android.view.video_player.model.VideoPlayerMediaData; @@ -145,4 +146,8 @@ public interface ScreenManager { void showAchievementsList(Context context, long userId, boolean isMyProfile); void openDeepLink(Context context, BranchRoute route); + + void showProfileEdit(Context context); + void showProfileEditInfo(Activity activity, Profile profile); + void showProfileEditPassword(Activity activity, long profileId); } diff --git a/app/src/main/java/org/stepic/droid/core/ScreenManagerImpl.java b/app/src/main/java/org/stepic/droid/core/ScreenManagerImpl.java index 949a9e6827..1e9807c227 100644 --- a/app/src/main/java/org/stepic/droid/core/ScreenManagerImpl.java +++ b/app/src/main/java/org/stepic/droid/core/ScreenManagerImpl.java @@ -28,6 +28,7 @@ import org.stepic.droid.di.AppSingleton; import org.stepic.droid.features.achievements.ui.activity.AchievementsListActivity; import org.stepic.droid.util.UriExtensionsKt; +import org.stepik.android.model.user.Profile; import org.stepik.android.view.course.routing.CourseScreenTab; import org.stepik.android.view.course.ui.activity.CourseActivity; import org.stepic.droid.model.CertificateViewItem; @@ -70,6 +71,9 @@ import org.stepic.droid.util.StringUtil; import org.stepic.droid.web.ViewAssignment; import org.stepik.android.model.Tag; +import org.stepik.android.view.profile_edit.ui.activity.ProfileEditInfoActivity; +import org.stepik.android.view.profile_edit.ui.activity.ProfileEditActivity; +import org.stepik.android.view.profile_edit.ui.activity.ProfileEditPasswordActivity; import org.stepik.android.view.routing.deeplink.BranchDeepLinkRouter; import org.stepik.android.view.routing.deeplink.BranchRoute; import org.stepik.android.view.video_player.model.VideoPlayerMediaData; @@ -699,4 +703,24 @@ public void openDeepLink(Context context, BranchRoute route) { } } } + + @Override + public void showProfileEdit(Context context) { + if (context instanceof Activity) { + ((Activity) context).overridePendingTransition(org.stepic.droid.R.anim.push_up, org.stepic.droid.R.anim.no_transition); + } + context.startActivity(ProfileEditActivity.Companion.createIntent(context)); + } + + @Override + public void showProfileEditInfo(Activity activity, Profile profile) { + activity.overridePendingTransition(org.stepic.droid.R.anim.push_up, org.stepic.droid.R.anim.no_transition); + activity.startActivityForResult(ProfileEditInfoActivity.Companion.createIntent(activity, profile), ProfileEditInfoActivity.REQUEST_CODE); + } + + @Override + public void showProfileEditPassword(Activity activity, long profileId) { + activity.overridePendingTransition(org.stepic.droid.R.anim.push_up, org.stepic.droid.R.anim.no_transition); + activity.startActivityForResult(ProfileEditPasswordActivity.Companion.createIntent(activity, profileId), ProfileEditPasswordActivity.REQUEST_CODE); + } } diff --git a/app/src/main/java/org/stepic/droid/core/presenters/CommentsBannerPresenter.kt b/app/src/main/java/org/stepic/droid/core/presenters/CommentsBannerPresenter.kt new file mode 100644 index 0000000000..0c04c30af2 --- /dev/null +++ b/app/src/main/java/org/stepic/droid/core/presenters/CommentsBannerPresenter.kt @@ -0,0 +1,64 @@ +package org.stepic.droid.core.presenters + +import io.reactivex.Scheduler +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.rxkotlin.plusAssign +import io.reactivex.rxkotlin.subscribeBy +import org.stepic.droid.analytic.experiments.CommentsTooltipSplitTest +import org.stepic.droid.core.presenters.contracts.CommentsView +import org.stepic.droid.di.qualifiers.BackgroundScheduler +import org.stepic.droid.di.qualifiers.MainScheduler +import org.stepic.droid.util.emptyOnErrorStub +import org.stepik.android.domain.comments.interactor.CommentsInteractor +import timber.log.Timber +import javax.inject.Inject + +class CommentsBannerPresenter +@Inject +constructor( + private val commentsBannerInteractor: CommentsInteractor, + private val commentsTooltipSplitTest: CommentsTooltipSplitTest, + + @BackgroundScheduler + private val backgroundScheduler: Scheduler, + @MainScheduler + private val mainScheduler: Scheduler +) : PresenterBase() { + + + private val commentsDisposable = CompositeDisposable() + + fun fetchCommentsBanner(courseId: Long) { + if (!commentsTooltipSplitTest.currentGroup.isCommentsToolTipEnabled) { + return + } + commentsDisposable += commentsBannerInteractor + .shouldShowCommentsBannerForCourse(courseId) + .subscribeOn(backgroundScheduler) + .observeOn(mainScheduler) + .subscribeBy( + onSuccess = { wasCommentsBannerShown -> + if (!wasCommentsBannerShown) { + view?.showCommentsBanner() + } + }, + onError = emptyOnErrorStub + ) + } + + fun onBannerShown(courseId: Long) { + commentsDisposable += commentsBannerInteractor + .onBannerShown(courseId) + .subscribeOn(backgroundScheduler) + .observeOn(mainScheduler) + .subscribeBy( + onComplete = { Timber.d("Complete") }, + onError = emptyOnErrorStub + ) + } + + override fun detachView(view: CommentsView) { + commentsDisposable.clear() + super.detachView(view) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/stepic/droid/core/presenters/ProfilePresenterImpl.kt b/app/src/main/java/org/stepic/droid/core/presenters/ProfilePresenterImpl.kt index c45eb54371..af7aa5a87f 100644 --- a/app/src/main/java/org/stepic/droid/core/presenters/ProfilePresenterImpl.kt +++ b/app/src/main/java/org/stepic/droid/core/presenters/ProfilePresenterImpl.kt @@ -1,10 +1,15 @@ package org.stepic.droid.core.presenters import android.support.annotation.WorkerThread - +import io.reactivex.Observable +import io.reactivex.Scheduler +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.rxkotlin.plusAssign import org.stepic.droid.analytic.Analytic import org.stepic.droid.concurrency.MainHandler import org.stepic.droid.core.ProfilePresenter +import org.stepic.droid.core.presenters.contracts.ProfileView +import org.stepic.droid.di.qualifiers.BackgroundScheduler import org.stepic.droid.model.UserViewModel import org.stepic.droid.preferences.SharedPreferenceHelper import org.stepic.droid.util.StepikUtil @@ -14,12 +19,18 @@ import java.util.concurrent.ThreadPoolExecutor import javax.inject.Inject class ProfilePresenterImpl -@Inject constructor( - private val threadPoolExecutor: ThreadPoolExecutor, - analytic: Analytic, - private val mainHandler: MainHandler, - private val api: Api, - private val sharedPreferences: SharedPreferenceHelper +@Inject +constructor( + private val threadPoolExecutor: ThreadPoolExecutor, + analytic: Analytic, + private val mainHandler: MainHandler, + private val api: Api, + private val sharedPreferences: SharedPreferenceHelper, + + private val profileObservable: Observable, + + @BackgroundScheduler + private val backgroundScheduler: Scheduler ) : ProfilePresenter(analytic) { private var isLoading: Boolean = false //main thread only @@ -28,6 +39,8 @@ class ProfilePresenterImpl private var maxStreak: Int? = null private var haveSolvedToday: Boolean? = null + private val compositeDisposable = CompositeDisposable() + override fun initProfile() { // default params are not allowed for override. // moreover, abstract function with default param is used in Java code @@ -35,6 +48,7 @@ class ProfilePresenterImpl } override fun initProfile(profileId: Long) { + subscribeForProfileUpdates(profileId) if (isLoading) return isLoading = true userViewModel?.let { @@ -87,6 +101,13 @@ class ProfilePresenterImpl } } + private fun subscribeForProfileUpdates(profileId: Long) { + compositeDisposable += profileObservable + .filter { profileId == 0L || it.id == profileId } + .observeOn(backgroundScheduler) + .subscribe(::showLocalProfile) + } + @WorkerThread private fun showInternetProfile(userId: Long) { //1) show profile @@ -185,4 +206,8 @@ class ProfilePresenterImpl return if (source.isBlank()) "" else source } + override fun detachView(view: ProfileView) { + super.detachView(view) + compositeDisposable.clear() + } } \ No newline at end of file diff --git a/app/src/main/java/org/stepic/droid/core/presenters/contracts/CommentsView.kt b/app/src/main/java/org/stepic/droid/core/presenters/contracts/CommentsView.kt new file mode 100644 index 0000000000..350101fca8 --- /dev/null +++ b/app/src/main/java/org/stepic/droid/core/presenters/contracts/CommentsView.kt @@ -0,0 +1,5 @@ +package org.stepic.droid.core.presenters.contracts + +interface CommentsView { + fun showCommentsBanner() +} \ No newline at end of file diff --git a/app/src/main/java/org/stepic/droid/di/AppCoreComponent.kt b/app/src/main/java/org/stepic/droid/di/AppCoreComponent.kt index d7a0639cee..d2d7bb5338 100644 --- a/app/src/main/java/org/stepic/droid/di/AppCoreComponent.kt +++ b/app/src/main/java/org/stepic/droid/di/AppCoreComponent.kt @@ -50,11 +50,12 @@ import org.stepic.droid.ui.custom.* import org.stepic.droid.ui.dialogs.* import org.stepic.droid.ui.fragments.StoreManagementFragment import org.stepic.droid.util.glide.GlideCustomModule -import org.stepic.droid.util.glide.RelativeUrlLoader import org.stepik.android.view.injection.billing.BillingModule import org.stepik.android.view.injection.course.CourseComponent import org.stepik.android.view.injection.course.CourseRoutingModule import org.stepik.android.view.injection.personal_deadlines.PersonalDeadlinesDataModule +import org.stepik.android.view.injection.profile.ProfileBusModule +import org.stepik.android.view.injection.profile_edit.ProfileEditComponent import org.stepik.android.view.injection.progress.ProgressBusModule import org.stepik.android.view.injection.video_player.VideoPlayerComponent @@ -80,6 +81,7 @@ import org.stepik.android.view.injection.video_player.VideoPlayerComponent BillingModule::class, CourseEnrollmentBusModule::class, // todo unite it in BusModule::class + ProfileBusModule::class, ProgressBusModule::class, PersonalDeadlinesDataModule::class, @@ -130,6 +132,8 @@ interface AppCoreComponent { fun videoPlayerComponentBuilder(): VideoPlayerComponent.Builder + fun profileEditComponentBuilder(): ProfileEditComponent.Builder + fun inject(someActivity: FragmentActivityBase) fun inject(adapter: StepikRadioGroupAdapter) diff --git a/app/src/main/java/org/stepic/droid/di/analytic/AnalyticModule.kt b/app/src/main/java/org/stepic/droid/di/analytic/AnalyticModule.kt index d67d353856..11122b6053 100644 --- a/app/src/main/java/org/stepic/droid/di/analytic/AnalyticModule.kt +++ b/app/src/main/java/org/stepic/droid/di/analytic/AnalyticModule.kt @@ -6,6 +6,7 @@ import dagger.multibindings.IntoSet import org.stepic.droid.analytic.Analytic import org.stepic.droid.analytic.AnalyticImpl import org.stepic.droid.analytic.experiments.AchievementsSplitTest +import org.stepic.droid.analytic.experiments.CommentsTooltipSplitTest import org.stepic.droid.analytic.experiments.SplitTest import org.stepic.droid.analytic.experiments.PersonalDeadlinesSplitTest import org.stepic.droid.analytic.experiments.VideoSplitTest @@ -27,6 +28,11 @@ abstract class AnalyticModule { @IntoSet internal abstract fun bindAchievementsSplitTest(achievementsSplitTest: AchievementsSplitTest): SplitTest<*> + @AppSingleton + @Binds + @IntoSet + internal abstract fun bindCommentsSplitTest(commentsTooltipSplitTest: CommentsTooltipSplitTest): SplitTest<*> + @AppSingleton @Binds @IntoSet diff --git a/app/src/main/java/org/stepic/droid/di/network/NetworkModule.kt b/app/src/main/java/org/stepic/droid/di/network/NetworkModule.kt index 2fda373b5d..9dc4799aff 100644 --- a/app/src/main/java/org/stepic/droid/di/network/NetworkModule.kt +++ b/app/src/main/java/org/stepic/droid/di/network/NetworkModule.kt @@ -12,6 +12,8 @@ import org.stepic.droid.web.ApiImpl import org.stepic.droid.web.StepicRestLoggedService import org.stepic.droid.web.achievements.AchievementsService import org.stepic.droid.web.storage.RemoteStorageService +import org.stepik.android.view.injection.base.Authorized +import retrofit2.Retrofit @Module(includes = [NetworkUtilModule::class]) abstract class NetworkModule { @@ -27,16 +29,26 @@ abstract class NetworkModule { @Provides @AppSingleton @JvmStatic - fun provideLoggedService(apiImpl: ApiImpl): StepicRestLoggedService = apiImpl.loggedService + fun provideLoggedService(apiImpl: ApiImpl): StepicRestLoggedService = + apiImpl.loggedService @Provides @AppSingleton @JvmStatic - fun provideRemoteStorageService(apiImpl: ApiImpl): RemoteStorageService = apiImpl.remoteStorageService + fun provideRemoteStorageService(apiImpl: ApiImpl): RemoteStorageService = + apiImpl.remoteStorageService @Provides @AppSingleton @JvmStatic - fun provideAchievementsService(apiImpl: ApiImpl): AchievementsService = apiImpl.achievementsService + fun provideAchievementsService(apiImpl: ApiImpl): AchievementsService = + apiImpl.achievementsService + + @Provides + @AppSingleton + @JvmStatic + @Authorized + fun provideAuhtorizedRetrofit(apiImpl: ApiImpl): Retrofit = + apiImpl.authorizedRetrofit } } \ No newline at end of file diff --git a/app/src/main/java/org/stepic/droid/di/step/StepComponent.kt b/app/src/main/java/org/stepic/droid/di/step/StepComponent.kt index 6e49121c3b..5c6209fc7a 100644 --- a/app/src/main/java/org/stepic/droid/di/step/StepComponent.kt +++ b/app/src/main/java/org/stepic/droid/di/step/StepComponent.kt @@ -7,9 +7,10 @@ import org.stepic.droid.di.step.code.CodeComponent import org.stepic.droid.di.streak.StreakModule import org.stepic.droid.ui.fragments.StepAttemptFragment import org.stepic.droid.ui.fragments.VideoStepFragment +import org.stepik.android.view.injection.comments.CommentsBannerDataModule @StepScope -@Subcomponent(modules = arrayOf(StreakModule::class, CommentCountModule::class)) +@Subcomponent(modules = arrayOf(StreakModule::class, CommentCountModule::class, CommentsBannerDataModule::class)) interface StepComponent { @Subcomponent.Builder interface Builder { diff --git a/app/src/main/java/org/stepic/droid/di/storage/StorageComponent.kt b/app/src/main/java/org/stepic/droid/di/storage/StorageComponent.kt index a1e48f6237..f4fa9c823d 100644 --- a/app/src/main/java/org/stepic/droid/di/storage/StorageComponent.kt +++ b/app/src/main/java/org/stepic/droid/di/storage/StorageComponent.kt @@ -9,6 +9,7 @@ import org.stepic.droid.persistence.storage.dao.PersistentItemDao import org.stepic.droid.persistence.storage.dao.PersistentStateDao import org.stepic.droid.storage.dao.IDao import org.stepic.droid.storage.operations.DatabaseFacade +import org.stepik.android.cache.comments.dao.CommentsBannerDao import org.stepik.android.cache.personal_deadlines.dao.PersonalDeadlinesDao import org.stepik.android.domain.course_reviews.model.CourseReview import org.stepik.android.model.user.User @@ -29,6 +30,7 @@ interface StorageComponent { val deadlinesDao: PersonalDeadlinesDao val deadlinesBannerDao: DeadlinesBannerDao + val commentsBannerDao: CommentsBannerDao val persistentItemDao: PersistentItemDao val persistentStateDao: PersistentStateDao diff --git a/app/src/main/java/org/stepic/droid/di/storage/StorageModule.kt b/app/src/main/java/org/stepic/droid/di/storage/StorageModule.kt index f2301855e9..cf3ec69b5f 100644 --- a/app/src/main/java/org/stepic/droid/di/storage/StorageModule.kt +++ b/app/src/main/java/org/stepic/droid/di/storage/StorageModule.kt @@ -25,6 +25,8 @@ import org.stepic.droid.storage.DatabaseHelper import org.stepic.droid.storage.dao.* import org.stepic.droid.storage.operations.* import org.stepic.droid.web.ViewAssignment +import org.stepik.android.cache.comments.dao.CommentsBannerDao +import org.stepik.android.cache.comments.dao.CommentsBannerDaoImpl import org.stepik.android.cache.user.dao.UserDaoImpl import org.stepik.android.cache.video.dao.VideoEntityDaoImpl import org.stepik.android.cache.video.dao.VideoDao @@ -169,6 +171,10 @@ abstract class StorageModule { @Binds internal abstract fun bindCourseReviewsDao(courseReviewsDaoImpl: CourseReviewsDaoImpl): IDao + @StorageSingleton + @Binds + internal abstract fun provideCommentsBannerDao(commentsBannerDaoImpl: CommentsBannerDaoImpl): CommentsBannerDao + @Module companion object { diff --git a/app/src/main/java/org/stepic/droid/storage/DatabaseHelper.java b/app/src/main/java/org/stepic/droid/storage/DatabaseHelper.java index eadeac6aaf..551cf0e40f 100644 --- a/app/src/main/java/org/stepic/droid/storage/DatabaseHelper.java +++ b/app/src/main/java/org/stepic/droid/storage/DatabaseHelper.java @@ -6,6 +6,7 @@ import org.stepic.droid.storage.migration.MigrationFrom37To38; import org.stepic.droid.storage.migration.MigrationFrom38To39; +import org.stepic.droid.storage.migration.MigrationFrom39To40; import org.stepik.android.cache.personal_deadlines.structure.DbStructureDeadlines; import org.stepik.android.cache.personal_deadlines.structure.DbStructureDeadlinesBanner; import org.stepic.droid.storage.migration.MigrationFrom33To34; @@ -109,6 +110,7 @@ public void onCreate(SQLiteDatabase db) { upgradeFrom36To37(db); upgradeFrom37To38(db); upgradeFrom38To39(db); + upgradeFrom39To40(db); } @@ -307,6 +309,14 @@ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { if (oldVersion < 39) { upgradeFrom38To39(db); } + + if (oldVersion < 40) { + upgradeFrom39To40(db); + } + } + + private void upgradeFrom39To40(SQLiteDatabase db) { + MigrationFrom39To40.INSTANCE.migrate(db); } private void upgradeFrom38To39(SQLiteDatabase db) { diff --git a/app/src/main/java/org/stepic/droid/storage/migration/MigrationFrom39To40.kt b/app/src/main/java/org/stepic/droid/storage/migration/MigrationFrom39To40.kt new file mode 100644 index 0000000000..6f1fd3582b --- /dev/null +++ b/app/src/main/java/org/stepic/droid/storage/migration/MigrationFrom39To40.kt @@ -0,0 +1,10 @@ +package org.stepic.droid.storage.migration + +import android.database.sqlite.SQLiteDatabase +import org.stepik.android.cache.comments.structure.DbStructureCommentsBanner + +object MigrationFrom39To40 : Migration { + override fun migrate(db: SQLiteDatabase) { + DbStructureCommentsBanner.createTable(db) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/stepic/droid/storage/structure/DatabaseInfo.kt b/app/src/main/java/org/stepic/droid/storage/structure/DatabaseInfo.kt index 8e7dab0f7c..bc6c1555c0 100644 --- a/app/src/main/java/org/stepic/droid/storage/structure/DatabaseInfo.kt +++ b/app/src/main/java/org/stepic/droid/storage/structure/DatabaseInfo.kt @@ -2,5 +2,5 @@ package org.stepic.droid.storage.structure object DatabaseInfo { const val FILE_NAME = "stepic_database.db" - const val VERSION = 39 + const val VERSION = 40 } diff --git a/app/src/main/java/org/stepic/droid/ui/adapters/StepFragmentAdapter.kt b/app/src/main/java/org/stepic/droid/ui/adapters/StepFragmentAdapter.kt index aee50b922e..f3a3f79863 100644 --- a/app/src/main/java/org/stepic/droid/ui/adapters/StepFragmentAdapter.kt +++ b/app/src/main/java/org/stepic/droid/ui/adapters/StepFragmentAdapter.kt @@ -5,19 +5,25 @@ import android.os.Bundle import android.support.v4.app.Fragment import android.support.v4.app.FragmentManager import android.support.v4.app.FragmentStatePagerAdapter +import android.view.ViewGroup import org.stepic.droid.persistence.model.StepPersistentWrapper import org.stepik.android.model.Lesson import org.stepik.android.model.Section import org.stepik.android.model.Unit import org.stepic.droid.util.AppConstants import org.stepic.droid.util.resolvers.StepTypeResolver +import org.stepik.android.view.fragment_pager.ActiveFragmentPagerAdapter -class StepFragmentAdapter(fm: FragmentManager, val stepList: List, val stepTypeResolver: StepTypeResolver) : FragmentStatePagerAdapter(fm) { +class StepFragmentAdapter(fm: FragmentManager, val stepList: List, val stepTypeResolver: StepTypeResolver) : FragmentStatePagerAdapter(fm), + ActiveFragmentPagerAdapter { private var lesson: Lesson? = null private var unit: Unit? = null private var section: Section? = null + private val _activeFragments = mutableMapOf() + override val activeFragments: Map + get() = _activeFragments @JvmOverloads fun setDataIfNotNull(outLesson: Lesson? = null, outUnit: Unit? = null, outSection: Section? = null) { @@ -51,6 +57,18 @@ class StepFragmentAdapter(fm: FragmentManager, val stepList: List _activeFragments[position] = fragment } + } + + override fun destroyItem(container: ViewGroup, position: Int, `object`: Any) { + _activeFragments.remove(position) + super.destroyItem(container, position, `object`) + } + fun getTabDrawable(position: Int): Drawable? { if (position >= stepList.size) return null val step = stepList[position]?.step diff --git a/app/src/main/java/org/stepic/droid/ui/custom/LatexSupportableWebView.java b/app/src/main/java/org/stepic/droid/ui/custom/LatexSupportableWebView.java index 73e54867f8..7d47eb1dd4 100644 --- a/app/src/main/java/org/stepic/droid/ui/custom/LatexSupportableWebView.java +++ b/app/src/main/java/org/stepic/droid/ui/custom/LatexSupportableWebView.java @@ -89,6 +89,9 @@ public boolean onLongClick(View v) { webSettings.setDomStorageEnabled(true); webSettings.setJavaScriptEnabled(true); webSettings.setDefaultFontSize((int) textSize); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + webSettings.setMediaPlaybackRequiresUserGesture(false); + } addJavascriptInterface(new OnScrollWebListener(), HtmlHelper.HORIZONTAL_SCROLL_LISTENER); } diff --git a/app/src/main/java/org/stepic/droid/ui/fragments/LessonFragment.java b/app/src/main/java/org/stepic/droid/ui/fragments/LessonFragment.java index 71d69b89e4..8e4554807a 100644 --- a/app/src/main/java/org/stepic/droid/ui/fragments/LessonFragment.java +++ b/app/src/main/java/org/stepic/droid/ui/fragments/LessonFragment.java @@ -49,6 +49,7 @@ import org.stepic.droid.util.resolvers.StepHelper; import org.stepic.droid.util.resolvers.StepTypeResolver; import org.stepic.droid.web.ViewAssignment; +import org.stepik.android.view.fragment_pager.FragmentDelegateScrollStateChangeListener; import java.util.HashMap; import java.util.Map; @@ -206,6 +207,7 @@ public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { initIndependentUI(); stepAdapter = new StepFragmentAdapter(getChildFragmentManager(), stepsPresenter.getStepList(), stepTypeResolver); viewPager.setAdapter(stepAdapter); + viewPager.addOnPageChangeListener(new FragmentDelegateScrollStateChangeListener(viewPager, stepAdapter)); stepsPresenter.attachView(this); stepTrackingPresenter.attachView(this); if (lesson == null) { diff --git a/app/src/main/java/org/stepic/droid/ui/fragments/ProfileFragment.kt b/app/src/main/java/org/stepic/droid/ui/fragments/ProfileFragment.kt index 66b70c235f..255c3c00f0 100644 --- a/app/src/main/java/org/stepic/droid/ui/fragments/ProfileFragment.kt +++ b/app/src/main/java/org/stepic/droid/ui/fragments/ProfileFragment.kt @@ -23,6 +23,7 @@ import kotlinx.android.synthetic.main.fragment_profile_new.* import kotlinx.android.synthetic.main.latex_supportabe_enhanced_view.view.* import kotlinx.android.synthetic.main.view_notification_interval_chooser.* import org.stepic.droid.R +import org.stepic.droid.analytic.AmplitudeAnalytic import org.stepic.droid.analytic.Analytic import org.stepic.droid.base.App import org.stepic.droid.base.FragmentBase @@ -353,10 +354,9 @@ class ProfileFragment : FragmentBase(), } with(userViewModel) { + shortBioInfoContainer.changeVisibility(shortBio.isNotBlank() || information.isNotBlank()) + shortBioSecondHeader.changeVisibility(shortBio.isNotBlank() && information.isNotBlank()) when { - shortBio.isBlank() && information.isBlank() -> - shortBioInfoContainer.visibility = View.GONE //do not show any header - shortBio.isBlank() && information.isNotBlank() -> shortBioFirstHeader.setText(R.string.user_info) //show header with 'information' @@ -365,7 +365,6 @@ class ProfileFragment : FragmentBase(), shortBio.isNotBlank() && information.isNotBlank() -> { //show general header and all info shortBioFirstHeader.setText(R.string.short_bio_and_info) - shortBioSecondHeader.visibility = View.VISIBLE shortBioSecondHeader.setText(R.string.user_info) } } @@ -374,6 +373,7 @@ class ProfileFragment : FragmentBase(), shortBioFirstText.visibility = View.GONE } else { shortBioFirstText.text = shortBio.trim() + shortBioFirstText.visibility = View.VISIBLE } if (information.isBlank()) { @@ -381,6 +381,7 @@ class ProfileFragment : FragmentBase(), } else { shortBioSecondText.setPlainOrLaTeXTextWithCustomFontColored( information, fontsProvider.provideFontPath(FontType.light), R.color.new_accent_color, false) + shortBioSecondText.visibility = View.VISIBLE } } } @@ -457,7 +458,10 @@ class ProfileFragment : FragmentBase(), override fun onCreateOptionsMenu(menu: Menu?, inflater: MenuInflater) { if (localUserViewModel != null) { - inflater.inflate(R.menu.share_menu, menu) + inflater.inflate(R.menu.profile_menu, menu) + + menu?.findItem(R.id.menu_item_edit)?.isVisible = + localUserViewModel?.isMyProfile == true } } @@ -467,6 +471,11 @@ class ProfileFragment : FragmentBase(), shareProfile() return true } + R.id.menu_item_edit -> { + analytic.reportAmplitudeEvent(AmplitudeAnalytic.ProfileEdit.SCREEN_OPENED) + screenManager.showProfileEdit(context) + return true + } } return false } diff --git a/app/src/main/java/org/stepic/droid/ui/fragments/TextStepFragment.kt b/app/src/main/java/org/stepic/droid/ui/fragments/TextStepFragment.kt index c7a928b540..debe0b4b17 100644 --- a/app/src/main/java/org/stepic/droid/ui/fragments/TextStepFragment.kt +++ b/app/src/main/java/org/stepic/droid/ui/fragments/TextStepFragment.kt @@ -1,6 +1,7 @@ package org.stepic.droid.ui.fragments import android.os.Bundle +import android.os.Handler import android.view.LayoutInflater import android.view.View import android.view.ViewGroup diff --git a/app/src/main/java/org/stepic/droid/ui/util/PopupHelper.kt b/app/src/main/java/org/stepic/droid/ui/util/PopupHelper.kt index 4183eb1eb7..e32125f200 100644 --- a/app/src/main/java/org/stepic/droid/ui/util/PopupHelper.kt +++ b/app/src/main/java/org/stepic/droid/ui/util/PopupHelper.kt @@ -7,6 +7,7 @@ import android.support.v4.widget.PopupWindowCompat import android.view.Gravity import android.view.LayoutInflater import android.view.View +import android.view.ViewTreeObserver import android.widget.LinearLayout import android.widget.PopupWindow import org.stepic.droid.R @@ -22,7 +23,9 @@ object PopupHelper { val backgroundRes: Int ) { DARK(R.drawable.popup_arrow_up, R.drawable.background_popup), - LIGHT(R.drawable.popup_arrow_up_light, R.drawable.background_popup_light) + LIGHT(R.drawable.popup_arrow_up_light, R.drawable.background_popup_light), + DARK_ABOVE(R.drawable.popup_arrow_down, R.drawable.background_popup), + LIGHT_ABOVE(R.drawable.popup_arrow_down_light, R.drawable.background_popup_light) } private fun calcArrowHorizontalOffset(anchorView: View, popupView: View, arrowView: View): Float { @@ -40,12 +43,13 @@ object PopupHelper { popupText: String, theme: PopupTheme = PopupTheme.DARK, cancelableOnTouchOutside: Boolean = false, gravity: Int = Gravity.CENTER, - withArrow: Boolean = false + withArrow: Boolean = false, + isAboveAnchor: Boolean = false ): PopupWindow? { anchorView ?: return null val inflater = context.getSystemService(LAYOUT_INFLATER_SERVICE) as LayoutInflater - val popupView = inflater.inflate(R.layout.popup_window, null) + val popupView = inflatePopupWindow(inflater, withArrow, isAboveAnchor) val popupTextView = popupView.popupText val popupArrowView = popupView.arrowView @@ -56,16 +60,33 @@ object PopupHelper { popupArrowView.setBackgroundResource(theme.arrowRes) popupArrowView.changeVisibility(withArrow) - if (withArrow) { - popupView.viewTreeObserver.addOnGlobalLayoutListener { - popupArrowView.x = calcArrowHorizontalOffset(anchorView, popupView, popupView.arrowView) - } - } - val popupWindow = PopupWindow(popupView, LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT) popupWindow.animationStyle = R.style.PopupAnimations popupWindow.isOutsideTouchable = cancelableOnTouchOutside + if (withArrow) { + popupView.viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener { + var offsetY = 0 + var measuredHeight = -(anchorView.measuredHeight + popupView.measuredHeight) + override fun onGlobalLayout() { + popupArrowView.x = calcArrowHorizontalOffset(anchorView, popupView, popupView.arrowView) + measuredHeight = -(anchorView.measuredHeight + popupView.measuredHeight) + if (isAboveAnchor) { + if (offsetY != measuredHeight) { + offsetY = measuredHeight + popupWindow.update( + anchorView, + 0, + offsetY, + popupWindow.width, + popupWindow.height + ) + } + } + } + }) + } + popupView.setOnClickListener { popupWindow.dismiss() } @@ -82,4 +103,11 @@ object PopupHelper { return popupWindow } + + private fun inflatePopupWindow(inflater: LayoutInflater, withArrow: Boolean, isAboveAnchor: Boolean): View = + if (withArrow && isAboveAnchor) { + inflater.inflate(R.layout.popup_window_down, null) + } else { + inflater.inflate(R.layout.popup_window, null) + } } \ No newline at end of file diff --git a/app/src/main/java/org/stepic/droid/util/DisplayUtils.java b/app/src/main/java/org/stepic/droid/util/DisplayUtils.java index a5705ee1ec..9ff5681e15 100644 --- a/app/src/main/java/org/stepic/droid/util/DisplayUtils.java +++ b/app/src/main/java/org/stepic/droid/util/DisplayUtils.java @@ -2,6 +2,9 @@ import android.content.Context; import android.content.res.Resources; +import android.graphics.Rect; +import android.support.v4.widget.NestedScrollView; +import android.view.View; public class DisplayUtils { // A method to find height of the status bar @@ -17,4 +20,18 @@ public static int getStatusBarHeight(Context context) { public static int getScreenHeight() { return Resources.getSystem().getDisplayMetrics().heightPixels; } + + public static int getScreenWidth() { + return Resources.getSystem().getDisplayMetrics().widthPixels; + } + + public static boolean isVisible(NestedScrollView scrollView, View view) { + Rect scrollBounds = new Rect(); + scrollView.getDrawingRect(scrollBounds); + + float top = view.getY(); + float bottom = top + view.getHeight(); + + return scrollBounds.top <= top && scrollBounds.bottom >= bottom; + } } diff --git a/app/src/main/java/org/stepic/droid/util/ProgressHelper.java b/app/src/main/java/org/stepic/droid/util/ProgressHelper.java index fd90b78569..3a33995e73 100644 --- a/app/src/main/java/org/stepic/droid/util/ProgressHelper.java +++ b/app/src/main/java/org/stepic/droid/util/ProgressHelper.java @@ -47,8 +47,9 @@ public static void dismiss(LoadingProgressDialog progressDialog) { } public static void activate(DialogFragment progressDialog, FragmentManager fragmentManager, String tag) { - if (progressDialog != null && !progressDialog.isAdded()) + if (progressDialog != null && !progressDialog.isAdded() && fragmentManager.findFragmentByTag(tag) == null) { progressDialog.show(fragmentManager, tag); + } } public static void dismiss(FragmentManager fragmentManager, String tag) { diff --git a/app/src/main/java/org/stepic/droid/web/Api.java b/app/src/main/java/org/stepic/droid/web/Api.java index 98496e5514..286f66b7dd 100644 --- a/app/src/main/java/org/stepic/droid/web/Api.java +++ b/app/src/main/java/org/stepic/droid/web/Api.java @@ -18,6 +18,7 @@ import org.stepik.android.model.Tag; import org.stepik.android.model.Reply; import org.stepik.android.model.user.User; +import org.stepik.android.remote.email_address.model.EmailAddressResponse; import java.util.List; diff --git a/app/src/main/java/org/stepic/droid/web/ApiImpl.java b/app/src/main/java/org/stepic/droid/web/ApiImpl.java index 8fbed9c49f..9c9dda6a87 100644 --- a/app/src/main/java/org/stepic/droid/web/ApiImpl.java +++ b/app/src/main/java/org/stepic/droid/web/ApiImpl.java @@ -69,6 +69,7 @@ import org.stepik.android.model.user.Profile; import org.stepik.android.model.user.User; import org.stepik.android.model.attempts.DatasetWrapper; +import org.stepik.android.remote.email_address.model.EmailAddressResponse; import java.io.IOException; import java.net.HttpCookie; @@ -119,14 +120,15 @@ public class ApiImpl implements Api { private final StepikLogoutManager stepikLogoutManager; private final ScreenManager screenManager; private final UserAgentProvider userAgentProvider; - private final FirebaseRemoteConfig firebaseRemoteConfig; - private StepicRestLoggedService loggedService; + private final StepicRestLoggedService loggedService; private StepicRestOAuthService oAuthService; - private StepicEmptyAuthService stepikEmptyAuthService; - private RemoteStorageService remoteStorageService; - private RatingService ratingService; - private AchievementsService achievementsService; + private final StepicEmptyAuthService stepikEmptyAuthService; + private final RemoteStorageService remoteStorageService; + private final RatingService ratingService; + private final AchievementsService achievementsService; + + private final Retrofit authorizedRetrofit; @Inject public ApiImpl( @@ -146,11 +148,16 @@ public ApiImpl( this.stepikLogoutManager = stepikLogoutManager; this.screenManager = screenManager; this.userAgentProvider = userAgentProvider; - this.firebaseRemoteConfig = firebaseRemoteConfig; this.stethoInterceptor = stethoInterceptor; makeOauthServiceWithNewAuthHeader(this.sharedPreference.isLastTokenSocial() ? TokenType.social : TokenType.loginPassword); - makeLoggedServices(); + + authorizedRetrofit = createAuthorizedRetrofit(config.getBaseUrl()); + + achievementsService = authorizedRetrofit.create(AchievementsService.class); + loggedService = authorizedRetrofit.create(StepicRestLoggedService.class); + remoteStorageService = authorizedRetrofit.create(RemoteStorageService.class); + ratingService = createAuthorizedRetrofit(firebaseRemoteConfig.getString(RemoteConfig.ADAPTIVE_BACKEND_URL)).create(RatingService.class); OkHttpClient.Builder okHttpClient = new OkHttpClient.Builder(); setTimeout(okHttpClient, TIMEOUT_IN_SECONDS); @@ -170,13 +177,6 @@ public ApiImpl( } } - private void makeLoggedServices() { - loggedService = createLoggedService(StepicRestLoggedService.class, config.getBaseUrl()); - remoteStorageService = createLoggedService(RemoteStorageService.class, config.getBaseUrl()); - ratingService = createLoggedService(RatingService.class, firebaseRemoteConfig.getString(RemoteConfig.ADAPTIVE_BACKEND_URL)); - achievementsService = createLoggedService(AchievementsService.class, config.getBaseUrl()); - } - public StepicRestLoggedService getLoggedService() { return loggedService; } @@ -189,7 +189,11 @@ public AchievementsService getAchievementsService() { return achievementsService; } - private T createLoggedService(final Class service, final String host) { + public Retrofit getAuthorizedRetrofit() { + return authorizedRetrofit; + } + + private Retrofit createAuthorizedRetrofit(final String host) { OkHttpClient.Builder okHttpBuilder = new OkHttpClient.Builder(); Interceptor interceptor = new Interceptor() { @Override @@ -320,13 +324,12 @@ public Unit invoke() { okHttpBuilder.addNetworkInterceptor(this.stethoInterceptor); setTimeout(okHttpBuilder, TIMEOUT_IN_SECONDS); OkHttpClient okHttpClient = okHttpBuilder.build(); - Retrofit retrofit = new Retrofit.Builder() + return new Retrofit.Builder() .baseUrl(host) .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) .addConverterFactory(generateGsonFactory()) .client(okHttpClient) .build(); - return retrofit.create(service); } private void makeOauthServiceWithNewAuthHeader(final TokenType type) { diff --git a/app/src/main/java/org/stepic/droid/web/EmailAddressResponse.kt b/app/src/main/java/org/stepic/droid/web/EmailAddressResponse.kt deleted file mode 100644 index f95784fa1a..0000000000 --- a/app/src/main/java/org/stepic/droid/web/EmailAddressResponse.kt +++ /dev/null @@ -1,11 +0,0 @@ -package org.stepic.droid.web - -import com.google.gson.annotations.SerializedName -import org.stepik.android.model.user.EmailAddress -import org.stepik.android.model.Meta - -class EmailAddressResponse( - var meta: Meta?, - @SerializedName("email-addresses") - var emailAddresses: List? -) \ No newline at end of file diff --git a/app/src/main/java/org/stepic/droid/web/StepicRestLoggedService.java b/app/src/main/java/org/stepic/droid/web/StepicRestLoggedService.java index 2438b41c45..37ccfde8bb 100644 --- a/app/src/main/java/org/stepic/droid/web/StepicRestLoggedService.java +++ b/app/src/main/java/org/stepic/droid/web/StepicRestLoggedService.java @@ -8,6 +8,7 @@ import org.stepik.android.remote.course_payments.model.CoursePaymentRequest; import org.stepik.android.remote.course_payments.model.CoursePaymentsResponse; import org.stepik.android.remote.course_reviews.model.CourseReviewsResponse; +import org.stepik.android.remote.email_address.model.EmailAddressResponse; import java.util.List; diff --git a/app/src/main/java/org/stepik/android/cache/comments/CommentsBannerDataCacheSourceImpl.kt b/app/src/main/java/org/stepik/android/cache/comments/CommentsBannerDataCacheSourceImpl.kt new file mode 100644 index 0000000000..06e72ca5a1 --- /dev/null +++ b/app/src/main/java/org/stepik/android/cache/comments/CommentsBannerDataCacheSourceImpl.kt @@ -0,0 +1,29 @@ +package org.stepik.android.cache.comments + +import io.reactivex.Completable +import io.reactivex.Single +import org.stepik.android.cache.comments.dao.CommentsBannerDao +import org.stepik.android.cache.comments.structure.DbStructureCommentsBanner +import org.stepik.android.data.comments.source.CommentsBannerCacheDataSource +import javax.inject.Inject + +class CommentsBannerDataCacheSourceImpl +@Inject +constructor( + private val commentsBannerDao: CommentsBannerDao +) : CommentsBannerCacheDataSource { + override fun addCourseId(courseId: Long): Completable = + Completable.fromAction { + commentsBannerDao.insertOrReplace(courseId) + } + + override fun removeCourseId(courseId: Long): Completable = + Completable.fromAction { + commentsBannerDao.remove(DbStructureCommentsBanner.Columns.COURSE_ID, courseId.toString()) + } + + override fun hasCourseId(courseId: Long): Single = + Single.fromCallable { + commentsBannerDao.isInDb(courseId) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/stepik/android/cache/comments/dao/CommentsBannerDao.java b/app/src/main/java/org/stepik/android/cache/comments/dao/CommentsBannerDao.java new file mode 100644 index 0000000000..8150e37444 --- /dev/null +++ b/app/src/main/java/org/stepik/android/cache/comments/dao/CommentsBannerDao.java @@ -0,0 +1,5 @@ +package org.stepik.android.cache.comments.dao; + +import org.stepic.droid.storage.dao.IDao; + +public interface CommentsBannerDao extends IDao { } diff --git a/app/src/main/java/org/stepik/android/cache/comments/dao/CommentsBannerDaoImpl.kt b/app/src/main/java/org/stepik/android/cache/comments/dao/CommentsBannerDaoImpl.kt new file mode 100644 index 0000000000..378b6f825f --- /dev/null +++ b/app/src/main/java/org/stepik/android/cache/comments/dao/CommentsBannerDaoImpl.kt @@ -0,0 +1,31 @@ +package org.stepik.android.cache.comments.dao + +import android.content.ContentValues +import android.database.Cursor +import org.stepic.droid.storage.dao.DaoBase +import org.stepic.droid.storage.operations.DatabaseOperations +import org.stepik.android.cache.comments.structure.DbStructureCommentsBanner +import javax.inject.Inject + +class CommentsBannerDaoImpl +@Inject +constructor( + databaseOperations: DatabaseOperations +) : DaoBase(databaseOperations), CommentsBannerDao { + override fun getDbName(): String = + DbStructureCommentsBanner.COMMENTS_BANNER + + override fun getDefaultPrimaryColumn(): String = + DbStructureCommentsBanner.Columns.COURSE_ID + + override fun getDefaultPrimaryValue(persistentObject: Long?): String = + persistentObject.toString() + + override fun getContentValues(persistentObject: Long?): ContentValues = + ContentValues().apply { + put(DbStructureCommentsBanner.Columns.COURSE_ID, persistentObject) + } + + override fun parsePersistentObject(cursor: Cursor): Long = + cursor.getLong(cursor.getColumnIndex(DbStructureCommentsBanner.Columns.COURSE_ID)) +} \ No newline at end of file diff --git a/app/src/main/java/org/stepik/android/cache/comments/structure/DbStructureCommentsBanner.kt b/app/src/main/java/org/stepik/android/cache/comments/structure/DbStructureCommentsBanner.kt new file mode 100644 index 0000000000..a47c7254c7 --- /dev/null +++ b/app/src/main/java/org/stepik/android/cache/comments/structure/DbStructureCommentsBanner.kt @@ -0,0 +1,19 @@ +package org.stepik.android.cache.comments.structure + +import android.database.sqlite.SQLiteDatabase + +object DbStructureCommentsBanner { + const val COMMENTS_BANNER = "comments_banner" + + object Columns { + const val COURSE_ID = "course_id" + } + + fun createTable(db: SQLiteDatabase) { + val sql = """ + CREATE TABLE IF NOT EXISTS $COMMENTS_BANNER ( + ${Columns.COURSE_ID} LONG PRIMARY KEY + )""".trimIndent() + db.execSQL(sql) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/stepik/android/cache/profile/ProfileCacheDataSourceImpl.kt b/app/src/main/java/org/stepik/android/cache/profile/ProfileCacheDataSourceImpl.kt new file mode 100644 index 0000000000..f3050914b0 --- /dev/null +++ b/app/src/main/java/org/stepik/android/cache/profile/ProfileCacheDataSourceImpl.kt @@ -0,0 +1,24 @@ +package org.stepik.android.cache.profile + +import io.reactivex.Completable +import io.reactivex.Maybe +import org.stepic.droid.preferences.SharedPreferenceHelper +import org.stepik.android.data.profile.source.ProfileCacheDataSource +import org.stepik.android.model.user.Profile +import javax.inject.Inject + +class ProfileCacheDataSourceImpl +@Inject +constructor( + private val sharedPreferenceHelper: SharedPreferenceHelper +) : ProfileCacheDataSource { + override fun getProfile(): Maybe = + sharedPreferenceHelper.profile + ?.let { Maybe.just(it) } + ?: Maybe.empty() + + override fun saveProfile(profile: Profile): Completable = + Completable.fromAction { + sharedPreferenceHelper.storeProfile(profile) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/stepik/android/data/comments/repository/CommentsBannerRepositoryImpl.kt b/app/src/main/java/org/stepik/android/data/comments/repository/CommentsBannerRepositoryImpl.kt new file mode 100644 index 0000000000..a3128e86ae --- /dev/null +++ b/app/src/main/java/org/stepik/android/data/comments/repository/CommentsBannerRepositoryImpl.kt @@ -0,0 +1,22 @@ +package org.stepik.android.data.comments.repository + +import io.reactivex.Completable +import io.reactivex.Single +import org.stepik.android.data.comments.source.CommentsBannerCacheDataSource +import org.stepik.android.domain.comments.repository.CommentsBannerRepository +import javax.inject.Inject + +class CommentsBannerRepositoryImpl +@Inject +constructor( + private val commentsBannerCacheDataSource: CommentsBannerCacheDataSource +) : CommentsBannerRepository { + override fun addCourseId(courseId: Long): Completable = + commentsBannerCacheDataSource.addCourseId(courseId) + + override fun removeCourseId(courseId: Long): Completable = + commentsBannerCacheDataSource.removeCourseId(courseId) + + override fun hasCourseId(courseId: Long): Single = + commentsBannerCacheDataSource.hasCourseId(courseId) +} \ No newline at end of file diff --git a/app/src/main/java/org/stepik/android/data/comments/source/CommentsBannerCacheDataSource.kt b/app/src/main/java/org/stepik/android/data/comments/source/CommentsBannerCacheDataSource.kt new file mode 100644 index 0000000000..2d21ce2f69 --- /dev/null +++ b/app/src/main/java/org/stepik/android/data/comments/source/CommentsBannerCacheDataSource.kt @@ -0,0 +1,10 @@ +package org.stepik.android.data.comments.source + +import io.reactivex.Completable +import io.reactivex.Single + +interface CommentsBannerCacheDataSource { + fun addCourseId(courseId: Long): Completable + fun removeCourseId(courseId: Long): Completable + fun hasCourseId(courseId: Long): Single +} \ No newline at end of file diff --git a/app/src/main/java/org/stepik/android/data/email_address/repository/EmailAddressRepositoryImpl.kt b/app/src/main/java/org/stepik/android/data/email_address/repository/EmailAddressRepositoryImpl.kt new file mode 100644 index 0000000000..1fb36e88b1 --- /dev/null +++ b/app/src/main/java/org/stepik/android/data/email_address/repository/EmailAddressRepositoryImpl.kt @@ -0,0 +1,60 @@ +package org.stepik.android.data.email_address.repository + +import io.reactivex.Completable +import io.reactivex.Single +import org.stepic.droid.util.doCompletableOnSuccess +import org.stepic.droid.util.requireSize +import org.stepik.android.data.email_address.source.EmailAddressCacheDataSource +import org.stepik.android.data.email_address.source.EmailAddressRemoteDataSource +import org.stepik.android.domain.base.DataSourceType +import org.stepik.android.domain.email_address.repository.EmailAddressRepository +import org.stepik.android.model.user.EmailAddress +import javax.inject.Inject + +class EmailAddressRepositoryImpl +@Inject +constructor( + private val remoteDataSource: EmailAddressRemoteDataSource, + private val cacheDataSource: EmailAddressCacheDataSource +) : EmailAddressRepository { + + override fun getEmailAddresses(vararg emailIds: Long, primarySourceType: DataSourceType): Single> { + val remoteSource = remoteDataSource + .getEmailAddresses(*emailIds) + .doCompletableOnSuccess(cacheDataSource::saveEmailAddresses) + + val cacheSource = cacheDataSource + .getEmailAddresses(*emailIds) + + return when (primarySourceType) { + DataSourceType.REMOTE -> + remoteSource.onErrorResumeNext(cacheSource.requireSize(emailIds.size)) + + DataSourceType.CACHE -> + cacheSource.flatMap { cachedEmails -> + val ids = (emailIds.toList() - cachedEmails.map(EmailAddress::id)).toLongArray() + remoteDataSource + .getEmailAddresses(*ids) + .doCompletableOnSuccess(cacheDataSource::saveEmailAddresses) + .map { remoteEmails -> cachedEmails + remoteEmails } + } + + else -> + throw IllegalArgumentException("Unsupported source type = $primarySourceType") + }.map { emailAddresses -> emailAddresses.sortedBy { emailIds.indexOf(it.id) } } + } + + override fun createEmailAddress(emailAddress: EmailAddress): Single = + remoteDataSource + .createEmailAddress(emailAddress) + .doCompletableOnSuccess(cacheDataSource::saveEmailAddress) + + override fun setPrimaryEmailAddress(emailId: Long): Completable = + remoteDataSource + .setPrimaryEmailAddress(emailId) + + override fun removeEmailAddress(emailId: Long): Completable = + remoteDataSource + .removeEmailAddress(emailId) + .andThen(cacheDataSource.removeEmailAddress(emailId)) +} \ No newline at end of file diff --git a/app/src/main/java/org/stepik/android/data/email_address/source/EmailAddressCacheDataSource.kt b/app/src/main/java/org/stepik/android/data/email_address/source/EmailAddressCacheDataSource.kt new file mode 100644 index 0000000000..48ee67b906 --- /dev/null +++ b/app/src/main/java/org/stepik/android/data/email_address/source/EmailAddressCacheDataSource.kt @@ -0,0 +1,16 @@ +package org.stepik.android.data.email_address.source + +import io.reactivex.Completable +import io.reactivex.Single +import org.stepik.android.model.user.EmailAddress + +interface EmailAddressCacheDataSource { + fun getEmailAddresses(vararg emailIds: Long): Single> + + fun saveEmailAddress(emailAddress: EmailAddress): Completable = + saveEmailAddresses(listOf(emailAddress)) + + fun saveEmailAddresses(emailAddresses: List): Completable + + fun removeEmailAddress(emailId: Long): Completable +} \ No newline at end of file diff --git a/app/src/main/java/org/stepik/android/data/email_address/source/EmailAddressRemoteDataSource.kt b/app/src/main/java/org/stepik/android/data/email_address/source/EmailAddressRemoteDataSource.kt new file mode 100644 index 0000000000..50c9ca824c --- /dev/null +++ b/app/src/main/java/org/stepik/android/data/email_address/source/EmailAddressRemoteDataSource.kt @@ -0,0 +1,15 @@ +package org.stepik.android.data.email_address.source + +import io.reactivex.Completable +import io.reactivex.Single +import org.stepik.android.model.user.EmailAddress + +interface EmailAddressRemoteDataSource { + fun getEmailAddresses(vararg emailIds: Long): Single> + + fun createEmailAddress(emailAddress: EmailAddress): Single + + fun setPrimaryEmailAddress(emailId: Long): Completable + + fun removeEmailAddress(emailId: Long): Completable +} \ No newline at end of file diff --git a/app/src/main/java/org/stepik/android/data/profile/repository/ProfileRepositoryImpl.kt b/app/src/main/java/org/stepik/android/data/profile/repository/ProfileRepositoryImpl.kt new file mode 100644 index 0000000000..39ecefd7e4 --- /dev/null +++ b/app/src/main/java/org/stepik/android/data/profile/repository/ProfileRepositoryImpl.kt @@ -0,0 +1,48 @@ +package org.stepik.android.data.profile.repository + +import io.reactivex.Completable +import io.reactivex.Single +import org.stepic.droid.util.doCompletableOnSuccess +import org.stepik.android.data.profile.source.ProfileCacheDataSource +import org.stepik.android.data.profile.source.ProfileRemoteDataSource +import org.stepik.android.domain.base.DataSourceType +import org.stepik.android.domain.profile.repository.ProfileRepository +import org.stepik.android.model.user.Profile +import javax.inject.Inject + +class ProfileRepositoryImpl +@Inject +constructor( + private val profileRemoteDataSource: ProfileRemoteDataSource, + private val profileCacheDataSource: ProfileCacheDataSource +) : ProfileRepository { + + override fun getProfile(primarySourceType: DataSourceType): Single { + val remoteSource = profileRemoteDataSource + .getProfile() + .doCompletableOnSuccess(profileCacheDataSource::saveProfile) + + val cacheSource = profileCacheDataSource + .getProfile() + + return when (primarySourceType) { + DataSourceType.REMOTE -> + remoteSource.onErrorResumeNext(cacheSource.toSingle()) + + DataSourceType.CACHE -> + cacheSource.switchIfEmpty(remoteSource) + + else -> + throw IllegalArgumentException("Unsupported source type = $primarySourceType") + } + } + + override fun saveProfile(profile: Profile): Single = + profileRemoteDataSource + .saveProfile(profile) + .doCompletableOnSuccess(profileCacheDataSource::saveProfile) + + override fun saveProfilePassword(profileId: Long, currentPassword: String, newPassword: String): Completable = + profileRemoteDataSource + .saveProfilePassword(profileId, currentPassword, newPassword) +} \ No newline at end of file diff --git a/app/src/main/java/org/stepik/android/data/profile/source/ProfileCacheDataSource.kt b/app/src/main/java/org/stepik/android/data/profile/source/ProfileCacheDataSource.kt new file mode 100644 index 0000000000..23b1bd7e30 --- /dev/null +++ b/app/src/main/java/org/stepik/android/data/profile/source/ProfileCacheDataSource.kt @@ -0,0 +1,17 @@ +package org.stepik.android.data.profile.source + +import io.reactivex.Completable +import io.reactivex.Maybe +import org.stepik.android.model.user.Profile + +interface ProfileCacheDataSource { + /** + * Returns current profile + */ + fun getProfile(): Maybe + + /** + * Updates profile data + */ + fun saveProfile(profile: Profile): Completable +} \ No newline at end of file diff --git a/app/src/main/java/org/stepik/android/data/profile/source/ProfileRemoteDataSource.kt b/app/src/main/java/org/stepik/android/data/profile/source/ProfileRemoteDataSource.kt new file mode 100644 index 0000000000..e80425ac27 --- /dev/null +++ b/app/src/main/java/org/stepik/android/data/profile/source/ProfileRemoteDataSource.kt @@ -0,0 +1,22 @@ +package org.stepik.android.data.profile.source + +import io.reactivex.Completable +import io.reactivex.Single +import org.stepik.android.model.user.Profile + +interface ProfileRemoteDataSource { + /** + * Returns current profile + */ + fun getProfile(): Single + + /** + * Updates profile data + */ + fun saveProfile(profile: Profile): Single + + /** + * Updates profile password + */ + fun saveProfilePassword(profileId: Long, currentPassword: String, newPassword: String): Completable +} \ No newline at end of file diff --git a/app/src/main/java/org/stepik/android/domain/comments/interactor/CommentsInteractor.kt b/app/src/main/java/org/stepik/android/domain/comments/interactor/CommentsInteractor.kt new file mode 100644 index 0000000000..5bdeb3bf21 --- /dev/null +++ b/app/src/main/java/org/stepik/android/domain/comments/interactor/CommentsInteractor.kt @@ -0,0 +1,18 @@ +package org.stepik.android.domain.comments.interactor + +import io.reactivex.Completable +import io.reactivex.Single +import org.stepik.android.domain.comments.repository.CommentsBannerRepository +import javax.inject.Inject + +class CommentsInteractor +@Inject +constructor( + private val commentsBannerRepository: CommentsBannerRepository +) { + fun shouldShowCommentsBannerForCourse(courseId: Long): Single = + commentsBannerRepository.hasCourseId(courseId) + + fun onBannerShown(courseId: Long): Completable = + commentsBannerRepository.addCourseId(courseId) +} \ No newline at end of file diff --git a/app/src/main/java/org/stepik/android/domain/comments/repository/CommentsBannerRepository.kt b/app/src/main/java/org/stepik/android/domain/comments/repository/CommentsBannerRepository.kt new file mode 100644 index 0000000000..fcd0902d39 --- /dev/null +++ b/app/src/main/java/org/stepik/android/domain/comments/repository/CommentsBannerRepository.kt @@ -0,0 +1,10 @@ +package org.stepik.android.domain.comments.repository + +import io.reactivex.Completable +import io.reactivex.Single + +interface CommentsBannerRepository { + fun addCourseId(courseId: Long): Completable + fun removeCourseId(courseId: Long): Completable + fun hasCourseId(courseId: Long): Single +} \ No newline at end of file diff --git a/app/src/main/java/org/stepik/android/domain/email_address/repository/EmailAddressRepository.kt b/app/src/main/java/org/stepik/android/domain/email_address/repository/EmailAddressRepository.kt new file mode 100644 index 0000000000..12fd38a70d --- /dev/null +++ b/app/src/main/java/org/stepik/android/domain/email_address/repository/EmailAddressRepository.kt @@ -0,0 +1,28 @@ +package org.stepik.android.domain.email_address.repository + +import io.reactivex.Completable +import io.reactivex.Single +import org.stepik.android.domain.base.DataSourceType +import org.stepik.android.model.user.EmailAddress + +interface EmailAddressRepository { + /** + * Returns email addresses with given ids from primary and secondary data sources + */ + fun getEmailAddresses(vararg emailIds: Long, primarySourceType: DataSourceType = DataSourceType.CACHE): Single> + + /** + * Creates new email address and returns created email address object + */ + fun createEmailAddress(emailAddress: EmailAddress): Single + + /** + * Marks email address with given emailId as primary + */ + fun setPrimaryEmailAddress(emailId: Long): Completable + + /** + * Removes given email address + */ + fun removeEmailAddress(emailId: Long): Completable +} \ No newline at end of file diff --git a/app/src/main/java/org/stepik/android/domain/profile/repository/ProfileRepository.kt b/app/src/main/java/org/stepik/android/domain/profile/repository/ProfileRepository.kt new file mode 100644 index 0000000000..7f9e2f70a9 --- /dev/null +++ b/app/src/main/java/org/stepik/android/domain/profile/repository/ProfileRepository.kt @@ -0,0 +1,23 @@ +package org.stepik.android.domain.profile.repository + +import io.reactivex.Completable +import io.reactivex.Single +import org.stepik.android.domain.base.DataSourceType +import org.stepik.android.model.user.Profile + +interface ProfileRepository { + /** + * Returns current profile + */ + fun getProfile(primarySourceType: DataSourceType = DataSourceType.CACHE): Single + + /** + * Updates profile data + */ + fun saveProfile(profile: Profile): Single + + /** + * Updates profile password + */ + fun saveProfilePassword(profileId: Long, currentPassword: String, newPassword: String): Completable +} \ No newline at end of file diff --git a/app/src/main/java/org/stepik/android/domain/profile_edit/ProfileEditInteractor.kt b/app/src/main/java/org/stepik/android/domain/profile_edit/ProfileEditInteractor.kt new file mode 100644 index 0000000000..dd461b1986 --- /dev/null +++ b/app/src/main/java/org/stepik/android/domain/profile_edit/ProfileEditInteractor.kt @@ -0,0 +1,30 @@ +package org.stepik.android.domain.profile_edit + +import io.reactivex.Completable +import io.reactivex.Single +import io.reactivex.subjects.PublishSubject +import org.stepik.android.domain.base.DataSourceType +import org.stepik.android.domain.profile.repository.ProfileRepository +import org.stepik.android.model.user.Profile +import javax.inject.Inject + +class ProfileEditInteractor +@Inject +constructor( + private val profileRepository: ProfileRepository, + private val profileSubject: PublishSubject +) { + fun getProfile(): Single = + profileRepository + .getProfile(primarySourceType = DataSourceType.CACHE) + + fun updateProfile(profile: Profile): Completable = + profileRepository + .saveProfile(profile) + .doOnSuccess(profileSubject::onNext) + .ignoreElement() + + fun updateProfilePassword(profileId: Long, currentPassword: String, newPassword: String): Completable = + profileRepository + .saveProfilePassword(profileId, currentPassword, newPassword) +} \ No newline at end of file diff --git a/app/src/main/java/org/stepik/android/presentation/profile_edit/ProfileEditInfoPresenter.kt b/app/src/main/java/org/stepik/android/presentation/profile_edit/ProfileEditInfoPresenter.kt new file mode 100644 index 0000000000..30a54b7e67 --- /dev/null +++ b/app/src/main/java/org/stepik/android/presentation/profile_edit/ProfileEditInfoPresenter.kt @@ -0,0 +1,83 @@ +package org.stepik.android.presentation.profile_edit + +import io.reactivex.Scheduler +import io.reactivex.rxkotlin.plusAssign +import io.reactivex.rxkotlin.subscribeBy +import org.stepic.droid.analytic.AmplitudeAnalytic +import org.stepic.droid.analytic.Analytic +import org.stepic.droid.di.qualifiers.BackgroundScheduler +import org.stepic.droid.di.qualifiers.MainScheduler +import org.stepik.android.domain.profile_edit.ProfileEditInteractor +import org.stepik.android.model.user.Profile +import org.stepik.android.presentation.base.PresenterBase +import retrofit2.HttpException +import javax.inject.Inject + +class ProfileEditInfoPresenter +@Inject +constructor( + private val analytic: Analytic, + private val profileEditInteractor: ProfileEditInteractor, + + @BackgroundScheduler + private val backgroundScheduler: Scheduler, + @MainScheduler + private val mainScheduler: Scheduler +) : PresenterBase() { + private var state = ProfileEditInfoView.State.IDLE + set(value) { + field = value + view?.setState(state) + } + + override fun attachView(view: ProfileEditInfoView) { + super.attachView(view) + view.setState(state) + } + + fun updateProfileInfo( + profile: Profile, + firstName: String, + lastName: String, + shortBio: String, + details: String + ) { + if (state != ProfileEditInfoView.State.IDLE) return + + if (profile.firstName == firstName && + profile.lastName == lastName && + profile.shortBio == shortBio && + profile.details == details + ) { + state = ProfileEditInfoView.State.COMPLETE + return + } + + state = ProfileEditInfoView.State.LOADING + val newProfile = profile + .copy( + firstName = firstName, + lastName = lastName, + shortBio = shortBio, + details = details + ) + compositeDisposable += profileEditInteractor + .updateProfile(newProfile) + .observeOn(mainScheduler) + .subscribeOn(backgroundScheduler) + .subscribeBy( + onComplete = { + state = ProfileEditInfoView.State.COMPLETE + analytic.reportAmplitudeEvent(AmplitudeAnalytic.ProfileEdit.SAVED) + }, + onError = { + state = ProfileEditInfoView.State.IDLE + if (it is HttpException) { + view?.showInfoError() + } else { + view?.showNetworkError() + } + } + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/stepik/android/presentation/profile_edit/ProfileEditInfoView.kt b/app/src/main/java/org/stepik/android/presentation/profile_edit/ProfileEditInfoView.kt new file mode 100644 index 0000000000..92e7e8e0c5 --- /dev/null +++ b/app/src/main/java/org/stepik/android/presentation/profile_edit/ProfileEditInfoView.kt @@ -0,0 +1,11 @@ +package org.stepik.android.presentation.profile_edit + +interface ProfileEditInfoView { + enum class State { + IDLE, LOADING, COMPLETE + } + + fun setState(state: State) + fun showInfoError() + fun showNetworkError() +} \ No newline at end of file diff --git a/app/src/main/java/org/stepik/android/presentation/profile_edit/ProfileEditPasswordPresenter.kt b/app/src/main/java/org/stepik/android/presentation/profile_edit/ProfileEditPasswordPresenter.kt new file mode 100644 index 0000000000..c7377bbbcd --- /dev/null +++ b/app/src/main/java/org/stepik/android/presentation/profile_edit/ProfileEditPasswordPresenter.kt @@ -0,0 +1,54 @@ +package org.stepik.android.presentation.profile_edit + +import io.reactivex.Scheduler +import io.reactivex.rxkotlin.plusAssign +import io.reactivex.rxkotlin.subscribeBy +import org.stepic.droid.di.qualifiers.BackgroundScheduler +import org.stepic.droid.di.qualifiers.MainScheduler +import org.stepik.android.domain.profile_edit.ProfileEditInteractor +import org.stepik.android.presentation.base.PresenterBase +import retrofit2.HttpException +import javax.inject.Inject + +class ProfileEditPasswordPresenter +@Inject +constructor( + private val profileEditInteractor: ProfileEditInteractor, + + @BackgroundScheduler + private val backgroundScheduler: Scheduler, + @MainScheduler + private val mainScheduler: Scheduler +) : PresenterBase() { + private var state = ProfileEditPasswordView.State.IDLE + set(value) { + field = value + view?.setState(state) + } + + override fun attachView(view: ProfileEditPasswordView) { + super.attachView(view) + view.setState(state) + } + + fun updateProfilePassword(profileId: Long, currentPassword: String, newPassword: String) { + if (state != ProfileEditPasswordView.State.IDLE) return + + state = ProfileEditPasswordView.State.LOADING + compositeDisposable += profileEditInteractor + .updateProfilePassword(profileId, currentPassword, newPassword) + .observeOn(mainScheduler) + .subscribeOn(backgroundScheduler) + .subscribeBy( + onComplete = { state = ProfileEditPasswordView.State.COMPLETE }, + onError = { + state = ProfileEditPasswordView.State.IDLE + if (it is HttpException) { + view?.showPasswordError() + } else { + view?.showNetworkError() + } + } + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/stepik/android/presentation/profile_edit/ProfileEditPasswordView.kt b/app/src/main/java/org/stepik/android/presentation/profile_edit/ProfileEditPasswordView.kt new file mode 100644 index 0000000000..609a7cf91c --- /dev/null +++ b/app/src/main/java/org/stepik/android/presentation/profile_edit/ProfileEditPasswordView.kt @@ -0,0 +1,11 @@ +package org.stepik.android.presentation.profile_edit + +interface ProfileEditPasswordView { + enum class State { + IDLE, LOADING, COMPLETE + } + + fun setState(state: State) + fun showPasswordError() + fun showNetworkError() +} \ No newline at end of file diff --git a/app/src/main/java/org/stepik/android/presentation/profile_edit/ProfileEditPresenter.kt b/app/src/main/java/org/stepik/android/presentation/profile_edit/ProfileEditPresenter.kt new file mode 100644 index 0000000000..ff417a40ad --- /dev/null +++ b/app/src/main/java/org/stepik/android/presentation/profile_edit/ProfileEditPresenter.kt @@ -0,0 +1,55 @@ +package org.stepik.android.presentation.profile_edit + +import io.reactivex.Observable +import io.reactivex.Scheduler +import io.reactivex.rxkotlin.plusAssign +import io.reactivex.rxkotlin.subscribeBy +import org.stepic.droid.di.qualifiers.BackgroundScheduler +import org.stepic.droid.di.qualifiers.MainScheduler +import org.stepik.android.domain.profile_edit.ProfileEditInteractor +import org.stepik.android.model.user.Profile +import org.stepik.android.presentation.base.PresenterBase +import javax.inject.Inject + +class ProfileEditPresenter +@Inject +constructor( + profileEditInteractor: ProfileEditInteractor, + profileObservable: Observable, + + @BackgroundScheduler + backgroundScheduler: Scheduler, + @MainScheduler + mainScheduler: Scheduler +) : PresenterBase() { + private var state: ProfileEditView.State = ProfileEditView.State.Idle + set(value) { + field = value + view?.setState(state) + } + + init { + compositeDisposable += profileEditInteractor + .getProfile() + .subscribeOn(backgroundScheduler) + .observeOn(mainScheduler) + .subscribeBy( + onSuccess = { state = ProfileEditView.State.ProfileLoaded(it) }, + onError = { state = ProfileEditView.State.Error } + ) + + compositeDisposable += profileObservable + .subscribeOn(backgroundScheduler) + .observeOn(mainScheduler) + .subscribeBy( + onNext = { + state = ProfileEditView.State.ProfileLoaded(it) + } + ) + } + + override fun attachView(view: ProfileEditView) { + super.attachView(view) + view.setState(state) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/stepik/android/presentation/profile_edit/ProfileEditView.kt b/app/src/main/java/org/stepik/android/presentation/profile_edit/ProfileEditView.kt new file mode 100644 index 0000000000..5b0d256ba7 --- /dev/null +++ b/app/src/main/java/org/stepik/android/presentation/profile_edit/ProfileEditView.kt @@ -0,0 +1,14 @@ +package org.stepik.android.presentation.profile_edit + +import org.stepik.android.model.user.Profile + +interface ProfileEditView { + sealed class State { + object Idle : State() + object Error : State() + object Loading : State() + class ProfileLoaded(val profile: Profile) : State() + } + + fun setState(state: State) +} \ No newline at end of file diff --git a/app/src/main/java/org/stepik/android/remote/email_address/EmailAddressRemoteDataSourceImpl.kt b/app/src/main/java/org/stepik/android/remote/email_address/EmailAddressRemoteDataSourceImpl.kt new file mode 100644 index 0000000000..d16b589587 --- /dev/null +++ b/app/src/main/java/org/stepik/android/remote/email_address/EmailAddressRemoteDataSourceImpl.kt @@ -0,0 +1,34 @@ +package org.stepik.android.remote.email_address + +import io.reactivex.Completable +import io.reactivex.Single +import org.stepik.android.data.email_address.source.EmailAddressRemoteDataSource +import org.stepik.android.model.user.EmailAddress +import org.stepik.android.remote.email_address.model.EmailAddressRequest +import org.stepik.android.remote.email_address.model.EmailAddressResponse +import org.stepik.android.remote.email_address.service.EmailAddressService +import javax.inject.Inject + +class EmailAddressRemoteDataSourceImpl +@Inject +constructor( + private val emailAddressService: EmailAddressService +) : EmailAddressRemoteDataSource { + override fun getEmailAddresses(vararg emailIds: Long): Single> = + emailAddressService + .getEmailAddresses(emailIds) + .map(EmailAddressResponse::emailAddresses) + + override fun createEmailAddress(emailAddress: EmailAddress): Single = + emailAddressService + .createEmailAddress(EmailAddressRequest(emailAddress)) + .map { it.emailAddresses.first() } + + override fun setPrimaryEmailAddress(emailId: Long): Completable = + emailAddressService + .setPrimaryEmailAddress(emailId) + + override fun removeEmailAddress(emailId: Long): Completable = + emailAddressService + .removeEmailAddress(emailId) +} \ No newline at end of file diff --git a/app/src/main/java/org/stepik/android/remote/email_address/model/EmailAddressRequest.kt b/app/src/main/java/org/stepik/android/remote/email_address/model/EmailAddressRequest.kt new file mode 100644 index 0000000000..2206d4f38a --- /dev/null +++ b/app/src/main/java/org/stepik/android/remote/email_address/model/EmailAddressRequest.kt @@ -0,0 +1,9 @@ +package org.stepik.android.remote.email_address.model + +import com.google.gson.annotations.SerializedName +import org.stepik.android.model.user.EmailAddress + +class EmailAddressRequest( + @SerializedName("emailAddress") + val emailAddress: EmailAddress +) \ No newline at end of file diff --git a/app/src/main/java/org/stepik/android/remote/email_address/model/EmailAddressResponse.kt b/app/src/main/java/org/stepik/android/remote/email_address/model/EmailAddressResponse.kt new file mode 100644 index 0000000000..f0b96343ce --- /dev/null +++ b/app/src/main/java/org/stepik/android/remote/email_address/model/EmailAddressResponse.kt @@ -0,0 +1,13 @@ +package org.stepik.android.remote.email_address.model + +import com.google.gson.annotations.SerializedName +import org.stepik.android.model.user.EmailAddress +import org.stepik.android.model.Meta +import org.stepik.android.remote.base.model.MetaResponse + +class EmailAddressResponse( + @SerializedName("meta") + override val meta: Meta, + @SerializedName("email-addresses") + val emailAddresses: List +) : MetaResponse \ No newline at end of file diff --git a/app/src/main/java/org/stepik/android/remote/email_address/service/EmailAddressService.kt b/app/src/main/java/org/stepik/android/remote/email_address/service/EmailAddressService.kt new file mode 100644 index 0000000000..d7673d961c --- /dev/null +++ b/app/src/main/java/org/stepik/android/remote/email_address/service/EmailAddressService.kt @@ -0,0 +1,34 @@ +package org.stepik.android.remote.email_address.service + +import io.reactivex.Completable +import io.reactivex.Single +import org.stepik.android.remote.email_address.model.EmailAddressRequest +import org.stepik.android.remote.email_address.model.EmailAddressResponse +import retrofit2.http.Body +import retrofit2.http.DELETE +import retrofit2.http.GET +import retrofit2.http.POST +import retrofit2.http.Path +import retrofit2.http.Query + +interface EmailAddressService { + @GET("api/email-addresses") + fun getEmailAddresses( + @Query("ids[]") ids: LongArray + ): Single + + @POST("api/email-addresses") + fun createEmailAddress( + @Body request: EmailAddressRequest + ): Single + + @POST("api/email-addresses/{emailId}/set-as-primary") + fun setPrimaryEmailAddress( + @Path("emailId") emailId: Long + ): Completable + + @DELETE("api/email-addresses/{emailId}") + fun removeEmailAddress( + @Path("emailId") emailId: Long + ): Completable +} \ No newline at end of file diff --git a/app/src/main/java/org/stepik/android/remote/profile/ProfileRemoteDataSourceImpl.kt b/app/src/main/java/org/stepik/android/remote/profile/ProfileRemoteDataSourceImpl.kt new file mode 100644 index 0000000000..a1ece88911 --- /dev/null +++ b/app/src/main/java/org/stepik/android/remote/profile/ProfileRemoteDataSourceImpl.kt @@ -0,0 +1,30 @@ +package org.stepik.android.remote.profile + +import io.reactivex.Completable +import io.reactivex.Single +import org.stepik.android.data.profile.source.ProfileRemoteDataSource +import org.stepik.android.model.user.Profile +import org.stepik.android.remote.profile.model.ProfilePasswordRequest +import org.stepik.android.remote.profile.model.ProfileRequest +import org.stepik.android.remote.profile.service.ProfileService +import javax.inject.Inject + +class ProfileRemoteDataSourceImpl +@Inject +constructor( + private val profileService: ProfileService +) : ProfileRemoteDataSource { + override fun getProfile(): Single = + profileService + .getProfile() + .map { it.profiles.first() } + + override fun saveProfile(profile: Profile): Single = + profileService + .saveProfile(profile.id, ProfileRequest(profile)) + .map { it.profiles.first() } + + override fun saveProfilePassword(profileId: Long, currentPassword: String, newPassword: String): Completable = + profileService + .saveProfilePassword(profileId, ProfilePasswordRequest(currentPassword, newPassword)) +} \ No newline at end of file diff --git a/app/src/main/java/org/stepik/android/remote/profile/model/ProfilePasswordRequest.kt b/app/src/main/java/org/stepik/android/remote/profile/model/ProfilePasswordRequest.kt new file mode 100644 index 0000000000..e270c80c36 --- /dev/null +++ b/app/src/main/java/org/stepik/android/remote/profile/model/ProfilePasswordRequest.kt @@ -0,0 +1,10 @@ +package org.stepik.android.remote.profile.model + +import com.google.gson.annotations.SerializedName + +class ProfilePasswordRequest( + @SerializedName("current_password") + val currentPassword: String, + @SerializedName("new_password") + val newPassword: String +) \ No newline at end of file diff --git a/app/src/main/java/org/stepik/android/remote/profile/model/ProfileRequest.kt b/app/src/main/java/org/stepik/android/remote/profile/model/ProfileRequest.kt new file mode 100644 index 0000000000..6198f9a48c --- /dev/null +++ b/app/src/main/java/org/stepik/android/remote/profile/model/ProfileRequest.kt @@ -0,0 +1,9 @@ +package org.stepik.android.remote.profile.model + +import com.google.gson.annotations.SerializedName +import org.stepik.android.model.user.Profile + +class ProfileRequest( + @SerializedName("profile") + val profile: Profile +) \ No newline at end of file diff --git a/app/src/main/java/org/stepik/android/remote/profile/model/ProfileResponse.kt b/app/src/main/java/org/stepik/android/remote/profile/model/ProfileResponse.kt new file mode 100644 index 0000000000..91bdcc64d4 --- /dev/null +++ b/app/src/main/java/org/stepik/android/remote/profile/model/ProfileResponse.kt @@ -0,0 +1,9 @@ +package org.stepik.android.remote.profile.model + +import com.google.gson.annotations.SerializedName +import org.stepik.android.model.user.Profile + +class ProfileResponse( + @SerializedName("profiles") + val profiles: List +) \ No newline at end of file diff --git a/app/src/main/java/org/stepik/android/remote/profile/service/ProfileService.kt b/app/src/main/java/org/stepik/android/remote/profile/service/ProfileService.kt new file mode 100644 index 0000000000..0394ed98bd --- /dev/null +++ b/app/src/main/java/org/stepik/android/remote/profile/service/ProfileService.kt @@ -0,0 +1,29 @@ +package org.stepik.android.remote.profile.service + +import io.reactivex.Completable +import io.reactivex.Single +import org.stepik.android.remote.profile.model.ProfilePasswordRequest +import org.stepik.android.remote.profile.model.ProfileRequest +import org.stepik.android.remote.profile.model.ProfileResponse +import retrofit2.http.Body +import retrofit2.http.GET +import retrofit2.http.POST +import retrofit2.http.PUT +import retrofit2.http.Path + +interface ProfileService { + @GET("api/stepics/1") + fun getProfile(): Single + + @PUT("api/profiles/{profileId}") + fun saveProfile( + @Path("profileId") profileId: Long, + @Body profileRequest: ProfileRequest + ): Single + + @POST("api/profiles/{profileId}/change-password") + fun saveProfilePassword( + @Path("profileId") profileId: Long, + @Body profilePasswordRequest: ProfilePasswordRequest + ): Completable +} \ No newline at end of file diff --git a/app/src/main/java/org/stepik/android/view/course/ui/activity/CourseActivity.kt b/app/src/main/java/org/stepik/android/view/course/ui/activity/CourseActivity.kt index 81379f4157..4732b72d46 100644 --- a/app/src/main/java/org/stepik/android/view/course/ui/activity/CourseActivity.kt +++ b/app/src/main/java/org/stepik/android/view/course/ui/activity/CourseActivity.kt @@ -38,13 +38,13 @@ import org.stepik.android.model.Course import org.stepik.android.presentation.course.CoursePresenter import org.stepik.android.presentation.course.CourseView import org.stepik.android.presentation.course.model.EnrollmentError -import org.stepik.android.view.course.listener.CourseFragmentPageChangeListener import org.stepik.android.view.course.routing.CourseScreenTab import org.stepik.android.view.course.routing.getCourseIdFromDeepLink import org.stepik.android.view.course.routing.getCourseTabFromDeepLink import org.stepik.android.view.course.ui.adapter.CoursePagerAdapter import org.stepik.android.view.course.ui.delegates.CourseHeaderDelegate import org.stepik.android.view.ui.delegate.ViewStateDelegate +import org.stepik.android.view.fragment_pager.FragmentDelegateScrollStateChangeListener import uk.co.chrisjenx.calligraphy.TypefaceUtils import javax.inject.Inject @@ -196,7 +196,7 @@ class CourseActivity : FragmentActivityBase(), CourseView { val coursePagerAdapter = CoursePagerAdapter(courseId, this, supportFragmentManager) coursePager.adapter = coursePagerAdapter - coursePager.addOnPageChangeListener(CourseFragmentPageChangeListener(coursePager, coursePagerAdapter)) + coursePager.addOnPageChangeListener(FragmentDelegateScrollStateChangeListener(coursePager, coursePagerAdapter)) coursePager.addOnPageChangeListener(object : ViewPager.OnPageChangeListener { override fun onPageScrollStateChanged(scrollState: Int) { viewPagerScrollState = scrollState diff --git a/app/src/main/java/org/stepik/android/view/course/ui/adapter/CoursePagerAdapter.kt b/app/src/main/java/org/stepik/android/view/course/ui/adapter/CoursePagerAdapter.kt index 043bc4c237..31c4388642 100644 --- a/app/src/main/java/org/stepik/android/view/course/ui/adapter/CoursePagerAdapter.kt +++ b/app/src/main/java/org/stepik/android/view/course/ui/adapter/CoursePagerAdapter.kt @@ -9,12 +9,13 @@ import org.stepic.droid.R import org.stepik.android.view.course_content.ui.fragment.CourseContentFragment import org.stepik.android.view.course_info.ui.fragment.CourseInfoFragment import org.stepik.android.view.course_reviews.ui.fragment.CourseReviewsFragment +import org.stepik.android.view.fragment_pager.ActiveFragmentPagerAdapter class CoursePagerAdapter( courseId: Long, context: Context, fragmentManager: FragmentManager -) : FragmentPagerAdapter(fragmentManager) { +) : FragmentPagerAdapter(fragmentManager), ActiveFragmentPagerAdapter { private val fragments = listOf( { CourseInfoFragment.newInstance(courseId) } to context.getString(R.string.course_tab_info), { CourseReviewsFragment.newInstance(courseId) } to context.getString(R.string.course_tab_reviews), @@ -22,7 +23,7 @@ class CoursePagerAdapter( ) private val _activeFragments = mutableMapOf() - val activeFragments: Map + override val activeFragments: Map get() = _activeFragments override fun getItem(position: Int): Fragment = diff --git a/app/src/main/java/org/stepik/android/view/fragment_pager/ActiveFragmentPagerAdapter.kt b/app/src/main/java/org/stepik/android/view/fragment_pager/ActiveFragmentPagerAdapter.kt new file mode 100644 index 0000000000..3574e14027 --- /dev/null +++ b/app/src/main/java/org/stepik/android/view/fragment_pager/ActiveFragmentPagerAdapter.kt @@ -0,0 +1,7 @@ +package org.stepik.android.view.fragment_pager + +import android.support.v4.app.Fragment + +interface ActiveFragmentPagerAdapter { + val activeFragments: Map +} \ No newline at end of file diff --git a/app/src/main/java/org/stepik/android/view/course/listener/CourseFragmentPageChangeListener.kt b/app/src/main/java/org/stepik/android/view/fragment_pager/FragmentDelegateScrollStateChangeListener.kt similarity index 82% rename from app/src/main/java/org/stepik/android/view/course/listener/CourseFragmentPageChangeListener.kt rename to app/src/main/java/org/stepik/android/view/fragment_pager/FragmentDelegateScrollStateChangeListener.kt index 9ec3ad74b7..f758b765ed 100644 --- a/app/src/main/java/org/stepik/android/view/course/listener/CourseFragmentPageChangeListener.kt +++ b/app/src/main/java/org/stepik/android/view/fragment_pager/FragmentDelegateScrollStateChangeListener.kt @@ -1,15 +1,14 @@ -package org.stepik.android.view.course.listener +package org.stepik.android.view.fragment_pager import android.support.v4.view.ViewPager -import org.stepik.android.view.course.ui.adapter.CoursePagerAdapter import org.stepik.android.view.ui.listener.FragmentViewPagerScrollStateListener -class CourseFragmentPageChangeListener( +class FragmentDelegateScrollStateChangeListener( private val viewPager: ViewPager, - private val coursePagerAdapter: CoursePagerAdapter + private val fragmentAdapter: ActiveFragmentPagerAdapter ) : ViewPager.SimpleOnPageChangeListener() { override fun onPageScrollStateChanged(state: Int) { - coursePagerAdapter + fragmentAdapter .activeFragments .entries .forEach { (position, fragment) -> diff --git a/app/src/main/java/org/stepik/android/view/injection/base/Authorized.kt b/app/src/main/java/org/stepik/android/view/injection/base/Authorized.kt new file mode 100644 index 0000000000..952e9facb8 --- /dev/null +++ b/app/src/main/java/org/stepik/android/view/injection/base/Authorized.kt @@ -0,0 +1,6 @@ +package org.stepik.android.view.injection.base + +import javax.inject.Qualifier + +@Qualifier +annotation class Authorized \ No newline at end of file diff --git a/app/src/main/java/org/stepik/android/view/injection/comments/CommentsBannerDataModule.kt b/app/src/main/java/org/stepik/android/view/injection/comments/CommentsBannerDataModule.kt new file mode 100644 index 0000000000..4cfdb90378 --- /dev/null +++ b/app/src/main/java/org/stepik/android/view/injection/comments/CommentsBannerDataModule.kt @@ -0,0 +1,21 @@ +package org.stepik.android.view.injection.comments + +import dagger.Binds +import dagger.Module +import org.stepik.android.cache.comments.CommentsBannerDataCacheSourceImpl +import org.stepik.android.data.comments.repository.CommentsBannerRepositoryImpl +import org.stepik.android.data.comments.source.CommentsBannerCacheDataSource +import org.stepik.android.domain.comments.repository.CommentsBannerRepository + +@Module +abstract class CommentsBannerDataModule { + @Binds + internal abstract fun bindCommentsBannerRepository( + commentsBannerRepositoryImpl: CommentsBannerRepositoryImpl + ): CommentsBannerRepository + + @Binds + internal abstract fun bindCommentsBannerCacheDataSource( + commentsBannerCacheDataSourceImpl: CommentsBannerDataCacheSourceImpl + ): CommentsBannerCacheDataSource +} \ No newline at end of file diff --git a/app/src/main/java/org/stepik/android/view/injection/email_address/EmailAddressDataModule.kt b/app/src/main/java/org/stepik/android/view/injection/email_address/EmailAddressDataModule.kt new file mode 100644 index 0000000000..198504a950 --- /dev/null +++ b/app/src/main/java/org/stepik/android/view/injection/email_address/EmailAddressDataModule.kt @@ -0,0 +1,33 @@ +package org.stepik.android.view.injection.email_address + +import dagger.Binds +import dagger.Module +import dagger.Provides +import org.stepik.android.data.email_address.repository.EmailAddressRepositoryImpl +import org.stepik.android.data.email_address.source.EmailAddressRemoteDataSource +import org.stepik.android.domain.email_address.repository.EmailAddressRepository +import org.stepik.android.remote.email_address.EmailAddressRemoteDataSourceImpl +import org.stepik.android.remote.email_address.service.EmailAddressService +import org.stepik.android.view.injection.base.Authorized +import retrofit2.Retrofit + +@Module +abstract class EmailAddressDataModule { + @Binds + internal abstract fun bindEmailAddressRepository( + emailAddressRepositoryImpl: EmailAddressRepositoryImpl + ): EmailAddressRepository + + @Binds + internal abstract fun bindEmailAddressRemoteDataSource( + emailAddressRemoteDataSourceImpl: EmailAddressRemoteDataSourceImpl + ): EmailAddressRemoteDataSource + + @Module + companion object { + @Provides + @JvmStatic + internal fun provideEmailAddressService(@Authorized retrofit: Retrofit): EmailAddressService = + retrofit.create(EmailAddressService::class.java) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/stepik/android/view/injection/profile/ProfileBusModule.kt b/app/src/main/java/org/stepik/android/view/injection/profile/ProfileBusModule.kt new file mode 100644 index 0000000000..f7b81da1f3 --- /dev/null +++ b/app/src/main/java/org/stepik/android/view/injection/profile/ProfileBusModule.kt @@ -0,0 +1,28 @@ +package org.stepik.android.view.injection.profile + +import dagger.Module +import dagger.Provides +import io.reactivex.Observable +import io.reactivex.Scheduler +import io.reactivex.subjects.PublishSubject +import org.stepic.droid.di.AppSingleton +import org.stepic.droid.di.qualifiers.BackgroundScheduler +import org.stepik.android.model.user.Profile + +@Module +abstract class ProfileBusModule { + @Module + companion object { + @Provides + @JvmStatic + @AppSingleton + fun provideProfileSubject(): PublishSubject = + PublishSubject.create() + + @Provides + @JvmStatic + @AppSingleton + internal fun provideProfileObservable(profileSubject: PublishSubject, @BackgroundScheduler scheduler: Scheduler): Observable = + profileSubject.observeOn(scheduler) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/stepik/android/view/injection/profile/ProfileDataModule.kt b/app/src/main/java/org/stepik/android/view/injection/profile/ProfileDataModule.kt new file mode 100644 index 0000000000..71d13f6ac3 --- /dev/null +++ b/app/src/main/java/org/stepik/android/view/injection/profile/ProfileDataModule.kt @@ -0,0 +1,40 @@ +package org.stepik.android.view.injection.profile + +import dagger.Binds +import dagger.Module +import dagger.Provides +import org.stepik.android.cache.profile.ProfileCacheDataSourceImpl +import org.stepik.android.data.profile.repository.ProfileRepositoryImpl +import org.stepik.android.data.profile.source.ProfileCacheDataSource +import org.stepik.android.data.profile.source.ProfileRemoteDataSource +import org.stepik.android.domain.profile.repository.ProfileRepository +import org.stepik.android.remote.profile.ProfileRemoteDataSourceImpl +import org.stepik.android.remote.profile.service.ProfileService +import org.stepik.android.view.injection.base.Authorized +import retrofit2.Retrofit + +@Module +abstract class ProfileDataModule { + @Binds + internal abstract fun bindProfileRepository( + profileRepositoryImpl: ProfileRepositoryImpl + ): ProfileRepository + + @Binds + internal abstract fun bindProfileRemoteDataSource( + profileRemoteDataSourceImpl: ProfileRemoteDataSourceImpl + ): ProfileRemoteDataSource + + @Binds + internal abstract fun bindProfileCacheDataSource( + profileCacheDataSourceImpl: ProfileCacheDataSourceImpl + ): ProfileCacheDataSource + + @Module + companion object { + @Provides + @JvmStatic + internal fun provideProfileService(@Authorized retrofit: Retrofit): ProfileService = + retrofit.create(ProfileService::class.java) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/stepik/android/view/injection/profile_edit/ProfileEditComponent.kt b/app/src/main/java/org/stepik/android/view/injection/profile_edit/ProfileEditComponent.kt new file mode 100644 index 0000000000..acb383551c --- /dev/null +++ b/app/src/main/java/org/stepik/android/view/injection/profile_edit/ProfileEditComponent.kt @@ -0,0 +1,22 @@ +package org.stepik.android.view.injection.profile_edit + +import dagger.Subcomponent +import org.stepik.android.view.injection.profile.ProfileDataModule +import org.stepik.android.view.profile_edit.ui.activity.ProfileEditInfoActivity +import org.stepik.android.view.profile_edit.ui.activity.ProfileEditActivity +import org.stepik.android.view.profile_edit.ui.activity.ProfileEditPasswordActivity + +@Subcomponent(modules = [ + ProfileDataModule::class, + ProfileEditModule::class +]) +interface ProfileEditComponent { + @Subcomponent.Builder + interface Builder { + fun build(): ProfileEditComponent + } + + fun inject(profileEditNavigationActivity: ProfileEditActivity) + fun inject(profileEditInfoActivity: ProfileEditInfoActivity) + fun inject(profileEditPasswordActivity: ProfileEditPasswordActivity) +} \ No newline at end of file diff --git a/app/src/main/java/org/stepik/android/view/injection/profile_edit/ProfileEditModule.kt b/app/src/main/java/org/stepik/android/view/injection/profile_edit/ProfileEditModule.kt new file mode 100644 index 0000000000..b3676b4c36 --- /dev/null +++ b/app/src/main/java/org/stepik/android/view/injection/profile_edit/ProfileEditModule.kt @@ -0,0 +1,32 @@ +package org.stepik.android.view.injection.profile_edit + +import android.arch.lifecycle.ViewModel +import dagger.Binds +import dagger.Module +import dagger.multibindings.IntoMap +import org.stepik.android.presentation.base.injection.ViewModelKey +import org.stepik.android.presentation.profile_edit.ProfileEditInfoPresenter +import org.stepik.android.presentation.profile_edit.ProfileEditPasswordPresenter +import org.stepik.android.presentation.profile_edit.ProfileEditPresenter + +@Module +abstract class ProfileEditModule { + + /** + * Presentation layer + */ + @Binds + @IntoMap + @ViewModelKey(ProfileEditInfoPresenter::class) + internal abstract fun bindProfileEditInfoPresenter(profileEditInfoPresenter: ProfileEditInfoPresenter): ViewModel + + @Binds + @IntoMap + @ViewModelKey(ProfileEditPasswordPresenter::class) + internal abstract fun bindProfileEditPasswordPresenter(profileEditPasswordPresenter: ProfileEditPasswordPresenter): ViewModel + + @Binds + @IntoMap + @ViewModelKey(ProfileEditPresenter::class) + internal abstract fun bindProfileEditPresenter(profileEditPresenter: ProfileEditPresenter): ViewModel +} \ No newline at end of file diff --git a/app/src/main/java/org/stepik/android/view/profile_edit/model/ProfileEditItem.kt b/app/src/main/java/org/stepik/android/view/profile_edit/model/ProfileEditItem.kt new file mode 100644 index 0000000000..2d317836ee --- /dev/null +++ b/app/src/main/java/org/stepik/android/view/profile_edit/model/ProfileEditItem.kt @@ -0,0 +1,12 @@ +package org.stepik.android.view.profile_edit.model + +class ProfileEditItem( + val type: Type, + val title: String, + val subtitle: String +) { + enum class Type { + PERSONAL_INFO, + PASSWORD + } +} \ No newline at end of file diff --git a/app/src/main/java/org/stepik/android/view/profile_edit/ui/activity/ProfileEditActivity.kt b/app/src/main/java/org/stepik/android/view/profile_edit/ui/activity/ProfileEditActivity.kt new file mode 100644 index 0000000000..fbc04e4752 --- /dev/null +++ b/app/src/main/java/org/stepik/android/view/profile_edit/ui/activity/ProfileEditActivity.kt @@ -0,0 +1,141 @@ +package org.stepik.android.view.profile_edit.ui.activity + +import android.app.Activity +import android.arch.lifecycle.ViewModelProvider +import android.arch.lifecycle.ViewModelProviders +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.support.design.widget.Snackbar +import android.support.v4.content.ContextCompat +import android.support.v7.app.AppCompatActivity +import android.support.v7.widget.DividerItemDecoration +import android.support.v7.widget.LinearLayoutManager +import android.view.MenuItem +import kotlinx.android.synthetic.main.activity_profile_edit.* +import org.stepic.droid.R +import org.stepic.droid.base.App +import org.stepic.droid.core.ScreenManager +import org.stepic.droid.ui.util.initCenteredToolbar +import org.stepic.droid.util.setTextColor +import org.stepik.android.model.user.Profile +import org.stepik.android.presentation.profile_edit.ProfileEditPresenter +import org.stepik.android.presentation.profile_edit.ProfileEditView +import org.stepik.android.view.profile_edit.model.ProfileEditItem +import org.stepik.android.view.profile_edit.ui.adapter.ProfileEditAdapter +import org.stepik.android.view.ui.delegate.ViewStateDelegate +import javax.inject.Inject + +class ProfileEditActivity : AppCompatActivity(), ProfileEditView { + companion object { + fun createIntent(context: Context): Intent = + Intent(context, ProfileEditActivity::class.java) + } + + private lateinit var profileEditPresenter: ProfileEditPresenter + + @Inject + internal lateinit var screenManager: ScreenManager + + @Inject + internal lateinit var viewModelFactory: ViewModelProvider.Factory + + private var profile: Profile? = null + private val viewStateDelegate = + ViewStateDelegate() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_profile_edit) + injectComponent() + profileEditPresenter = ViewModelProviders + .of(this, viewModelFactory) + .get(ProfileEditPresenter::class.java) + initCenteredToolbar(R.string.profile_title, showHomeButton = true, homeIndicator = R.drawable.ic_close_dark) + + val navigationItems = listOf( + ProfileEditItem(ProfileEditItem.Type.PERSONAL_INFO, getString(R.string.profile_edit_info_title), getString(R.string.profile_edit_info_subtitle)), + ProfileEditItem(ProfileEditItem.Type.PASSWORD, getString(R.string.profile_edit_password_title), getString(R.string.profile_edit_password_subtitle)) + ) + + navigationRecycler.layoutManager = LinearLayoutManager(this) + navigationRecycler.adapter = ProfileEditAdapter(navigationItems) { item -> + val profile = profile ?: return@ProfileEditAdapter + when (item.type) { + ProfileEditItem.Type.PERSONAL_INFO -> + screenManager.showProfileEditInfo(this, profile) + ProfileEditItem.Type.PASSWORD -> + screenManager.showProfileEditPassword(this, profile.id) + } + } + + navigationRecycler.addItemDecoration(DividerItemDecoration(this, DividerItemDecoration.VERTICAL).apply { + ContextCompat.getDrawable(this@ProfileEditActivity, R.drawable.list_divider_h)?.let(::setDrawable) + }) + + viewStateDelegate.addState() + viewStateDelegate.addState() + viewStateDelegate.addState(profileEditEmptyLogin) + viewStateDelegate.addState(navigationRecycler) + } + + private fun injectComponent() { + App.component() + .profileEditComponentBuilder() + .build() + .inject(this) + } + + override fun onStart() { + super.onStart() + profileEditPresenter.attachView(this) + } + + override fun onStop() { + profileEditPresenter.detachView(this) + super.onStop() + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean = + if (item.itemId == android.R.id.home) { + onBackPressed() + true + } else { + false + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + when (requestCode) { + ProfileEditInfoActivity.REQUEST_CODE -> + if (resultCode == Activity.RESULT_OK) { + Snackbar + .make(root, R.string.profile_edit_change_success_info, Snackbar.LENGTH_SHORT) + .setTextColor(ContextCompat.getColor(this, R.color.white)) + .show() + } + + ProfileEditPasswordActivity.REQUEST_CODE -> + if (resultCode == Activity.RESULT_OK) { + Snackbar + .make(root, R.string.profile_edit_change_success_password, Snackbar.LENGTH_SHORT) + .setTextColor(ContextCompat.getColor(this, R.color.white)) + .show() + } + + else -> + super.onActivityResult(requestCode, resultCode, data) + } + } + + override fun setState(state: ProfileEditView.State) { + viewStateDelegate.switchState(state) + if (state is ProfileEditView.State.ProfileLoaded) { + profile = state.profile + } + } + + override fun finish() { + super.finish() + overridePendingTransition(org.stepic.droid.R.anim.no_transition, org.stepic.droid.R.anim.push_down) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/stepik/android/view/profile_edit/ui/activity/ProfileEditInfoActivity.kt b/app/src/main/java/org/stepik/android/view/profile_edit/ui/activity/ProfileEditInfoActivity.kt new file mode 100644 index 0000000000..54965cf071 --- /dev/null +++ b/app/src/main/java/org/stepik/android/view/profile_edit/ui/activity/ProfileEditInfoActivity.kt @@ -0,0 +1,146 @@ +package org.stepik.android.view.profile_edit.ui.activity + +import android.app.Activity +import android.arch.lifecycle.ViewModelProvider +import android.arch.lifecycle.ViewModelProviders +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.support.design.widget.Snackbar +import android.support.v4.app.DialogFragment +import android.support.v4.content.ContextCompat +import android.support.v7.app.AppCompatActivity +import android.view.Menu +import android.view.MenuItem +import kotlinx.android.synthetic.main.activity_profile_edit_info.* +import org.stepic.droid.R +import org.stepic.droid.base.App +import org.stepic.droid.ui.dialogs.LoadingProgressDialogFragment +import org.stepic.droid.ui.util.initCenteredToolbar +import org.stepic.droid.util.ProgressHelper +import org.stepic.droid.util.setTextColor +import org.stepik.android.model.user.Profile +import org.stepik.android.presentation.profile_edit.ProfileEditInfoPresenter +import org.stepik.android.presentation.profile_edit.ProfileEditInfoView +import javax.inject.Inject + +class ProfileEditInfoActivity : AppCompatActivity(), ProfileEditInfoView { + companion object { + const val REQUEST_CODE = 12090 + + private const val EXTRA_PROFILE = "profile" + + fun createIntent(context: Context, profile: Profile): Intent = + Intent(context, ProfileEditInfoActivity::class.java) + .putExtra(EXTRA_PROFILE, profile) + } + + private val progressDialogFragment: DialogFragment = + LoadingProgressDialogFragment.newInstance() + + private lateinit var profileEditInfoPresenter: ProfileEditInfoPresenter + + @Inject + internal lateinit var viewModelFactory: ViewModelProvider.Factory + + private val profile by lazy { intent.getParcelableExtra(EXTRA_PROFILE) } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_profile_edit_info) + + injectComponent() + profileEditInfoPresenter = ViewModelProviders + .of(this, viewModelFactory) + .get(ProfileEditInfoPresenter::class.java) + + initCenteredToolbar(R.string.profile_edit_info_title, showHomeButton = true, homeIndicator = R.drawable.ic_close_dark) + + if (savedInstanceState == null) { + firstNameEditText.setText(profile.firstName ?: "") + lastNameEditText.setText(profile.lastName ?: "") + + shortBioEditText.setText(profile.shortBio ?: "") + detailsEditText.setText(profile.details ?: "") + } + } + + private fun injectComponent() { + App.component() + .profileEditComponentBuilder() + .build() + .inject(this) + } + + override fun onStart() { + super.onStart() + profileEditInfoPresenter.attachView(this) + } + + override fun onStop() { + profileEditInfoPresenter.detachView(this) + super.onStop() + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.profile_edit_menu, menu) + return super.onCreateOptionsMenu(menu) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean = + when (item.itemId) { + android.R.id.home -> { + onBackPressed() + true + } + R.id.profile_edit_save -> { + submit() + true + } + else -> false + } + + private fun submit() { + val firstName = firstNameEditText.text.toString() + val lastName = lastNameEditText.text.toString() + val shortBio = shortBioEditText.text.toString() + val details = detailsEditText.text.toString() + + profileEditInfoPresenter.updateProfileInfo(profile, firstName, lastName, shortBio, details) + } + + override fun setState(state: ProfileEditInfoView.State) { + when (state) { + ProfileEditInfoView.State.IDLE -> + ProgressHelper.dismiss(supportFragmentManager, LoadingProgressDialogFragment.TAG) + + ProfileEditInfoView.State.LOADING -> + ProgressHelper.activate(progressDialogFragment, supportFragmentManager, LoadingProgressDialogFragment.TAG) + + ProfileEditInfoView.State.COMPLETE -> { + ProgressHelper.dismiss(supportFragmentManager, LoadingProgressDialogFragment.TAG) + setResult(Activity.RESULT_OK) + finish() + } + } + } + + override fun showNetworkError() { + Snackbar + .make(root, R.string.no_connection, Snackbar.LENGTH_SHORT) + .setTextColor(ContextCompat.getColor(this, R.color.white)) + .show() + } + + override fun showInfoError() { + Snackbar + .make(root, R.string.profile_edit_error_info, Snackbar.LENGTH_SHORT) + .setTextColor(ContextCompat.getColor(this, R.color.white)) + .show() + } + + override fun finish() { + super.finish() + overridePendingTransition(org.stepic.droid.R.anim.no_transition, org.stepic.droid.R.anim.push_down) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/stepik/android/view/profile_edit/ui/activity/ProfileEditPasswordActivity.kt b/app/src/main/java/org/stepik/android/view/profile_edit/ui/activity/ProfileEditPasswordActivity.kt new file mode 100644 index 0000000000..6b0534432f --- /dev/null +++ b/app/src/main/java/org/stepik/android/view/profile_edit/ui/activity/ProfileEditPasswordActivity.kt @@ -0,0 +1,165 @@ +package org.stepik.android.view.profile_edit.ui.activity + +import android.app.Activity +import android.arch.lifecycle.ViewModelProvider +import android.arch.lifecycle.ViewModelProviders +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.support.design.widget.Snackbar +import android.support.v4.app.DialogFragment +import android.support.v4.content.ContextCompat +import android.support.v7.app.AppCompatActivity +import android.text.Editable +import android.text.TextWatcher +import android.view.Menu +import android.view.MenuItem +import kotlinx.android.synthetic.main.activity_profile_edit_password.* +import org.stepic.droid.R +import org.stepic.droid.base.App +import org.stepic.droid.ui.dialogs.LoadingProgressDialogFragment +import org.stepic.droid.ui.util.initCenteredToolbar +import org.stepic.droid.util.ProgressHelper +import org.stepic.droid.util.setTextColor +import org.stepik.android.presentation.profile_edit.ProfileEditPasswordPresenter +import org.stepik.android.presentation.profile_edit.ProfileEditPasswordView +import org.stepik.android.view.profile_edit.ui.util.ValidateUtil +import javax.inject.Inject + +class ProfileEditPasswordActivity : AppCompatActivity(), ProfileEditPasswordView { + companion object { + const val REQUEST_CODE = 12992 + + private const val EXTRA_PROFILE_ID = "profile_id" + + fun createIntent(context: Context, profileId: Long): Intent = + Intent(context, ProfileEditPasswordActivity::class.java) + .putExtra(EXTRA_PROFILE_ID, profileId) + } + + private val progressDialogFragment: DialogFragment = + LoadingProgressDialogFragment.newInstance() + + private lateinit var profileEditPasswordPresenter: ProfileEditPasswordPresenter + + @Inject + internal lateinit var viewModelFactory: ViewModelProvider.Factory + + private val profileId by lazy { intent.getLongExtra(EXTRA_PROFILE_ID, -1) } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_profile_edit_password) + + injectComponent() + profileEditPasswordPresenter = ViewModelProviders + .of(this, viewModelFactory) + .get(ProfileEditPasswordPresenter::class.java) + + initCenteredToolbar(R.string.profile_edit_password_title, showHomeButton = true, homeIndicator = R.drawable.ic_close_dark) + + currentPasswordEditText.addTextChangedListener(object : TextWatcher { + override fun afterTextChanged(s: Editable?) { + currentPasswordInputLayout.isErrorEnabled = false + } + + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {} + }) + + newPasswordEditText.addTextChangedListener(object : TextWatcher { + override fun afterTextChanged(s: Editable?) { + newPasswordInputLayout.isErrorEnabled = false + } + + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {} + }) + } + + private fun injectComponent() { + App.component() + .profileEditComponentBuilder() + .build() + .inject(this) + } + + override fun onStart() { + super.onStart() + profileEditPasswordPresenter.attachView(this) + } + + override fun onStop() { + profileEditPasswordPresenter.detachView(this) + super.onStop() + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.profile_edit_menu, menu) + return super.onCreateOptionsMenu(menu) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean = + when (item.itemId) { + android.R.id.home -> { + onBackPressed() + true + } + R.id.profile_edit_save -> { + submit() + true + } + else -> false + } + + private fun submit() { + val isCurrentPasswordFilled = ValidateUtil.validateRequiredField(currentPasswordInputLayout, currentPasswordEditText) + val isNewPasswordFilled = ValidateUtil.validateRequiredField(newPasswordInputLayout, newPasswordEditText) + + if (!isCurrentPasswordFilled || + !isNewPasswordFilled) { + return + } + + val currentPassword = currentPasswordEditText.text.toString() + val newPassword = newPasswordEditText.text.toString() + + profileEditPasswordPresenter + .updateProfilePassword(profileId, currentPassword, newPassword) + } + + override fun setState(state: ProfileEditPasswordView.State) { + when (state) { + ProfileEditPasswordView.State.IDLE -> + ProgressHelper.dismiss(supportFragmentManager, LoadingProgressDialogFragment.TAG) + + ProfileEditPasswordView.State.LOADING -> + ProgressHelper.activate(progressDialogFragment, supportFragmentManager, LoadingProgressDialogFragment.TAG) + + ProfileEditPasswordView.State.COMPLETE -> { + ProgressHelper.dismiss(supportFragmentManager, LoadingProgressDialogFragment.TAG) + setResult(Activity.RESULT_OK) + finish() + } + } + } + + override fun showNetworkError() { + Snackbar + .make(root, R.string.no_connection, Snackbar.LENGTH_SHORT) + .setTextColor(ContextCompat.getColor(this, R.color.white)) + .show() + } + + override fun showPasswordError() { + Snackbar + .make(root, R.string.profile_edit_error_password, Snackbar.LENGTH_SHORT) + .setTextColor(ContextCompat.getColor(this, R.color.white)) + .show() + } + + override fun finish() { + super.finish() + overridePendingTransition(org.stepic.droid.R.anim.no_transition, org.stepic.droid.R.anim.push_down) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/stepik/android/view/profile_edit/ui/adapter/ProfileEditAdapter.kt b/app/src/main/java/org/stepik/android/view/profile_edit/ui/adapter/ProfileEditAdapter.kt new file mode 100644 index 0000000000..f894b684ed --- /dev/null +++ b/app/src/main/java/org/stepik/android/view/profile_edit/ui/adapter/ProfileEditAdapter.kt @@ -0,0 +1,21 @@ +package org.stepik.android.view.profile_edit.ui.adapter + +import org.stepic.droid.ui.custom.adapter_delegates.DelegateAdapter +import org.stepic.droid.ui.custom.adapter_delegates.DelegateViewHolder +import org.stepik.android.view.profile_edit.model.ProfileEditItem +import org.stepik.android.view.profile_edit.ui.adapter.delegates.ProfileEditTextDelegate + +class ProfileEditAdapter( + private val items: List, + onItemClicked: (ProfileEditItem) -> Unit +) : DelegateAdapter>() { + init { + addDelegate(ProfileEditTextDelegate(this, onItemClicked)) + } + + override fun getItemAtPosition(position: Int): ProfileEditItem = + items[position] + + override fun getItemCount(): Int = + items.size +} \ No newline at end of file diff --git a/app/src/main/java/org/stepik/android/view/profile_edit/ui/adapter/delegates/ProfileEditTextDelegate.kt b/app/src/main/java/org/stepik/android/view/profile_edit/ui/adapter/delegates/ProfileEditTextDelegate.kt new file mode 100644 index 0000000000..6ac7843fa7 --- /dev/null +++ b/app/src/main/java/org/stepik/android/view/profile_edit/ui/adapter/delegates/ProfileEditTextDelegate.kt @@ -0,0 +1,35 @@ +package org.stepik.android.view.profile_edit.ui.adapter.delegates + +import android.view.View +import android.view.ViewGroup +import kotlinx.android.synthetic.main.item_profile_edit_navigation.view.* +import org.stepic.droid.R +import org.stepic.droid.ui.custom.adapter_delegates.AdapterDelegate +import org.stepic.droid.ui.custom.adapter_delegates.DelegateAdapter +import org.stepic.droid.ui.custom.adapter_delegates.DelegateViewHolder +import org.stepik.android.view.profile_edit.model.ProfileEditItem + +class ProfileEditTextDelegate( + adapter: DelegateAdapter>, + private val onItemClicked: (ProfileEditItem) -> Unit +) : AdapterDelegate>(adapter) { + override fun onCreateViewHolder(parent: ViewGroup): DelegateViewHolder = + ViewHolder(createView(parent, R.layout.item_profile_edit_navigation)) + + override fun isForViewType(position: Int): Boolean = + getItemAtPosition(position) is ProfileEditItem + + inner class ViewHolder(root: View) : DelegateViewHolder(root) { + private val title = root.title + private val subtitle = root.subtitle + + init { + root.setOnClickListener { itemData?.let(onItemClicked) } + } + + override fun onBind(data: ProfileEditItem) { + title.text = data.title + subtitle.text = data.subtitle + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/stepik/android/view/profile_edit/ui/util/ValidateUtil.kt b/app/src/main/java/org/stepik/android/view/profile_edit/ui/util/ValidateUtil.kt new file mode 100644 index 0000000000..82820e3836 --- /dev/null +++ b/app/src/main/java/org/stepik/android/view/profile_edit/ui/util/ValidateUtil.kt @@ -0,0 +1,19 @@ +package org.stepik.android.view.profile_edit.ui.util + +import android.support.design.widget.TextInputEditText +import android.support.design.widget.TextInputLayout +import android.text.TextUtils +import org.stepic.droid.R + +object ValidateUtil { + fun validateRequiredField(layout: TextInputLayout, editText: TextInputEditText): Boolean { + val value = (editText.text ?: "").trim() + val valid = !TextUtils.isEmpty(value) + if (valid) { + layout.isErrorEnabled = false + } else { + layout.error = layout.context.getString(R.string.profile_edit_error_required_field) + } + return valid + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable-hdpi/ic_edit.png b/app/src/main/res/drawable-hdpi/ic_edit.png new file mode 100644 index 0000000000..e44f8fccaa Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_edit.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_profile_edit_save.png b/app/src/main/res/drawable-hdpi/ic_profile_edit_save.png new file mode 100644 index 0000000000..46d41cf9bc Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_profile_edit_save.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_edit.png b/app/src/main/res/drawable-mdpi/ic_edit.png new file mode 100644 index 0000000000..99c38f2f0d Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_edit.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_profile_edit_save.png b/app/src/main/res/drawable-mdpi/ic_profile_edit_save.png new file mode 100644 index 0000000000..245bbe58de Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_profile_edit_save.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_edit.png b/app/src/main/res/drawable-xhdpi/ic_edit.png new file mode 100644 index 0000000000..55967502d4 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_edit.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_profile_edit_save.png b/app/src/main/res/drawable-xhdpi/ic_profile_edit_save.png new file mode 100644 index 0000000000..6889c32232 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_profile_edit_save.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_edit.png b/app/src/main/res/drawable-xxhdpi/ic_edit.png new file mode 100644 index 0000000000..cb9033407f Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_edit.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_profile_edit_save.png b/app/src/main/res/drawable-xxhdpi/ic_profile_edit_save.png new file mode 100644 index 0000000000..a12e10434b Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_profile_edit_save.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_edit.png b/app/src/main/res/drawable-xxxhdpi/ic_edit.png new file mode 100644 index 0000000000..9a5c86696c Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_edit.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_profile_edit_save.png b/app/src/main/res/drawable-xxxhdpi/ic_profile_edit_save.png new file mode 100644 index 0000000000..22e2c5e90d Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_profile_edit_save.png differ diff --git a/app/src/main/res/drawable/popup_arrow_down.xml b/app/src/main/res/drawable/popup_arrow_down.xml new file mode 100644 index 0000000000..62306fa587 --- /dev/null +++ b/app/src/main/res/drawable/popup_arrow_down.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/popup_arrow_down_light.xml b/app/src/main/res/drawable/popup_arrow_down_light.xml new file mode 100644 index 0000000000..929cac39b4 --- /dev/null +++ b/app/src/main/res/drawable/popup_arrow_down_light.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_profile_edit.xml b/app/src/main/res/layout/activity_profile_edit.xml new file mode 100644 index 0000000000..871ba74bf2 --- /dev/null +++ b/app/src/main/res/layout/activity_profile_edit.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_profile_edit_info.xml b/app/src/main/res/layout/activity_profile_edit_info.xml new file mode 100644 index 0000000000..e70efe7c27 --- /dev/null +++ b/app/src/main/res/layout/activity_profile_edit_info.xml @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_profile_edit_password.xml b/app/src/main/res/layout/activity_profile_edit_password.xml new file mode 100644 index 0000000000..5f0f436a74 --- /dev/null +++ b/app/src/main/res/layout/activity_profile_edit_password.xml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_text_step.xml b/app/src/main/res/layout/fragment_text_step.xml index c89d656ea4..988470afd5 100644 --- a/app/src/main/res/layout/fragment_text_step.xml +++ b/app/src/main/res/layout/fragment_text_step.xml @@ -2,7 +2,7 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/popup_window_down.xml b/app/src/main/res/layout/popup_window_down.xml new file mode 100644 index 0000000000..3b5e646e0c --- /dev/null +++ b/app/src/main/res/layout/popup_window_down.xml @@ -0,0 +1,26 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/profile_edit_menu.xml b/app/src/main/res/menu/profile_edit_menu.xml new file mode 100644 index 0000000000..92a662d61a --- /dev/null +++ b/app/src/main/res/menu/profile_edit_menu.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/profile_menu.xml b/app/src/main/res/menu/profile_menu.xml new file mode 100644 index 0000000000..866aaef91f --- /dev/null +++ b/app/src/main/res/menu/profile_menu.xml @@ -0,0 +1,16 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index b834a1da5b..74b4e928e3 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -667,4 +667,32 @@ Вы проделали отличную работу вчера - это хороший повод зайти сегодня и продолжить учиться! 🚀 Время учиться ‍🎓 Вас давно не было. Заходите и учитесь новому! 💡 + + Откройте комментарии, чтобы лучше понять материал или задать вопрос + + + Редактировать профиль + Пароль + Текущий пароль + Новый пароль + Новый пароль (ещё раз) + + Персональные данные + Ваше имя и другая информация + + Имя + Фамилия + Это имя будет использовано в сертификатах + Краткая информация + Город, университет, работа… + О вас + + Сохранить + + Обязательное поле + Ошибка при попытке смены пароля + Ошибка при попытке изменения информации + + Информация обновлена + Пароль изменен \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index c64feacb29..b6665988b0 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -178,4 +178,8 @@ #4a90e2 #F6F6F6 #B4B4B4 + + + #222222 + #999999 \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3f01762673..a46c0371a9 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -692,4 +692,34 @@ You\'ve done a great job learning yesterday, come back today and become smarter! 🚀 Time to study ‍🎓 You haven\'t been here for a while. Come back and continue study! 💡 + + + Open discussions for better understanding or asking a question + + + Edit profile + Password + ************ + Current password + New password + New password (repeat) + + Personal info + Your name and bio + + First name + Last name + This name will be used in certificates + Short bio + City, university, job and etc. + About + + Save + + Required field + Cannot change password + Cannot change info + + Profile info changed + Password changed diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 160efbeb4f..6c6cc70ae8 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -378,7 +378,7 @@ @@ -468,4 +468,15 @@ @color/white @color/white + + + + diff --git a/app/src/main/res/values/text-styles.xml b/app/src/main/res/values/text-styles.xml index f0b10fc7da..f870e13be0 100644 --- a/app/src/main/res/values/text-styles.xml +++ b/app/src/main/res/values/text-styles.xml @@ -90,4 +90,18 @@ 1 end + + + + \ No newline at end of file diff --git a/app/src/test/java/org/stepic/droid/core/presenters/ProfilePresenterTest.java b/app/src/test/java/org/stepic/droid/core/presenters/ProfilePresenterTest.java index 39e73a68ce..b767f9c74c 100644 --- a/app/src/test/java/org/stepic/droid/core/presenters/ProfilePresenterTest.java +++ b/app/src/test/java/org/stepic/droid/core/presenters/ProfilePresenterTest.java @@ -25,6 +25,9 @@ import java.util.List; import java.util.concurrent.ThreadPoolExecutor; +import io.reactivex.Observable; +import io.reactivex.schedulers.Schedulers; + import static org.mockito.Matchers.any; import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; @@ -33,7 +36,6 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; - public class ProfilePresenterTest { private ProfilePresenter profilePresenter; //we test logic of this object @@ -78,7 +80,9 @@ public void beforeEachTest() throws IOException { analytic, mainHandler, api, - sharedPreferenceHelper + sharedPreferenceHelper, + Observable.empty(), + Schedulers.io() ); } diff --git a/dependencies.gradle b/dependencies.gradle index 8f4f7ddf15..0109948861 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -1,6 +1,6 @@ ext.versions = [ - code : 2019, - name : '1.80', + code : 2021, + name : '1.81', minSdk : 15, targetSdk : 26, @@ -71,7 +71,7 @@ ext.versions = [ MPAndroidChart : 'v3.0.3', shortcutBadger : '1.1.19@aar', - StoriesKit : '1.0.1@aar', + StoriesKit : '1.1.1', stetho : '1.4.2', leakCanary : '1.5.1', diff --git a/model/src/main/java/org/stepik/android/model/user/EmailAddress.kt b/model/src/main/java/org/stepik/android/model/user/EmailAddress.kt index bde44200c8..8df3d6139b 100644 --- a/model/src/main/java/org/stepik/android/model/user/EmailAddress.kt +++ b/model/src/main/java/org/stepik/android/model/user/EmailAddress.kt @@ -3,12 +3,15 @@ package org.stepik.android.model.user import com.google.gson.annotations.SerializedName data class EmailAddress( - val id: Long = 0, - val user: Long = 0, - val email: String? = null, + @SerializedName("id") + val id: Long = 0, + @SerializedName("user") + val user: Long = 0, + @SerializedName("email") + val email: String? = null, - @SerializedName("is_verified") - val isVerified: Boolean = false, - @SerializedName("is_primary") - val isPrimary: Boolean = false + @SerializedName("is_verified") + val isVerified: Boolean = false, + @SerializedName("is_primary") + val isPrimary: Boolean = false ) \ No newline at end of file diff --git a/model/src/main/java/org/stepik/android/model/user/Profile.kt b/model/src/main/java/org/stepik/android/model/user/Profile.kt index defc981d27..078ca48808 100644 --- a/model/src/main/java/org/stepik/android/model/user/Profile.kt +++ b/model/src/main/java/org/stepik/android/model/user/Profile.kt @@ -1,28 +1,67 @@ package org.stepik.android.model.user +import android.os.Parcel +import android.os.Parcelable import com.google.gson.annotations.SerializedName +import org.stepik.android.model.util.readBoolean +import org.stepik.android.model.util.writeBoolean data class Profile( - val id: Long = 0, + @SerializedName("id") + val id: Long = 0, - @SerializedName("first_name") - val firstName: String? = null, - @SerializedName("last_name") - val lastName: String? = null, + @SerializedName("first_name") + val firstName: String? = null, + @SerializedName("last_name") + val lastName: String? = null, - @SerializedName("full_name") - val fullName: String? = null, - @SerializedName("short_bio") - val shortBio: String? = null, + @SerializedName("full_name") + val fullName: String? = null, + @SerializedName("short_bio") + val shortBio: String? = null, - val details: String? = null, - val avatar: String? = null, + val details: String? = null, + val avatar: String? = null, - @SerializedName("is_private") - val isPrivate: Boolean = false, - @SerializedName("is_guest") - val isGuest: Boolean = false, + @SerializedName("is_private") + val isPrivate: Boolean = false, + @SerializedName("is_guest") + val isGuest: Boolean = false, - @SerializedName("email_addresses") - val emailAddresses: LongArray? = null -) \ No newline at end of file + @SerializedName("email_addresses") + val emailAddresses: LongArray? = null +) : Parcelable { + override fun writeToParcel(parcel: Parcel, flags: Int) { + parcel.writeLong(id) + parcel.writeString(firstName) + parcel.writeString(lastName) + parcel.writeString(fullName) + parcel.writeString(shortBio) + parcel.writeString(details) + parcel.writeString(avatar) + parcel.writeBoolean(isPrivate) + parcel.writeBoolean(isGuest) + parcel.writeLongArray(emailAddresses) + } + + override fun describeContents(): Int = 0 + + companion object CREATOR : Parcelable.Creator { + override fun createFromParcel(parcel: Parcel): Profile = + Profile( + parcel.readLong(), + parcel.readString(), + parcel.readString(), + parcel.readString(), + parcel.readString(), + parcel.readString(), + parcel.readString(), + parcel.readBoolean(), + parcel.readBoolean(), + parcel.createLongArray() + ) + + override fun newArray(size: Int): Array = + arrayOfNulls(size) + } +} \ No newline at end of file