Browse Source

[1001694]增加登录界面

zk 1 year ago
parent
commit
0db7a47760
25 changed files with 900 additions and 33 deletions
  1. 3 0
      app/src/main/AndroidManifest.xml
  2. 14 0
      app/src/main/java/com/datarecovery/master/data/api/AtmobApi.java
  3. 19 0
      app/src/main/java/com/datarecovery/master/data/api/request/BaseRequest.java
  4. 18 0
      app/src/main/java/com/datarecovery/master/data/api/request/LoginRequest.java
  5. 14 0
      app/src/main/java/com/datarecovery/master/data/api/request/SendCodeRequest.java
  6. 13 0
      app/src/main/java/com/datarecovery/master/data/api/response/LoginResponse.java
  7. 5 0
      app/src/main/java/com/datarecovery/master/data/consts/ErrorCode.java
  8. 62 1
      app/src/main/java/com/datarecovery/master/data/repositories/AccountRepository.java
  9. 0 24
      app/src/main/java/com/datarecovery/master/di/GsonModule.java
  10. 2 2
      app/src/main/java/com/datarecovery/master/dialog/AgreementDialog.java
  11. 4 0
      app/src/main/java/com/datarecovery/master/module/browser/BrowserActivity.java
  12. 67 0
      app/src/main/java/com/datarecovery/master/module/login/LoginActivity.java
  13. 220 0
      app/src/main/java/com/datarecovery/master/module/login/LoginViewModel.java
  14. 2 1
      app/src/main/java/com/datarecovery/master/module/mine/MineViewModel.java
  15. 122 0
      app/src/main/java/com/datarecovery/master/utils/SpannableUtil.java
  16. BIN
      app/src/main/res/drawable-xxhdpi/icon_login_check_box_checked.webp
  17. BIN
      app/src/main/res/drawable-xxhdpi/icon_login_check_box_unchecked.webp
  18. 18 4
      app/src/main/res/drawable/bg_common_btn.xml
  19. 5 0
      app/src/main/res/drawable/bg_login_agree_check_box.xml
  20. 5 0
      app/src/main/res/drawable/bg_login_print.xml
  21. 5 0
      app/src/main/res/drawable/bg_login_send_code.xml
  22. 5 0
      app/src/main/res/drawable/login_disable_bg.xml
  23. 276 0
      app/src/main/res/layout/activity_login.xml
  24. 1 1
      app/src/main/res/values/colors.xml
  25. 20 0
      app/src/main/res/values/strings.xml

+ 3 - 0
app/src/main/AndroidManifest.xml

@@ -50,6 +50,9 @@
         <activity
             android:name=".module.feedback.UserFeedbackActivity"
             android:screenOrientation="portrait" />
+        <activity
+            android:name=".module.login.LoginActivity"
+            android:screenOrientation="portrait" />
 
     </application>
 

+ 14 - 0
app/src/main/java/com/datarecovery/master/data/api/AtmobApi.java

@@ -1,7 +1,21 @@
 package com.datarecovery.master.data.api;
 
 
+import com.atmob.app.lib.base.BaseResponse;
+import com.datarecovery.master.data.api.request.LoginRequest;
+import com.datarecovery.master.data.api.request.SendCodeRequest;
+import com.datarecovery.master.data.api.response.LoginResponse;
+
+import atmob.reactivex.rxjava3.core.Single;
+import atmob.retrofit2.http.Body;
+import atmob.retrofit2.http.POST;
+
 public interface AtmobApi {
 
 
+    @POST("/s/v1/user/code")
+    Single<BaseResponse<Object>> loginSendCode(@Body SendCodeRequest request);
+
+    @POST("/s/v1/user/login")
+    Single<BaseResponse<LoginResponse>> loginUserLogin(@Body LoginRequest request);
 }

+ 19 - 0
app/src/main/java/com/datarecovery/master/data/api/request/BaseRequest.java

@@ -0,0 +1,19 @@
+package com.datarecovery.master.data.api.request;
+
+import com.atmob.user.param.AtmobParams;
+import com.datarecovery.master.data.repositories.AccountRepository;
+import com.google.gson.annotations.SerializedName;
+
+public class BaseRequest extends AtmobParams {
+
+    @SerializedName("appPlatform")
+    private final int appPlatform;
+
+    @SerializedName("authToken")
+    private String authToken;
+
+    public BaseRequest() {
+        this.appPlatform = 1;
+        this.authToken = AccountRepository.token;
+    }
+}

+ 18 - 0
app/src/main/java/com/datarecovery/master/data/api/request/LoginRequest.java

@@ -0,0 +1,18 @@
+package com.datarecovery.master.data.api.request;
+
+import com.google.gson.annotations.SerializedName;
+
+public class LoginRequest extends BaseRequest {
+
+
+    @SerializedName("phone")
+    private final String phoneNum;
+
+    @SerializedName("code")
+    private final String verificationCode;
+
+    public LoginRequest(String phoneNum, String verificationCode) {
+        this.phoneNum = phoneNum;
+        this.verificationCode = verificationCode;
+    }
+}

+ 14 - 0
app/src/main/java/com/datarecovery/master/data/api/request/SendCodeRequest.java

@@ -0,0 +1,14 @@
+package com.datarecovery.master.data.api.request;
+
+import com.google.gson.annotations.SerializedName;
+
+public class SendCodeRequest extends BaseRequest {
+
+    @SerializedName("phone")
+    private final String phoneNum;
+
+
+    public SendCodeRequest(String phoneNum) {
+        this.phoneNum = phoneNum;
+    }
+}

