diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index c3b6d67e..531a4aae 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -32,6 +32,11 @@ android:label="@string/app_name" android:noHistory="true" /> + + + diff --git a/app/src/main/java/org/schulcloud/mobile/data/datamanagers/FeedbackDataManager.java b/app/src/main/java/org/schulcloud/mobile/data/datamanagers/FeedbackDataManager.java index 623ba0d2..c722a56a 100644 --- a/app/src/main/java/org/schulcloud/mobile/data/datamanagers/FeedbackDataManager.java +++ b/app/src/main/java/org/schulcloud/mobile/data/datamanagers/FeedbackDataManager.java @@ -4,11 +4,14 @@ import org.schulcloud.mobile.data.model.requestBodies.FeedbackRequest; import org.schulcloud.mobile.data.model.responseBodies.FeedbackResponse; import org.schulcloud.mobile.data.remote.RestService; +import org.schulcloud.mobile.ui.PasswordRecovery.PasswordRecoveryMvpView; import javax.inject.Inject; import javax.inject.Singleton; +import okhttp3.ResponseBody; import rx.Observable; +import rx.android.schedulers.AndroidSchedulers; import rx.functions.Func1; @Singleton @@ -41,4 +44,12 @@ public Observable call(FeedbackResponse feedbackResponse) { } }); } + + public void sendEmail(FeedbackRequest feedbackRequest){ + + mRestService + .sendFeedback(userDataManager.getAccessToken(),feedbackRequest) + .observeOn(AndroidSchedulers.mainThread()).subscribe(res->{},error -> {}); + + } } diff --git a/app/src/main/java/org/schulcloud/mobile/data/datamanagers/UserDataManager.java b/app/src/main/java/org/schulcloud/mobile/data/datamanagers/UserDataManager.java index b4909378..400029d8 100644 --- a/app/src/main/java/org/schulcloud/mobile/data/datamanagers/UserDataManager.java +++ b/app/src/main/java/org/schulcloud/mobile/data/datamanagers/UserDataManager.java @@ -2,9 +2,13 @@ import org.schulcloud.mobile.data.local.PreferencesHelper; import org.schulcloud.mobile.data.local.UserDatabaseHelper; +import org.schulcloud.mobile.data.model.Account; import org.schulcloud.mobile.data.model.CurrentUser; import org.schulcloud.mobile.data.model.User; import org.schulcloud.mobile.data.model.requestBodies.Credentials; +import org.schulcloud.mobile.data.model.requestBodies.ResetData; +import org.schulcloud.mobile.data.model.requestBodies.ResetRequest; +import org.schulcloud.mobile.data.model.responseBodies.ResetResponse; import org.schulcloud.mobile.data.remote.RestService; import org.schulcloud.mobile.util.crypt.JWTUtil; @@ -13,6 +17,7 @@ import javax.inject.Inject; import javax.inject.Singleton; +import okhttp3.ResponseBody; import rx.Observable; import rx.Single; import rx.functions.Func1; @@ -52,6 +57,12 @@ public Observable> getUsers() { return mUserDatabaseHelper.getUsers().distinctUntilChanged(); } + public Observable getAccount(String accountID){ + return mRestService.getAccount(getAccessToken(),accountID); + } + + + public String getAccessToken() { return mPreferencesHelper.getAccessToken(); } @@ -72,16 +83,19 @@ public void signOut() { mPreferencesHelper.clear(); } + public Observable requestResetPassword(String username){ + return mRestService.passwordRecovery(getAccessToken(), new ResetRequest(username)); + } + public Observable resetPassword(String accountId, String password) { + return mRestService.passwordReset(getAccessToken(), new ResetData(accountId, password)); + } + public Observable syncCurrentUser(String userId) { - return mRestService.getUser(getAccessToken(), userId).concatMap( - new Func1>() { - @Override - public Observable call(CurrentUser currentUser) { - mPreferencesHelper.saveCurrentUsername(currentUser.displayName); - mPreferencesHelper.saveCurrentSchoolId(currentUser.schoolId); - return mUserDatabaseHelper.setCurrentUser(currentUser); - } - }).doOnError(Throwable::printStackTrace); + return mRestService.getUser(getAccessToken(), userId).concatMap(currentUser -> { + mPreferencesHelper.saveCurrentUsername(currentUser.displayName); + mPreferencesHelper.saveCurrentSchoolId(currentUser.schoolId); + return mUserDatabaseHelper.setCurrentUser(currentUser); + }).doOnError(Throwable::printStackTrace); } public Single getCurrentUser() { diff --git a/app/src/main/java/org/schulcloud/mobile/data/local/UserDatabaseHelper.java b/app/src/main/java/org/schulcloud/mobile/data/local/UserDatabaseHelper.java index abbbea57..ce9ced94 100644 --- a/app/src/main/java/org/schulcloud/mobile/data/local/UserDatabaseHelper.java +++ b/app/src/main/java/org/schulcloud/mobile/data/local/UserDatabaseHelper.java @@ -4,6 +4,7 @@ import org.schulcloud.mobile.data.model.CurrentUser; import org.schulcloud.mobile.data.model.User; +import java.util.Arrays; import java.util.Collection; import java.util.List; @@ -12,6 +13,7 @@ import javax.inject.Singleton; import io.realm.Realm; +import io.realm.RealmResults; import rx.Observable; import rx.Single; import timber.log.Timber; @@ -49,6 +51,7 @@ public Observable> getUsers() { .map(users -> realm.copyFromRealm(users)); } + public Observable setAccessToken(final AccessToken newAccessToken) { return Observable.create(subscriber -> { if (subscriber.isUnsubscribed()) diff --git a/app/src/main/java/org/schulcloud/mobile/data/model/Account.java b/app/src/main/java/org/schulcloud/mobile/data/model/Account.java new file mode 100644 index 00000000..c1bc88e0 --- /dev/null +++ b/app/src/main/java/org/schulcloud/mobile/data/model/Account.java @@ -0,0 +1,45 @@ +package org.schulcloud.mobile.data.model; + +import io.realm.RealmModel; + +import io.realm.annotations.PrimaryKey; +import io.realm.annotations.RealmClass; + +@RealmClass +public class Account implements RealmModel { + @PrimaryKey + public String _id; + public String username; + public String password; + public String updatedAt; + public String createdAt; + public User userId; + + + public String get_id() { + return _id; + } + + public void set_id(String _id) { + this._id = _id; + } + + public void setUsername(String username){this.username = username;} + + public String getUsername(){return this.username;} + + public void setPassword(String password){} + + public String getUpdatedAt(){return this.updatedAt;} + + public void setUpdatedAt(String updatedAt){this.updatedAt = updatedAt;} + + public String getCreatedAt(){return this.createdAt;} + + public void setCreatedAt(String createdAt){this.createdAt = createdAt;} + + public User getUserId(){return this.userId;} + + public void setUserId(User userId){this.userId = userId;} + +} diff --git a/app/src/main/java/org/schulcloud/mobile/data/model/requestBodies/ResetData.java b/app/src/main/java/org/schulcloud/mobile/data/model/requestBodies/ResetData.java new file mode 100644 index 00000000..60c3110a --- /dev/null +++ b/app/src/main/java/org/schulcloud/mobile/data/model/requestBodies/ResetData.java @@ -0,0 +1,11 @@ +package org.schulcloud.mobile.data.model.requestBodies; + +public class ResetData { + String accountId; + String password; + + public ResetData(String accountId,String password){ + this.accountId = accountId; + this.password = password; + } +} diff --git a/app/src/main/java/org/schulcloud/mobile/data/model/requestBodies/ResetRequest.java b/app/src/main/java/org/schulcloud/mobile/data/model/requestBodies/ResetRequest.java new file mode 100644 index 00000000..0c039495 --- /dev/null +++ b/app/src/main/java/org/schulcloud/mobile/data/model/requestBodies/ResetRequest.java @@ -0,0 +1,9 @@ +package org.schulcloud.mobile.data.model.requestBodies; + +public class ResetRequest { + public String username; + + public ResetRequest(String username){ + this.username = username; + } +} diff --git a/app/src/main/java/org/schulcloud/mobile/data/model/responseBodies/ResetResponse.java b/app/src/main/java/org/schulcloud/mobile/data/model/responseBodies/ResetResponse.java new file mode 100644 index 00000000..52c513cb --- /dev/null +++ b/app/src/main/java/org/schulcloud/mobile/data/model/responseBodies/ResetResponse.java @@ -0,0 +1,9 @@ +package org.schulcloud.mobile.data.model.responseBodies; + +public class ResetResponse { + public String _id; + public String account; + public String updatedAt; + public String createdAt; + public boolean changed; +} diff --git a/app/src/main/java/org/schulcloud/mobile/data/remote/RestService.java b/app/src/main/java/org/schulcloud/mobile/data/remote/RestService.java index de0f25dc..f5de3db9 100644 --- a/app/src/main/java/org/schulcloud/mobile/data/remote/RestService.java +++ b/app/src/main/java/org/schulcloud/mobile/data/remote/RestService.java @@ -1,6 +1,7 @@ package org.schulcloud.mobile.data.remote; import org.schulcloud.mobile.data.model.AccessToken; +import org.schulcloud.mobile.data.model.Account; import org.schulcloud.mobile.data.model.Course; import org.schulcloud.mobile.data.model.CurrentUser; import org.schulcloud.mobile.data.model.Device; @@ -18,12 +19,15 @@ import org.schulcloud.mobile.data.model.requestBodies.Credentials; import org.schulcloud.mobile.data.model.requestBodies.DeviceRequest; import org.schulcloud.mobile.data.model.requestBodies.FeedbackRequest; +import org.schulcloud.mobile.data.model.requestBodies.ResetData; +import org.schulcloud.mobile.data.model.requestBodies.ResetRequest; import org.schulcloud.mobile.data.model.requestBodies.SignedUrlRequest; import org.schulcloud.mobile.data.model.responseBodies.AddHomeworkResponse; import org.schulcloud.mobile.data.model.responseBodies.DeviceResponse; import org.schulcloud.mobile.data.model.responseBodies.FeathersResponse; import org.schulcloud.mobile.data.model.responseBodies.FeedbackResponse; import org.schulcloud.mobile.data.model.responseBodies.FilesResponse; +import org.schulcloud.mobile.data.model.responseBodies.ResetResponse; import org.schulcloud.mobile.data.model.responseBodies.SignedUrlResponse; import java.util.List; @@ -51,9 +55,19 @@ public interface RestService { @GET("users/{userId}") Observable getUser(@Header("Authorization") String accessToken, @Path("userId") String userId); + + @GET("accounts/{accountId}?$populate[0]=userId") + Observable getAccount(@Header("Authorization") String accessToken, @Path("accountId") String accountId); + @POST("authentication") Observable signIn(@Body Credentials credentials); + @POST("passwordRecovery") + Observable passwordRecovery(@Header("Authorization") String accessToken, @Body ResetRequest username); + + @POST("passwordRecovery/reset") + Observable passwordReset(@Header("Authorization") String accessToken, @Body ResetData data); + // todo: move Authorization-Header to somewhere better @GET("fileStorage") Observable getFiles(@Header("Authorization") String accessToken, @Query("path") String path); diff --git a/app/src/main/java/org/schulcloud/mobile/injection/component/ActivityComponent.java b/app/src/main/java/org/schulcloud/mobile/injection/component/ActivityComponent.java index 6284f6f4..db38a310 100644 --- a/app/src/main/java/org/schulcloud/mobile/injection/component/ActivityComponent.java +++ b/app/src/main/java/org/schulcloud/mobile/injection/component/ActivityComponent.java @@ -2,6 +2,7 @@ import org.schulcloud.mobile.injection.module.ActivityModule; import org.schulcloud.mobile.injection.scope.PerActivity; +import org.schulcloud.mobile.ui.PasswordRecovery.PasswordRecovery; import org.schulcloud.mobile.ui.courses.CourseFragment; import org.schulcloud.mobile.ui.courses.detailed.DetailedCourseFragment; import org.schulcloud.mobile.ui.courses.topic.ContentAdapter; @@ -35,6 +36,9 @@ public interface ActivityComponent { // Sign in void inject(SignInActivity signInActivity); + // Password Recovery + void inject(PasswordRecovery passwordRecovery); + // Main void inject(MainActivity mainActivity); diff --git a/app/src/main/java/org/schulcloud/mobile/ui/PasswordRecovery/PasswordRecovery.java b/app/src/main/java/org/schulcloud/mobile/ui/PasswordRecovery/PasswordRecovery.java new file mode 100644 index 00000000..3bf32044 --- /dev/null +++ b/app/src/main/java/org/schulcloud/mobile/ui/PasswordRecovery/PasswordRecovery.java @@ -0,0 +1,167 @@ +package org.schulcloud.mobile.ui.PasswordRecovery; + +import android.content.Intent; +import android.os.Bundle; +import android.text.TextUtils; +import android.view.View; +import android.widget.Button; +import android.widget.EditText; +import android.widget.TextView; + +import org.schulcloud.mobile.R; +import org.schulcloud.mobile.data.datamanagers.FeedbackDataManager; +import org.schulcloud.mobile.data.datamanagers.UserDataManager; +import org.schulcloud.mobile.data.model.Account; +import org.schulcloud.mobile.data.model.requestBodies.FeedbackRequest; +import org.schulcloud.mobile.data.model.responseBodies.ResetResponse; +import org.schulcloud.mobile.data.sync.UserSyncService; +import org.schulcloud.mobile.ui.base.BaseActivity; +import org.schulcloud.mobile.ui.signin.SignInActivity; +import org.schulcloud.mobile.util.WebUtil; + +import javax.inject.Inject; + +import butterknife.BindView; +import butterknife.ButterKnife; + + +public class PasswordRecovery extends BaseActivity implements PasswordRecoveryMvpView { + @Inject + UserDataManager mUserDataManager; + @Inject + FeedbackDataManager mFeedbackDataManager; + + @Inject + PasswordRecoveryPresenter mPasswordRecoveryPresenter; + + @BindView(R.id.btn_recovery_input_confirm) + Button recoveryButton; + @BindView(R.id.recovery_input_emailUser) + EditText input_emailUser; + @BindView(R.id.recovery_error_message) + TextView errorMessageView; + @BindView(R.id.recovery_code_input) + EditText code_input; + @BindView(R.id.recovery_pw_input) + EditText pw_input; + @BindView(R.id.recovery_pw_input_repeat) + EditText pw_input_repeat; + + private int step = 0; + + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + activityComponent().inject(this); + setContentView(R.layout.activity_password_recovery); + setPresenter(mPasswordRecoveryPresenter); + ButterKnife.bind(this); + setStep(0); + this.startService(UserSyncService.getStartIntent(this)); + recoveryButton.setOnClickListener(v -> executeStep()); + } + + private void executeStep(){ + if(getStep() == 0) { + RequestNewPassword(input_emailUser.getText().toString()); + }else if(getStep() == 1){ + checkCode(code_input.getText().toString()); + }else if(getStep() == 2){ + checkPasswords(pw_input.getText().toString(), pw_input_repeat.getText().toString()); + } else { + startActivity(new Intent(this, SignInActivity.class)); + } + } + + private void setMessageText(String messageText, int color){ + errorMessageView.setText(messageText); + errorMessageView.setTextColor(color); + } + + private void RequestNewPassword(String email) { + mPasswordRecoveryPresenter.requestReset(email); + } + + private void checkCode(String code){ + mPasswordRecoveryPresenter.checkCode(code); + } + private void checkPasswords(String pw1, String pw2) { + if (!mPasswordRecoveryPresenter.isNewPasswortValid(pw1)) + showErrorMessage(false); + else if (!mPasswordRecoveryPresenter.passwordsEqual(pw1, pw2)) + showErrorMessage(true); + else + mPasswordRecoveryPresenter.setPassword(pw1); + } + + public void setStep(int step) { + if (step == 0) { + this.input_emailUser.setVisibility(View.VISIBLE); + this.code_input.setVisibility(View.GONE); + this.pw_input.setVisibility(View.GONE); + this.pw_input_repeat.setVisibility(View.GONE); + } else if (step == 1) { + this.input_emailUser.setVisibility(View.GONE); + this.code_input.setVisibility(View.VISIBLE); + this.pw_input.setVisibility(View.GONE); + this.pw_input_repeat.setVisibility(View.GONE); + } else if (step == 2) { + this.input_emailUser.setVisibility(View.GONE); + this.code_input.setVisibility(View.GONE); + this.pw_input.setVisibility(View.VISIBLE); + this.pw_input_repeat.setVisibility(View.VISIBLE); + } else if (step == 3) { + this.input_emailUser.setVisibility(View.GONE); + this.code_input.setVisibility(View.GONE); + this.pw_input.setVisibility(View.GONE); + this.pw_input_repeat.setVisibility(View.GONE); + } + this.step = step; + } + + public int getStep(){ + return this.step; + } + + @Override + public void showSuccessMessage() { + int color = getResources().getColor(R.color.hpiYellow); + if(getStep() == 0) + setMessageText(getResources().getString(R.string.recovery_success_request), color); + else if(getStep()==1) + setMessageText(getResources().getString(R.string.recovery_success_code_input), color); + else if(getStep()==3) { + setMessageText(getResources().getString(R.string.recovery_success), color); + recoveryButton.setText(R.string.recovery_button_done); + } + } + + @Override + public void showErrorMessage(boolean subStep) { + int color = getResources().getColor(R.color.hpiRed); + if (getStep() == 0) + setMessageText(getResources().getString(R.string.recovery_errorMessage_Email), color); + else if (getStep() == 1) + setMessageText(getResources().getString(R.string.recovery_errorMessage_code_input), color); + else if (getStep() == 2) + if (!subStep) + setMessageText(getResources() + .getString(R.string.recovery_errorMessage_pw_fehlende_zeichen), color); + else + setMessageText(getResources().getString(R.string.recovery_errorMessage_pw_ungleich), color); + else + setMessageText(getResources().getString(R.string.recovery_errorMessage_final_error), color); + } + + @Override + public void sendEmail(ResetResponse responseBody, Account account){ + String Code = responseBody._id; + String text = getResources().getString(R.string.recovery_email_text); + String subject = getResources().getString(R.string.recovery_reset_email_subject); + + String username = TextUtils.isEmpty(account.userId.firstName) ? account.username : account.userId.firstName + " " + account.userId.lastName; + text = String.format(text, username, Code, WebUtil.URL_BASE + "/pwrecovery/" + Code); + mFeedbackDataManager.sendEmail(new FeedbackRequest(text, subject, account.userId.email)); + } +} diff --git a/app/src/main/java/org/schulcloud/mobile/ui/PasswordRecovery/PasswordRecoveryMvpView.java b/app/src/main/java/org/schulcloud/mobile/ui/PasswordRecovery/PasswordRecoveryMvpView.java new file mode 100644 index 00000000..b63e37c0 --- /dev/null +++ b/app/src/main/java/org/schulcloud/mobile/ui/PasswordRecovery/PasswordRecoveryMvpView.java @@ -0,0 +1,12 @@ +package org.schulcloud.mobile.ui.PasswordRecovery; + + import org.schulcloud.mobile.data.model.Account; + import org.schulcloud.mobile.data.model.responseBodies.ResetResponse; + import org.schulcloud.mobile.ui.base.MvpView; + +public interface PasswordRecoveryMvpView extends MvpView { + void showSuccessMessage(); + void showErrorMessage(boolean subStep); + void setStep(int step); + void sendEmail(ResetResponse responseBody, Account account); +} diff --git a/app/src/main/java/org/schulcloud/mobile/ui/PasswordRecovery/PasswordRecoveryPresenter.java b/app/src/main/java/org/schulcloud/mobile/ui/PasswordRecovery/PasswordRecoveryPresenter.java new file mode 100644 index 00000000..4f7308a1 --- /dev/null +++ b/app/src/main/java/org/schulcloud/mobile/ui/PasswordRecovery/PasswordRecoveryPresenter.java @@ -0,0 +1,101 @@ +package org.schulcloud.mobile.ui.PasswordRecovery; + + +import android.support.annotation.NonNull; +import android.text.TextUtils; + +import org.schulcloud.mobile.data.datamanagers.UserDataManager; +import org.schulcloud.mobile.data.model.Account; +import org.schulcloud.mobile.data.model.responseBodies.ResetResponse; +import org.schulcloud.mobile.injection.ConfigPersistent; +import org.schulcloud.mobile.ui.base.BasePresenter; + +import javax.inject.Inject; + +import rx.android.schedulers.AndroidSchedulers; + +@ConfigPersistent +public class PasswordRecoveryPresenter extends BasePresenter { + private final UserDataManager mUserDataManager; + private Account resetAccount = null; + private ResetResponse resetResponse = null; + + @Inject + public PasswordRecoveryPresenter(UserDataManager userDataManager) { + mUserDataManager = userDataManager; + } + + public void requestReset(@NonNull String email) { + mUserDataManager.requestResetPassword(email) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + responseBody -> sendToView(view -> { + resetResponse = responseBody; + mUserDataManager.getAccount(responseBody.account) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(response -> { + resetAccount = response; + view.sendEmail(responseBody, getAccount()); + view.showSuccessMessage(); + view.setStep(1); + }, error -> System.out.print(error.getMessage())); + }), + error -> sendToView(view -> view.showErrorMessage(false))); + } + + public void setPassword(String password) { + mUserDataManager.resetPassword(resetAccount._id, password) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(responseBody -> sendToView(view -> { + view.setStep(3); + view.showSuccessMessage(); + }), error -> sendToView(view -> view.showErrorMessage(false))); + } + + public void checkCode(String code) { + final String rightCode = resetResponse._id; + sendToView(v -> { + if (code.equals(rightCode)) { + v.showSuccessMessage(); + v.setStep(2); + } else + v.showErrorMessage(false); + }); + } + public boolean passwordsEqual(String pw1, String pw2) { + return TextUtils.equals(pw1, pw2); + } + public boolean isNewPasswortValid(String passwort){ + int pwSize = passwort.length(); + int capChars = charsInInterval(passwort,'A','Z'); + int smallChars = charsInInterval(passwort,'a','z'); + int digits = charsInInterval(passwort,'0','9'); + int specialChars = pwSize - capChars - smallChars - digits; + + if (pwSize < 8) + return false; + if (capChars < 1) + return false; + if (smallChars < 1) + return false; + if (digits < 1) + return false; + if (specialChars < 1) + return false; + + return true; + } + private int charsInInterval(String pw,char min,char max){ + int amount = 0; + for(char c : pw.toCharArray()){ + if(c>=min && c<=max){ + amount++; + } + } + return amount; + } + + private Account getAccount(){ + return this.resetAccount; + } +} diff --git a/app/src/main/java/org/schulcloud/mobile/ui/signin/SignInActivity.java b/app/src/main/java/org/schulcloud/mobile/ui/signin/SignInActivity.java index 673c2467..c79cdd92 100644 --- a/app/src/main/java/org/schulcloud/mobile/ui/signin/SignInActivity.java +++ b/app/src/main/java/org/schulcloud/mobile/ui/signin/SignInActivity.java @@ -6,6 +6,7 @@ import android.widget.EditText; import org.schulcloud.mobile.R; +import org.schulcloud.mobile.ui.PasswordRecovery.PasswordRecovery; import org.schulcloud.mobile.ui.base.BaseActivity; import org.schulcloud.mobile.ui.main.MainActivity; import org.schulcloud.mobile.util.dialogs.DialogFactory; @@ -31,6 +32,8 @@ public class SignInActivity extends BaseActivity Button demoStudent; @BindView(R.id.btn_demo_teacher) Button demoTeacher; + @BindView(R.id.btn_password_recovery) + Button recoverPassword; @Override protected void onCreate(Bundle savedInstanceState) { @@ -50,6 +53,13 @@ protected void onCreate(Bundle savedInstanceState) { demoTeacher.setOnClickListener(v -> mSignInPresenter.signIn( getString(R.string.login_demo_teacher_username), getString(R.string.login_demo_teacher_password), true)); + recoverPassword.setOnClickListener(v -> StartPasswordRecovery()); + + } + + private void StartPasswordRecovery(){ + Intent recoveryIntent = new Intent(this,PasswordRecovery.class); + startActivity(recoveryIntent); } diff --git a/app/src/main/res/layouts/courses/layout/activity_password_recovery.xml b/app/src/main/res/layouts/courses/layout/activity_password_recovery.xml new file mode 100644 index 00000000..8afea5e7 --- /dev/null +++ b/app/src/main/res/layouts/courses/layout/activity_password_recovery.xml @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + +