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