+ 13 - 0
app/src/main/java/com/datarecovery/master/data/api/response/LoginResponse.java

@@ -0,0 +1,13 @@
+package com.datarecovery.master.data.api.response;
+
+import com.google.gson.annotations.SerializedName;
+
+public class LoginResponse {
+
+    @SerializedName("authToken")
+    private String token;
+
+    public String getToken() {
+        return token;
+    }
+}

+ 5 - 0
app/src/main/java/com/datarecovery/master/data/consts/ErrorCode.java

@@ -0,0 +1,5 @@
+package com.datarecovery.master.data.consts;
+
+public interface ErrorCode {
+    int ERROR_CODE_VERIFICATION_CODE_ERROR = 1005;
+}

+ 62 - 1
app/src/main/java/com/datarecovery/master/data/repositories/AccountRepository.java

@@ -6,13 +6,22 @@ import androidx.lifecycle.LiveData;
 import androidx.lifecycle.MutableLiveData;
 import androidx.lifecycle.Transformations;
 
+import com.atmob.app.lib.handler.RxHttpHandler;
 import com.atmob.common.data.KVUtils;
 import com.atmob.common.logging.AtmobLog;
+import com.datarecovery.master.data.api.AtmobApi;
+import com.datarecovery.master.data.api.request.LoginRequest;
+import com.datarecovery.master.data.api.request.SendCodeRequest;
+import com.datarecovery.master.data.api.response.LoginResponse;
+import com.datarecovery.master.data.consts.ErrorCode;
 import com.datarecovery.master.utils.BoxingUtil;
 
 import javax.inject.Inject;
 import javax.inject.Singleton;
 
+import atmob.reactivex.rxjava3.core.Single;
+import atmob.rxjava.utils.RxJavaUtil;
+
 @Singleton
 public class AccountRepository {
 
@@ -24,13 +33,19 @@ public class AccountRepository {
     private final LiveData<Boolean> isLogin = Transformations.map(loginPhoneNum, phoneNum -> !TextUtils.isEmpty(phoneNum));
     private final MutableLiveData<?> memberStatusInfo = new MutableLiveData<>();
     public static String token;
+    private final AtmobApi atmobApi;
+
+    private int errorCodeTimes;
+
+    private long lastRequestCodeTime;
 
     static {
         token = KVUtils.getDefault().getString(KEY_LOGIN_TOKEN, null);
     }
 
     @Inject
-    public AccountRepository() {
+    public AccountRepository(AtmobApi atmobApi) {
+        this.atmobApi = atmobApi;
         loginPhoneNum.setValue(KVUtils.getDefault().getString(KEY_LOGIN_PHONE_NUM, null));
         isLogin.observeForever(isLogin -> AtmobLog.d(TAG, "isLogin: " + isLogin));
     }
@@ -51,6 +66,44 @@ public class AccountRepository {
 
     }
 
+
+    public Single<?> requestUserCode(String phoneNum) {
+        long currentTime = System.currentTimeMillis();
+        if (currentTime - lastRequestCodeTime < 60 * 1000) {
+            return Single.error(new RequestCodeTooOftenException());
+        }
+        return atmobApi.loginSendCode(new SendCodeRequest(phoneNum))
+                .compose(RxHttpHandler.handle(true))
+                .compose(RxJavaUtil.SingleSchedule.io2Main())
+                .doOnSuccess(o -> {
+                    lastRequestCodeTime = currentTime;
+                    errorCodeTimes = 0;
+                });
+    }
+
+    public Single<LoginResponse> login(String phoneNum, String verificationCode) {
+        if (errorCodeTimes >= 5) {
+            return Single.error(new LoginTooOftenException());
+        }
+        LoginRequest loginRequest = new LoginRequest(phoneNum, verificationCode);
+        return atmobApi.loginUserLogin(loginRequest)
+                .compose(RxHttpHandler.handle(true))
+                .compose(RxJavaUtil.SingleSchedule.io2Main())
+                .doOnSuccess(response -> {
+                    errorCodeTimes = 0;
+                    onLoginSuccess(phoneNum, response.getToken());
+                })
+                .doOnError(throwable -> {
+                    RxHttpHandler.ServerErrorException serverErrorException
+                            = throwable instanceof RxHttpHandler.ServerErrorException ? ((RxHttpHandler.ServerErrorException) throwable) : null;
+                    if (serverErrorException != null) {
+                        if (serverErrorException.getCode() == ErrorCode.ERROR_CODE_VERIFICATION_CODE_ERROR) {
+                            errorCodeTimes++;
+                        }
+                    }
+                });
+    }
+
     public void logout() {
         if (!BoxingUtil.boxing(isLogin.getValue())) {
             return;
@@ -63,4 +116,12 @@ public class AccountRepository {
             memberStatusInfo.postValue(null);
         }
     }
+
+
+    public static class RequestCodeTooOftenException extends Exception {
+    }
+
+    public static class LoginTooOftenException extends Exception {
+    }
+
 }

+ 0 - 24
app/src/main/java/com/datarecovery/master/di/GsonModule.java

@@ -1,24 +0,0 @@
-package com.datarecovery.master.di;
-
-import com.google.gson.Gson;
-
-import javax.inject.Singleton;
-
-import dagger.Module;
-import dagger.Provides;
-import dagger.hilt.InstallIn;
-import dagger.hilt.components.SingletonComponent;
-
-/**
- * 提供全局单例Gson对象, 充分利用其内部缓存机制, 提高序列化效率
- */
-@InstallIn(SingletonComponent.class)
-@Module
-public class GsonModule {
-
-    @Provides
-    @Singleton
-    public static Gson provideGson() {
-        return new Gson();
-    }
-}

+ 2 - 2
app/src/main/java/com/datarecovery/master/dialog/AgreementDialog.java

@@ -59,7 +59,7 @@ public class AgreementDialog extends BaseDialog<DialogAgreementBinding> {
 
             @Override
             public void updateDrawState(@NonNull TextPaint ds) {
-                ds.setColor(context.getResources().getColor(R.color.colorClickPrimary));
+                ds.setColor(context.getResources().getColor(R.color.colorPrimary));
             }
 
             @Override
@@ -71,7 +71,7 @@ public class AgreementDialog extends BaseDialog<DialogAgreementBinding> {
 
             @Override
             public void updateDrawState(@NonNull TextPaint ds) {
-                ds.setColor(context.getResources().getColor(R.color.colorClickPrimary));
+                ds.setColor(context.getResources().getColor(R.color.colorPrimary));
             }
 
 

+ 4 - 0
app/src/main/java/com/datarecovery/master/module/browser/BrowserActivity.java

@@ -1,6 +1,7 @@
 package com.datarecovery.master.module.browser;
 
 import android.annotation.SuppressLint;
+import android.app.Activity;
 import android.content.Context;
 import android.content.Intent;
 import android.net.Uri;
@@ -29,6 +30,9 @@ public class BrowserActivity extends BaseActivity<ActivityBrowserBinding> {
 
     public static void start(Context context, String url) {
         Intent intent = new Intent(context, BrowserActivity.class);
+        if (!(context instanceof Activity)) {
+            intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        }
         intent.putExtra(KEY_URL, url);
         context.startActivity(intent);
     }

+ 67 - 0
app/src/main/java/com/datarecovery/master/module/login/LoginActivity.java

@@ -0,0 +1,67 @@
+package com.datarecovery.master.module.login;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.View;
+
+import androidx.annotation.Nullable;
+
+import com.atmob.app.lib.base.BaseActivity;
+import com.atmob.common.runtime.ContextUtil;
+import com.datarecovery.master.R;
+import com.datarecovery.master.data.consts.Constants;
+import com.datarecovery.master.databinding.ActivityLoginBinding;
+import com.datarecovery.master.module.browser.BrowserActivity;
+import com.datarecovery.master.utils.SpannableUtil;
+
+import dagger.hilt.android.AndroidEntryPoint;
+
+@AndroidEntryPoint
+public class LoginActivity extends BaseActivity<ActivityLoginBinding> {
+
+
+    private LoginViewModel loginViewModel;
+
+
+    public static void start(Context context) {
+        Intent intent = new Intent(context, LoginActivity.class);
+        if (!(context instanceof Activity)) {
+            intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        }
+        context.startActivity(intent);
+    }
+
+
+    @Override
+    protected void onCreate(@Nullable Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        initView();
+        initObserver();
+    }
+
+    private void initView() {
+        String agreeText = getString(R.string.login_agree_text);
+        String userTermsText = getString(R.string.login_agree_user_terms_text);
+        String privacyPolicyText = getString(R.string.login_agree_privacy_policy_text);
+        SpannableUtil.getAgreementSpannableStringBuilder(binding.loginAgreeText, agreeText, new String[]{userTermsText, privacyPolicyText}, getResources().getColor(R.color.colorPrimary), false,
+                v -> BrowserActivity.start(getBaseContext(), Constants.PRIVACY_POLICY), v -> BrowserActivity.start(getBaseContext(), Constants.USER_AGREEMENT));
+    }
+
+    private void initObserver() {
+        loginViewModel.getFinishEvent().observe(this, o -> finish());
+    }
+
+    @Override
+    protected boolean shouldImmersion() {
+        return true;
+    }
+
+    @Override
+    protected void initViewModel() {
+        super.initViewModel();
+        loginViewModel = getViewModelProvider().get(LoginViewModel.class);
+        binding.setLoginViewModel(loginViewModel);
+    }
+}

+ 220 - 0
app/src/main/java/com/datarecovery/master/module/login/LoginViewModel.java

@@ -0,0 +1,220 @@
+package com.datarecovery.master.module.login;
+
+import android.content.Context;
+import android.text.TextUtils;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
+
+import com.atmob.app.lib.base.BaseViewModel;
+import com.atmob.app.lib.handler.RxHttpHandler;
+import com.atmob.app.lib.livedata.SingleLiveEvent;
+import com.atmob.common.runtime.ContextUtil;
+import com.datarecovery.master.R;
+import com.datarecovery.master.data.consts.ErrorCode;
+import com.datarecovery.master.data.repositories.AccountRepository;
+import com.datarecovery.master.utils.BoxingUtil;
+import com.datarecovery.master.utils.SpannableUtil;
+import com.datarecovery.master.utils.ToastUtil;
+
+import java.util.concurrent.TimeUnit;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import javax.inject.Inject;
+
+import atmob.reactivex.rxjava3.core.SingleObserver;
+import atmob.reactivex.rxjava3.disposables.Disposable;
+import atmob.rxjava.utils.RxJavaUtil;
+import dagger.hilt.android.lifecycle.HiltViewModel;
+
+
+@HiltViewModel
+public class LoginViewModel extends BaseViewModel {
+
+    private final SingleLiveEvent<Boolean> showLoading = new SingleLiveEvent<>();
+    private final MutableLiveData<String> phoneNum = new MutableLiveData<>();
+    private final MutableLiveData<Boolean> isRequestCodeCountdown = new MutableLiveData<>(false);
+    private final MutableLiveData<Boolean> isCheckedAgreement = new MutableLiveData<>(false);
+    private final MutableLiveData<String> requestCodeCountdown = new MutableLiveData<>();
+    private final MutableLiveData<String> verificationCode = new MutableLiveData<>();
+    private final MutableLiveData<CharSequence> agreeText = new MutableLiveData<>();
+    private final SingleLiveEvent<?> finishEvent = new SingleLiveEvent<>();
+    private final AccountRepository accountRepository;
+    private Disposable getCodeCountdownDisposable;
+
+    @Inject
+    public LoginViewModel(AccountRepository accountRepository) {
+        this.accountRepository = accountRepository;
+        init();
+    }
+
+
+    public LiveData<Boolean> getShowLoading() {
+        return showLoading;
+    }
+
+    public MutableLiveData<String> getPhoneNum() {
+        return phoneNum;
+    }
+
+    public MutableLiveData<String> getVerificationCode() {
+        return verificationCode;
+    }
+
+    public MutableLiveData<Boolean> getIsCheckedAgreement() {
+        return isCheckedAgreement;
+    }
+
+    public LiveData<Boolean> getIsRequestCodeCountdown() {
+        return isRequestCodeCountdown;
+    }
+
+    public LiveData<?> getFinishEvent() {
+        return finishEvent;
+    }
+
+    public LiveData<String> getRequestCodeCountdown() {
+        return requestCodeCountdown;
+    }
+
+    public LiveData<CharSequence> getAgreeText() {
+        return agreeText;
+    }
+
+    public void onBackClick() {
+        finishEvent.call();
+    }
+
+
+    private void init() {
+
+    }
+
+
+    public void onGetCodeClick() {
+        String phoneNumText = phoneNum.getValue();
+        if (isPhone(phoneNumText)) {
+            doRequestVerificationCode(phoneNumText);
+        } else {
+            ToastUtil.show(R.string.login_phone_num_11, ToastUtil.LENGTH_SHORT);
+        }
+    }
+
+    private void doRequestVerificationCode(String phoneNumText) {
+        if (BoxingUtil.boxing(isRequestCodeCountdown.getValue())) {
+            ToastUtil.show(R.string.login_request_code_frequently_toast, ToastUtil.LENGTH_SHORT);
+            return;
+        }
+        accountRepository.requestUserCode(phoneNumText)
+                .subscribe(new SingleObserver<Object>() {
+                    @Override
+                    public void onSubscribe(@NonNull Disposable d) {
+                        addDisposable(d);
+                        showLoading.setValue(true);
+                    }
+
+                    @Override
+                    public void onSuccess(@NonNull Object o) {
+                        startGetCodeCountdown();
+                        showLoading.setValue(false);
+                        ToastUtil.show(R.string.login_verification_code_request_success_toast, ToastUtil.LENGTH_SHORT);
+                    }
+
+                    @Override
+                    public void onError(@NonNull Throwable e) {
+                        showLoading.setValue(false);
+                        if (e instanceof AccountRepository.RequestCodeTooOftenException) {
+                            ToastUtil.show(R.string.login_request_code_frequently_toast, ToastUtil.LENGTH_SHORT);
+                            return;
+                        }
+                        e.printStackTrace();
+                        stopGetCodeCountdown();
+                        ToastUtil.show(R.string.login_verification_code_request_failed_toast, ToastUtil.LENGTH_SHORT);
+                    }
+                });
+    }
+
+    private void stopGetCodeCountdown() {
+        if (getCodeCountdownDisposable != null) {
+            getCodeCountdownDisposable.dispose();
+        }
+        isRequestCodeCountdown.setValue(false);
+    }
+
+    private void startGetCodeCountdown() {
+        if (getCodeCountdownDisposable != null) {
+            getCodeCountdownDisposable.dispose();
+        }
+        isRequestCodeCountdown.setValue(true);
+        getCodeCountdownDisposable = RxJavaUtil.interval(0, 1, 60, TimeUnit.SECONDS,
+                index -> requestCodeCountdown.setValue(String.valueOf(60 - index)), () -> isRequestCodeCountdown.setValue(false));
+
+        addDisposable(getCodeCountdownDisposable);
+    }
+
+    public void onLoginClick() {
+        if (!isCanLogin(phoneNum.getValue(), verificationCode.getValue())) {
+            return;
+        }
+        if (!BoxingUtil.boxing(isCheckedAgreement.getValue())) {
+            ToastUtil.show(R.string.login_please_agree, ToastUtil.LENGTH_SHORT);
+            return;
+        }
+        accountRepository.login(phoneNum.getValue(), verificationCode.getValue())
+                .subscribe(new SingleObserver<Object>() {
+                    @Override
+                    public void onSubscribe(@NonNull Disposable d) {
+                        addDisposable(d);
+                        showLoading.setValue(true);
+                    }
+
+                    @Override
+                    public void onSuccess(@NonNull Object o) {
+                        showLoading.setValue(false);
+                        ToastUtil.show(R.string.login_success, ToastUtil.LENGTH_SHORT);
+                        finishEvent.call();
+                    }
+
+                    @Override
+                    public void onError(@NonNull Throwable e) {
+                        showLoading.setValue(false);
+                        if (e instanceof AccountRepository.LoginTooOftenException) {
+                            ToastUtil.show(R.string.login_too_often_toast, ToastUtil.LENGTH_SHORT);
+                            return;
+                        }
+                        RxHttpHandler.ServerErrorException serverErrorException
+                                = e instanceof RxHttpHandler.ServerErrorException ? ((RxHttpHandler.ServerErrorException) e) : null;
+                        if (serverErrorException != null) {
+                            if (serverErrorException.getCode() == ErrorCode.ERROR_CODE_VERIFICATION_CODE_ERROR) {
+                                ToastUtil.show(R.string.login_verification_code_error_toast, ToastUtil.LENGTH_SHORT);
+                            } else {
+                                ToastUtil.show(R.string.login_failed_toast, ToastUtil.LENGTH_SHORT);
+                            }
+                        } else {
+                            ToastUtil.show(R.string.login_failed_toast, ToastUtil.LENGTH_SHORT);
+                        }
+                    }
+                });
+    }
+
+
+    public boolean isCanLogin(String phoneNum, String verificationCode) {
+        return isPhone(phoneNum) && verificationCode != null && verificationCode.length() > 0;
+    }
+
+    private boolean isPhone(String phoneNumText) {
+        Pattern phonePattern = Pattern.compile("^1\\d{10}$");
+        if (phoneNumText == null) {
+            return false;
+        }
+        phoneNumText = phoneNumText.trim();
+        if (TextUtils.isEmpty(phoneNumText)) {
+            return false;
+        }
+        Matcher matcher = phonePattern.matcher(phoneNumText);
+        return matcher.matches();
+    }
+}

+ 2 - 1
app/src/main/java/com/datarecovery/master/module/mine/MineViewModel.java

@@ -13,6 +13,7 @@ import com.datarecovery.master.R;
 import com.datarecovery.master.data.repositories.AccountRepository;
 import com.datarecovery.master.module.about.AboutActivity;
 import com.datarecovery.master.module.feedback.UserFeedbackActivity;
+import com.datarecovery.master.module.login.LoginActivity;
 
 import javax.inject.Inject;
 
@@ -59,7 +60,7 @@ public class MineViewModel extends BaseViewModel {
     }
 
     public void onLoginClick() {
-
+        LoginActivity.start(ActivityUtil.getTopActivity());
     }
 
     public void onAboutClick() {

+ 122 - 0
app/src/main/java/com/datarecovery/master/utils/SpannableUtil.java

@@ -0,0 +1,122 @@
+package com.datarecovery.master.utils;
+
+import android.text.SpannableStringBuilder;
+import android.text.Spanned;
+import android.text.TextPaint;
+import android.text.method.LinkMovementMethod;
+import android.text.style.ClickableSpan;
+import android.view.View;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+
+public class SpannableUtil {
+
+    public static SpannableStringBuilder getSpannableStringBuilder(SpannableStringBuilder spannableStringBuilder, String targetTxt, int color, boolean isLine) {
+        if (spannableStringBuilder == null) {
+            return null;
+        }
+        int targetTextStart = spannableStringBuilder.toString().indexOf(targetTxt);
+        ClickableSpan targetSpan = new ClickableSpan() {
+
+            @Override
+            public void onClick(@NonNull View widget) {
+
+            }
+
+            @Override
+            public void updateDrawState(@NonNull TextPaint ds) {
+                ds.setColor(color);
+                ds.setUnderlineText(isLine);
+            }
+        };
+        spannableStringBuilder.setSpan(targetSpan, targetTextStart,
+                targetTextStart + targetTxt.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
+        return spannableStringBuilder;
+    }
+
+    public static SpannableStringBuilder getSpannableStringBuilder(String allTxt, String targetTxt, int color, boolean isLine) {
+        SpannableStringBuilder spannableStringBuilder = new SpannableStringBuilder(allTxt);
+        int targetTextStart = allTxt.indexOf(targetTxt);
+        ClickableSpan targetSpan = new ClickableSpan() {
+
+            @Override
+            public void onClick(@NonNull View widget) {
+
+            }
+
+            @Override
+            public void updateDrawState(@NonNull TextPaint ds) {
+                ds.setColor(color);
+                ds.setUnderlineText(isLine);
+            }
+        };
+        spannableStringBuilder.setSpan(targetSpan, targetTextStart,
+                targetTextStart + targetTxt.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
+        return spannableStringBuilder;
+    }
+
+
+    public static SpannableStringBuilder getSpannableStringBuilder(String allTxt, String[] targetTxt, int color, boolean isLine) {
+        SpannableStringBuilder spannableStringBuilder = new SpannableStringBuilder(allTxt);
+        int[] targetTextStart = new int[targetTxt.length];
+        for (int i = 0; i < targetTxt.length; i++) {
+            targetTextStart[i] = allTxt.indexOf(targetTxt[i]);
+        }
+        for (int i = 0; i < targetTxt.length; i++) {
+            ClickableSpan targetSpan = new ClickableSpan() {
+                @Override
+                public void onClick(@NonNull View widget) {
+
+                }
+
+                @Override
+                public void updateDrawState(@NonNull TextPaint ds) {
+                    ds.setColor(color);
+                    ds.setUnderlineText(isLine);
+                }
+            };
+            if (targetTextStart[i] < 0) {
+                continue;
+            }
+            spannableStringBuilder.setSpan(targetSpan, targetTextStart[i],
+                    targetTextStart[i] + targetTxt[i].length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
+        }
+        return spannableStringBuilder;
+    }
+
+
+    public static void getAgreementSpannableStringBuilder(TextView targetView, String allTxt, String[] targetTxt, int color, boolean isLine, View.OnClickListener... clickListener) {
+        if (clickListener.length > 0)
+            targetView.setMovementMethod(LinkMovementMethod.getInstance());
+        SpannableStringBuilder spannableStringBuilder = new SpannableStringBuilder(allTxt);
+        int[] targetTextStart = new int[targetTxt.length];
+        for (int i = 0; i < targetTxt.length; i++) {
+            targetTextStart[i] = allTxt.indexOf(targetTxt[i]);
+        }
+
+        for (int i = 0; i < targetTxt.length; i++) {
+            int finalI = i;
+            ClickableSpan targetSpan = new ClickableSpan() {
+                @Override
+                public void onClick(@NonNull View widget) {
+                    if (finalI < clickListener.length && clickListener[finalI] != null) {
+                        clickListener[finalI].onClick(widget);
+                    }
+                }
+
+                @Override
+                public void updateDrawState(@NonNull TextPaint ds) {
+                    ds.setColor(color);
+                    ds.setUnderlineText(isLine);
+                }
+            };
+            if (targetTextStart[i] < 0) {
+                continue;
+            }
+            spannableStringBuilder.setSpan(targetSpan, targetTextStart[i],
+                    targetTextStart[i] + targetTxt[i].length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
+        }
+        targetView.setText(spannableStringBuilder);
+    }
+}

BIN
app/src/main/res/drawable-xxhdpi/icon_login_check_box_checked.webp


BIN
app/src/main/res/drawable-xxhdpi/icon_login_check_box_unchecked.webp


+ 18 - 4
app/src/main/res/drawable/bg_common_btn.xml

@@ -1,5 +1,19 @@
 <?xml version="1.0" encoding="utf-8"?>
-<shape xmlns:android="http://schemas.android.com/apk/res/android">
-    <corners android:radius="8dp" />
-    <solid android:color="@color/colorPrimary" />
-</shape>
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+    <item>
+        <shape android:shape="rectangle">
+            <solid android:color="@color/colorPrimary" />
+            <corners android:radius="8dp" />
+        </shape>
+    </item>
+    <item>
+        <ripple android:color="@color/colorClickPrimary">
+            <item android:id="@android:id/mask">
+                <shape android:shape="rectangle">
+                    <solid android:color="#ff000000" />
+                    <corners android:radius="8dp" />
+                </shape>
+            </item>
+        </ripple>
+    </item>
+</layer-list>

+ 5 - 0
app/src/main/res/drawable/bg_login_agree_check_box.xml

@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:drawable="@drawable/icon_login_check_box_checked" android:state_checked="true" />
+    <item android:drawable="@drawable/icon_login_check_box_unchecked" />
+</selector>

+ 5 - 0
app/src/main/res/drawable/bg_login_print.xml

@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+    <corners android:radius="6dp" />
+    <solid android:color="#F8F8F8" />
+</shape>

+ 5 - 0
app/src/main/res/drawable/bg_login_send_code.xml

@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+    <corners android:radius="8dp" />
+    <solid android:color="#CEE1FF" />
+</shape>

+ 5 - 0
app/src/main/res/drawable/login_disable_bg.xml

@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+    <corners android:radius="8dp" />
+    <solid android:color="#CEE1FF" />
+</shape>

+ 276 - 0
app/src/main/res/layout/activity_login.xml

@@ -0,0 +1,276 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools">
+
+    <data>
+
+        <variable
+            name="loginViewModel"
+            type="com.datarecovery.master.module.login.LoginViewModel" />
+
+        <import type="com.atmob.common.ui.SizeUtil" />
+    </data>
+
+    <androidx.constraintlayout.widget.ConstraintLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent">
+
+        <ImageView
+            android:layout_width="match_parent"
+            android:layout_height="0dp"
+            android:src="@drawable/bg_mine_background"
+            app:layout_constraintDimensionRatio="1080:648"
+            app:layout_constraintTop_toTopOf="parent" />
+
+        <Space
+            android:id="@+id/space_status_bar"
+            android:layout_width="match_parent"
+            android:layout_height="@{SizeUtil.getStatusBarHeight(), default=@dimen/app_status_bar_height}"
+            app:layout_constraintTop_toTopOf="parent" />
+
+        <Space
+            android:id="@+id/space1"
+            android:layout_width="match_parent"
+            android:layout_height="0dp"
+            app:layout_constraintDimensionRatio="360:22"
+            app:layout_constraintTop_toBottomOf="@+id/space_status_bar" />
+
+        <ImageView
+            android:id="@+id/iv_back"
+            android:layout_width="0dp"
+            android:layout_height="0dp"
+            android:layout_marginStart="@dimen/app_common_page_horizontal_padding"
+            android:background="?android:attr/selectableItemBackgroundBorderless"
+            android:onClick="@{()->loginViewModel.onBackClick()}"
+            android:src="@drawable/icon_back"
+            app:layout_constraintDimensionRatio="1:1"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toBottomOf="@+id/space1"
+            app:layout_constraintWidth_percent="0.0666666666666667" />
+
+        <Space
+            android:id="@+id/space2"
+            android:layout_width="match_parent"
+            android:layout_height="0dp"
+            app:layout_constraintDimensionRatio="360:32.7"
+            app:layout_constraintTop_toBottomOf="@+id/iv_back" />
+
+        <TextView
+            android:id="@+id/tv_pleasure"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginStart="26dp"
+            android:text="@string/login_pleasure"
+            android:textColor="#202020"
+            android:textSize="30sp"
+            android:textStyle="bold"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toBottomOf="@+id/space2" />
+
+        <Space
+            android:id="@+id/space3"
+            android:layout_width="match_parent"
+            android:layout_height="0dp"
+            app:layout_constraintDimensionRatio="360:8.75"
+            app:layout_constraintTop_toBottomOf="@+id/tv_pleasure" />
+
+        <TextView
+            android:id="@+id/tv_desc"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginStart="26dp"
+            android:text="@string/login_pleasure_desc"
+            android:textColor="#404040"
+            android:textSize="16sp"
+            android:textStyle="bold"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toBottomOf="@+id/space3" />
+
+        <Space
+            android:id="@+id/space4"
+            android:layout_width="match_parent"
+            android:layout_height="0dp"
+            app:layout_constraintDimensionRatio="360:36.5"
+            app:layout_constraintTop_toBottomOf="@+id/tv_desc" />
+
+        <View
+            android:id="@+id/v_phone"
+            android:layout_width="match_parent"
+            android:layout_height="0dp"
+            android:layout_marginHorizontal="24dp"
+            android:background="@drawable/bg_login_print"
+            app:layout_constraintDimensionRatio="312:48"
+            app:layout_constraintTop_toBottomOf="@+id/space4" />
+
+        <TextView
+            android:id="@+id/tv_nation"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginStart="12dp"
+            android:text="@string/login_nation"
+            android:textColor="#404040"
+            android:textSize="16sp"
+            android:textStyle="bold"
+            app:layout_constraintBottom_toBottomOf="@+id/v_phone"
+            app:layout_constraintStart_toStartOf="@+id/v_phone"
+            app:layout_constraintTop_toTopOf="@+id/v_phone" />
+
+        <View
+            android:id="@+id/v_line"
+            android:layout_width="1dp"
+            android:layout_height="20dp"
+            android:layout_marginStart="9dp"
+            android:background="#E2E2E2"
+            app:layout_constraintBottom_toBottomOf="@+id/tv_nation"
+            app:layout_constraintStart_toEndOf="@+id/tv_nation"
+            app:layout_constraintTop_toTopOf="@+id/tv_nation" />
+
+        <EditText
+            android:id="@+id/et_phone"
+            isBold="@{loginViewModel.phoneNum.length() > 0}"
+            android:layout_width="0dp"
+            android:layout_height="0dp"
+            android:layout_marginHorizontal="25dp"
+            android:background="@color/transparent"
+            android:gravity="center_vertical"
+            android:hint="@string/login_phone_hint"
+            android:inputType="phone"
+            android:lines="1"
+            android:maxLength="11"
+            android:text="@={loginViewModel.phoneNum}"
+            android:textColor="@color/common_txt_color"
+            android:textColorHint="#A7A7A7"
+            android:textSize="16sp"
+            app:layout_constraintBottom_toBottomOf="@+id/v_phone"
+            app:layout_constraintEnd_toEndOf="@id/v_phone"
+            app:layout_constraintStart_toEndOf="@+id/v_line"
+            app:layout_constraintTop_toTopOf="@+id/v_phone" />
+
+        <Space
+            android:id="@+id/space5"
+            android:layout_width="match_parent"
+            android:layout_height="0dp"
+            app:layout_constraintDimensionRatio="360:11"
+            app:layout_constraintTop_toBottomOf="@+id/v_phone" />
+
+        <View
+            android:id="@+id/v_code"
+            android:layout_width="match_parent"
+            android:layout_height="0dp"
+            android:layout_marginHorizontal="24dp"
+            android:background="@drawable/bg_login_print"
+            app:layout_constraintDimensionRatio="312:48"
+            app:layout_constraintTop_toBottomOf="@+id/space5" />
+
+        <TextView
+            android:layout_width="0dp"
+            android:layout_height="0dp"
+            android:background="@drawable/bg_common_btn"
+            android:gravity="center"
+            android:onClick="@{()-> loginViewModel.onGetCodeClick()}"
+            android:text="@string/login_send_code"
+            android:textColor="@color/white"
+            android:textSize="14sp"
+            app:isGone="@{loginViewModel.isRequestCodeCountdown}"
+            app:layout_constraintBottom_toBottomOf="@+id/v_code"
+            app:layout_constraintDimensionRatio="90:32"
+            app:layout_constraintEnd_toEndOf="@+id/v_code"
+            app:layout_constraintHorizontal_bias="0.9459459459459459"
+            app:layout_constraintStart_toStartOf="@+id/v_code"
+            app:layout_constraintTop_toTopOf="@+id/v_code"
+            app:layout_constraintWidth_percent="0.25" />
+
+        <TextView
+            android:layout_width="0dp"
+            android:layout_height="0dp"
+            android:background="@drawable/bg_login_send_code"
+            android:gravity="center"
+            android:text="@{@string/login_request_code_countdown_format(loginViewModel.requestCodeCountdown)}"
+            android:textColor="@color/white"
+            android:textSize="14sp"
+            app:isGone="@{!loginViewModel.isRequestCodeCountdown}"
+            app:layout_constraintBottom_toBottomOf="@+id/v_code"
+            app:layout_constraintDimensionRatio="90:32"
+            app:layout_constraintEnd_toEndOf="@+id/v_code"
+            app:layout_constraintHorizontal_bias="0.9459459459459459"
+            app:layout_constraintStart_toStartOf="@+id/v_code"
+            app:layout_constraintTop_toTopOf="@+id/v_code"
+            app:layout_constraintWidth_percent="0.25"
+            tools:visibility="gone" />
+
+        <EditText
+            android:id="@+id/et_code"
+            isBold="@{loginViewModel.verificationCode.length() > 0}"
+            android:layout_width="0dp"
+            android:layout_height="0dp"
+            android:maxLength="6"
+            android:layout_marginHorizontal="12dp"
+            android:background="@color/transparent"
+            android:gravity="center_vertical"
+            android:hint="@string/login_code_hint"
+            android:inputType="number"
+            android:text="@={loginViewModel.verificationCode}"
+            android:textColor="@color/common_txt_color"
+            android:textColorHint="#A7A7A7"
+            android:textSize="16sp"
+            app:layout_constraintBottom_toBottomOf="@+id/v_code"
+            app:layout_constraintStart_toStartOf="@+id/v_code"
+            app:layout_constraintTop_toTopOf="@+id/v_code"
+            app:layout_constraintWidth_percent="0.4166666666666667" />
+
+        <Space
+            android:id="@+id/space6"
+            android:layout_width="match_parent"
+            android:layout_height="0dp"
+            app:layout_constraintDimensionRatio="360:12"
+            app:layout_constraintTop_toBottomOf="@+id/et_code" />
+
+        <CheckBox
+            android:id="@+id/login_agree_check_box"
+            android:layout_width="0dp"
+            android:layout_height="0dp"
+            android:background="@drawable/bg_login_agree_check_box"
+            android:button="@null"
+            android:checked="@={loginViewModel.isCheckedAgreement}"
+            app:expandTouchSize="@{10}"
+            app:layout_constraintDimensionRatio="1:1"
+            app:layout_constraintStart_toStartOf="@+id/et_code"
+            app:layout_constraintTop_toBottomOf="@+id/space6"
+            app:layout_constraintWidth_percent="0.0555555555555556"
+            tools:checked="true" />
+
+        <TextView
+            android:id="@+id/login_agree_text"
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:layout_marginStart="4dp"
+            android:textColor="#A7A7A7"
+            android:textSize="12sp"
+            app:layout_constraintBottom_toBottomOf="@+id/login_agree_check_box"
+            app:layout_constraintStart_toEndOf="@+id/login_agree_check_box"
+            app:layout_constraintTop_toTopOf="@+id/login_agree_check_box"
+            tools:text="@string/login_agree_text" />
+
+        <TextView
+            android:layout_width="0dp"
+            android:layout_height="0dp"
+            android:background="@{loginViewModel.isCanLogin(loginViewModel.phoneNum,loginViewModel.verificationCode) ? @drawable/bg_common_btn : @drawable/login_disable_bg}"
+            android:gravity="center"
+            android:onClick="@{()-> loginViewModel.onLoginClick()}"
+            android:text="@string/login_go"
+            android:textColor="@color/white"
+            android:textSize="16sp"
+            android:textStyle="bold"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintDimensionRatio="280:44"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toTopOf="@+id/space_status_bar"
+            app:layout_constraintVertical_bias="0.8947368421052632"
+            app:layout_constraintWidth_percent="0.7777777777777778"
+            tools:background="@drawable/bg_common_btn" />
+
+
+    </androidx.constraintlayout.widget.ConstraintLayout>
+</layout>

+ 1 - 1
app/src/main/res/values/colors.xml

@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 <resources>
     <color name="colorPrimary">#2B66FE</color>
-    <color name="colorClickPrimary">#2B66FE</color>
+    <color name="colorClickPrimary">#579AFF</color>
     <color name="colorPrimaryVariant">#E0EBFF</color>
 
 

+ 20 - 0
app/src/main/res/values/strings.xml

@@ -54,4 +54,24 @@
     <string name="feedback_hint">请详细描述您的意见</string>
     <string name="feedback_submit">提 交</string>
     <string name="feed_back_success">提交成功,感谢您的反馈!</string>
+    <string name="login_pleasure">您好!</string>
+    <string name="login_pleasure_desc">欢迎使用文件数据恢复大师</string>
+    <string name="login_nation">+86</string>
+    <string name="login_phone_hint">请输入11位手机号码</string>
+    <string name="login_code_hint">请输入验证码</string>
+    <string name="login_send_code">发送验证码</string>
+    <string name="login_agree_text">已阅读并同意《隐私权政策》和《服务条款》</string>
+    <string name="login_go">登 录</string>
+    <string name="login_success">登录成功</string>
+    <string name="login_too_often_toast">登录过于频繁,请稍后再试</string>
+    <string name="login_verification_code_error_toast">验证码错误</string>
+    <string name="login_failed_toast">登录失败</string>
+    <string name="login_please_agree">请同意用户协议</string>
+    <string name="login_request_code_countdown_format">%ss重新发送</string>
+    <string name="login_phone_num_11">请输入正确的11位手机号</string>
+    <string name="login_request_code_frequently_toast">请求过于频繁,请稍后再试</string>
+    <string name="login_verification_code_request_success_toast">验证码发送成功</string>
+    <string name="login_verification_code_request_failed_toast">验证码发送失败,请重试</string>
+    <string name="login_agree_user_terms_text">《隐私权政策》</string>
+    <string name="login_agree_privacy_policy_text">《服务条款》</string>
 </resources>