Browse Source

增加设置页面

zk 1 year ago
parent
commit
8828909935
24 changed files with 640 additions and 78 deletions
  1. 3 0
      app/src/main/AndroidManifest.xml
  2. 4 0
      app/src/main/java/com/atmob/voiceai/data/api/AtmobApi.java
  3. 26 0
      app/src/main/java/com/atmob/voiceai/data/api/bean/MemberInfoBean.java
  4. 29 0
      app/src/main/java/com/atmob/voiceai/data/api/response/UserInfoResponse.java
  5. 79 0
      app/src/main/java/com/atmob/voiceai/data/repositories/AccountRepository.java
  6. 0 26
      app/src/main/java/com/atmob/voiceai/data/repositories/MemberRepository.java
  7. 36 2
      app/src/main/java/com/atmob/voiceai/data/repositories/VoiceAIRepository.java
  8. 56 0
      app/src/main/java/com/atmob/voiceai/module/setting/SettingActivity.java
  9. 46 0
      app/src/main/java/com/atmob/voiceai/module/setting/SettingViewModel.java
  10. 39 50
      app/src/main/java/com/atmob/voiceai/module/voiceai/VoiceAIViewModel.java
  11. 106 0
      app/src/main/java/com/atmob/voiceai/utils/AppUtil.java
  12. BIN
      app/src/main/res/drawable-xxhdpi/icon_setting_back.webp
  13. BIN
      app/src/main/res/drawable-xxhdpi/icon_setting_email.webp
  14. BIN
      app/src/main/res/drawable-xxhdpi/icon_setting_logo.webp
  15. BIN
      app/src/main/res/drawable-xxhdpi/icon_setting_privacy_policy.webp
  16. BIN
      app/src/main/res/drawable-xxhdpi/icon_setting_pro_arrow.webp
  17. BIN
      app/src/main/res/drawable-xxhdpi/icon_setting_rate.webp
  18. BIN
      app/src/main/res/drawable-xxhdpi/icon_setting_terms_of_service.webp
  19. 9 0
      app/src/main/res/drawable/bg_ripple_common_mask.xml
  20. 11 0
      app/src/main/res/drawable/bg_setting_pro.xml
  21. 1 0
      app/src/main/res/layout/fragment_voice_ai.xml
  22. 49 0
      app/src/main/res/layout/layout_item_settings.xml
  23. 139 0
      app/src/main/res/layout/setting_activity.xml
  24. 7 0
      app/src/main/res/values/strings.xml

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

@@ -48,6 +48,9 @@
         <activity
             android:name=".module.result.VoiceResultActivity"
             android:screenOrientation="portrait" />
+        <activity
+            android:name=".module.setting.SettingActivity"
+            android:screenOrientation="portrait" />
 
         <provider
             android:name="androidx.core.content.FileProvider"

+ 4 - 0
app/src/main/java/com/atmob/voiceai/data/api/AtmobApi.java

@@ -9,6 +9,7 @@ import com.atmob.voiceai.data.api.request.VoiceCloneRequest;
 import com.atmob.voiceai.data.api.request.VoiceHistoryRequest;
 import com.atmob.voiceai.data.api.request.VoiceListRequest;
 import com.atmob.voiceai.data.api.response.TextToSpeechResponse;
+import com.atmob.voiceai.data.api.response.UserInfoResponse;
 import com.atmob.voiceai.data.api.response.VoiceCloneListResponse;
 import com.atmob.voiceai.data.api.response.VoiceCloneResponse;
 import com.atmob.voiceai.data.api.response.VoiceHistoryResponse;
@@ -44,6 +45,9 @@ public interface AtmobApi {
     @POST("/project/voice/v1/voice/history")
     Single<BaseResponse<VoiceHistoryResponse>> voiceHistory(@Body VoiceHistoryRequest request);
 
+    @POST("/project/voice/v1/user/info")
+    Single<BaseResponse<UserInfoResponse>> userInfo(@Body BaseRequest request);
+
 
 
 

+ 26 - 0
app/src/main/java/com/atmob/voiceai/data/api/bean/MemberInfoBean.java

@@ -0,0 +1,26 @@
+package com.atmob.voiceai.data.api.bean;
+
+import com.google.gson.annotations.SerializedName;
+
+public class MemberInfoBean {
+    @SerializedName("isMember")
+    private boolean isMember;
+    @SerializedName("expireTime")
+    private long expireTime;
+
+    public boolean isIsMember() {
+        return isMember;
+    }
+
+    public void setIsMember(boolean isMember) {
+        this.isMember = isMember;
+    }
+
+    public long getExpireTime() {
+        return expireTime;
+    }
+
+    public void setExpireTime(long expireTime) {
+        this.expireTime = expireTime;
+    }
+}

+ 29 - 0
app/src/main/java/com/atmob/voiceai/data/api/response/UserInfoResponse.java

@@ -0,0 +1,29 @@
+package com.atmob.voiceai.data.api.response;
+
+import com.atmob.voiceai.data.api.bean.MemberInfoBean;
+import com.google.gson.annotations.SerializedName;
+
+public class UserInfoResponse {
+
+
+    @SerializedName("ssid")
+    private String ssid;
+    @SerializedName("deviceId")
+    private String deviceId;
+    @SerializedName("memberInfo")
+    private MemberInfoBean memberInfo;
+
+    public String getSsid() {
+        return ssid;
+    }
+
+    public String getDeviceId() {
+        return deviceId;
+    }
+
+    public MemberInfoBean getMemberInfo() {
+        return memberInfo;
+    }
+
+
+}

+ 79 - 0
app/src/main/java/com/atmob/voiceai/data/repositories/AccountRepository.java

@@ -0,0 +1,79 @@
+package com.atmob.voiceai.data.repositories;
+
+
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
+
+import com.atmob.app.lib.handler.RxHttpHandler;
+import com.atmob.voiceai.data.api.AtmobApi;
+import com.atmob.voiceai.data.api.bean.MemberInfoBean;
+import com.atmob.voiceai.data.api.request.BaseRequest;
+import com.atmob.voiceai.data.api.response.UserInfoResponse;
+
+import java.util.concurrent.TimeUnit;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
+import atmob.reactivex.rxjava3.annotations.NonNull;
+import atmob.reactivex.rxjava3.core.Single;
+import atmob.reactivex.rxjava3.core.SingleObserver;
+import atmob.reactivex.rxjava3.disposables.Disposable;
+import atmob.rxjava.utils.RxJavaUtil;
+
+@Singleton
+public class AccountRepository {
+
+    private final AtmobApi atmobApi;
+    private boolean requestUserInfoDisabled;
+
+    private final MutableLiveData<Boolean> isMember = new MutableLiveData<>();
+
+
+    @Inject
+    public AccountRepository(AtmobApi atmobApi) {
+        this.atmobApi = atmobApi;
+        refreshUserData();
+    }
+
+    public LiveData<Boolean> getIsMember() {
+        return isMember;
+    }
+
+
+    private void refreshUserData() {
+        if (requestUserInfoDisabled) {
+            return;
+        }
+        userInfo().subscribe(new SingleObserver<UserInfoResponse>() {
+
+
+            @Override
+            public void onSubscribe(@NonNull Disposable d) {
+                requestUserInfoDisabled = true;
+            }
+
+            @Override
+            public void onSuccess(@NonNull UserInfoResponse userInfoResponse) {
+                requestUserInfoDisabled = false;
+            }
+
+            @Override
+            public void onError(@NonNull Throwable e) {
+                requestUserInfoDisabled = false;
+            }
+        });
+    }
+
+
+    private Single<UserInfoResponse> userInfo() {
+        return atmobApi.userInfo(new BaseRequest())
+                .compose(RxHttpHandler.handle(true))
+                .retryWhen(RxJavaUtil.exponentialBackOff(null, 3, TimeUnit.SECONDS))
+                .compose(RxJavaUtil.SingleSchedule.io2Main())
+                .doOnSuccess(userInfoResponse -> {
+                    MemberInfoBean memberInfo = userInfoResponse.getMemberInfo();
+                    isMember.setValue(memberInfo != null && memberInfo.isIsMember());
+                });
+    }
+}

+ 0 - 26
app/src/main/java/com/atmob/voiceai/data/repositories/MemberRepository.java

@@ -1,26 +0,0 @@
-package com.atmob.voiceai.data.repositories;
-
-
-import androidx.lifecycle.LiveData;
-import androidx.lifecycle.MutableLiveData;
-
-import javax.inject.Inject;
-import javax.inject.Singleton;
-
-@Singleton
-public class MemberRepository {
-
-
-    private final MutableLiveData<Boolean> isMember = new MutableLiveData<>(true);
-
-
-    @Inject
-    public MemberRepository() {
-
-    }
-
-    public LiveData<Boolean> getIsMember() {
-        return isMember;
-    }
-
-}

+ 36 - 2
app/src/main/java/com/atmob/voiceai/data/repositories/VoiceAIRepository.java

@@ -43,6 +43,7 @@ public class VoiceAIRepository {
     private final GenerateApi generateApi;
 
     private boolean textToSpeechRequest;
+    private final MutableLiveData<Integer> adFreeGenerateNumber = new MutableLiveData<>();
 
     private VoiceListBean requestCloneBean;
     private String textToSpeechTxt;
@@ -53,6 +54,8 @@ public class VoiceAIRepository {
 
     private final MutableLiveData<VoiceListBean> recommendClickBean = new MutableLiveData<>();
 
+    private boolean isRequestFreeGenerate;
+
 
     @IntDef({TextToSpeechState.GENERATING, TextToSpeechState.GENERATED, TextToSpeechState.ERROR})
     public @interface TextToSpeechState {
@@ -65,6 +68,11 @@ public class VoiceAIRepository {
     public VoiceAIRepository(AtmobApi atmobApi, GenerateApi generateApi) {
         this.atmobApi = atmobApi;
         this.generateApi = generateApi;
+        refreshVoiceInfo();
+    }
+
+    public LiveData<Integer> getAdFreeGenerateNumber() {
+        return adFreeGenerateNumber;
     }
 
     public void setRecommendClickBean(VoiceListBean bean) {
@@ -91,7 +99,32 @@ public class VoiceAIRepository {
         return textToSpeechState;
     }
 
-    public Single<VoiceInfoResponse> requestVoiceInfo() {
+
+    private void refreshVoiceInfo() {
+        if (isRequestFreeGenerate) {
+            return;
+        }
+        requestVoiceInfo().subscribe(new SingleObserver<VoiceInfoResponse>() {
+            @Override
+            public void onSubscribe(@NonNull Disposable d) {
+                isRequestFreeGenerate = true;
+            }
+
+            @Override
+            public void onSuccess(@NonNull VoiceInfoResponse voiceInfoResponse) {
+                isRequestFreeGenerate = false;
+                adFreeGenerateNumber.setValue(voiceInfoResponse.getVoiceTimes());
+            }
+
+            @Override
+            public void onError(@NonNull Throwable e) {
+                e.printStackTrace();
+                isRequestFreeGenerate = false;
+            }
+        });
+    }
+
+    private Single<VoiceInfoResponse> requestVoiceInfo() {
         return atmobApi.getVoiceInfo(new BaseRequest())
                 .compose(RxHttpHandler.handle(true))
                 .compose(RxJavaUtil.SingleSchedule.io2Main());
@@ -155,7 +188,8 @@ public class VoiceAIRepository {
     private Single<TextToSpeechResponse> textToSpeech(int id, int voiceType, String content) {
         return generateApi.textToSpeech(new TextTosSpeechRequest(id, voiceType, content))
                 .compose(RxHttpHandler.handle(true))
-                .compose(RxJavaUtil.SingleSchedule.io2Main());
+                .compose(RxJavaUtil.SingleSchedule.io2Main())
+                .doOnSuccess(data -> refreshVoiceInfo());
     }
 
 

+ 56 - 0
app/src/main/java/com/atmob/voiceai/module/setting/SettingActivity.java

@@ -0,0 +1,56 @@
+package com.atmob.voiceai.module.setting;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+
+import androidx.annotation.Nullable;
+
+import com.atmob.app.lib.base.BaseActivity;
+import com.atmob.voiceai.databinding.SettingActivityBinding;
+
+import dagger.hilt.android.AndroidEntryPoint;
+
+@AndroidEntryPoint
+public class SettingActivity extends BaseActivity<SettingActivityBinding> {
+
+    private SettingViewModel settingViewModel;
+
+
+    public static void start(Context context) {
+        Intent intent = new Intent(context, SettingActivity.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 initObserver() {
+
+    }
+
+    private void initView() {
+        addTopStatusBarHeight(binding.toolbar);
+    }
+
+    @Override
+    protected boolean shouldImmersion() {
+        return true;
+    }
+
+    @Override
+    protected void initViewModel() {
+        super.initViewModel();
+        settingViewModel = getViewModelProvider().get(SettingViewModel.class);
+        binding.setSettingViewModel(settingViewModel);
+    }
+}

+ 46 - 0
app/src/main/java/com/atmob/voiceai/module/setting/SettingViewModel.java

@@ -0,0 +1,46 @@
+package com.atmob.voiceai.module.setting;
+
+import com.atmob.app.lib.base.BaseViewModel;
+import com.atmob.common.runtime.ActivityUtil;
+import com.atmob.common.runtime.ContextUtil;
+import com.atmob.voiceai.R;
+import com.atmob.voiceai.module.subscription.SubscriptionPageActivity;
+import com.atmob.voiceai.utils.AppUtil;
+import com.atmob.voiceai.utils.ToastUtil;
+
+import javax.inject.Inject;
+
+import dagger.hilt.android.lifecycle.HiltViewModel;
+
+@HiltViewModel
+public class SettingViewModel extends BaseViewModel {
+
+
+    @Inject
+    public SettingViewModel() {
+    }
+
+    public void onSettingProClick() {
+        SubscriptionPageActivity.start(ActivityUtil.getTopActivity());
+    }
+
+    public void onRateClick() {
+        if (AppUtil.isAppExist(AppUtil.GooglePlay)) {
+            AppUtil.launchGooglePlayDetail(ContextUtil.getContext().getPackageName());
+        } else {
+            ToastUtil.show(R.string.google_play_not_found, ToastUtil.LENGTH_SHORT);
+        }
+    }
+
+    public void onSendEmailClick() {
+
+    }
+
+    public void onPrivacyPolicyClick() {
+
+    }
+
+    public void onTermsOfServiceClick() {
+
+    }
+}

+ 39 - 50
app/src/main/java/com/atmob/voiceai/module/voiceai/VoiceAIViewModel.java

@@ -7,6 +7,7 @@ import androidx.annotation.ColorInt;
 import androidx.lifecycle.LiveData;
 import androidx.lifecycle.MediatorLiveData;
 import androidx.lifecycle.MutableLiveData;
+import androidx.lifecycle.Observer;
 import androidx.lifecycle.Transformations;
 
 import com.atmob.app.lib.base.BaseViewModel;
@@ -20,10 +21,11 @@ import com.atmob.voiceai.data.api.bean.VoiceListBean;
 import com.atmob.voiceai.data.api.response.VoiceInfoResponse;
 import com.atmob.voiceai.data.api.response.VoiceListResponse;
 import com.atmob.voiceai.data.api.response.VoiceTypeResponse;
-import com.atmob.voiceai.data.repositories.MemberRepository;
+import com.atmob.voiceai.data.repositories.AccountRepository;
 import com.atmob.voiceai.data.repositories.VoiceAIRepository;
 import com.atmob.voiceai.helper.ErrorHelper;
 import com.atmob.voiceai.module.generating.VoiceGeneratingActivity;
+import com.atmob.voiceai.module.setting.SettingActivity;
 import com.atmob.voiceai.module.subscription.SubscriptionPageActivity;
 import com.atmob.voiceai.utils.BoxingUtil;
 import com.atmob.voiceai.utils.SpannableUtil;
@@ -45,11 +47,11 @@ public class VoiceAIViewModel extends BaseViewModel {
 
     private final String LAST_GENERATE_TEXT = "last_generate_text";
 
-    private final MediatorLiveData<CharSequence> voiceProbationTxt = new MediatorLiveData<>();
+    private final LiveData<CharSequence> voiceProbationTxt;
     private final MutableLiveData<String> voicePrintTxt = new MutableLiveData<>();
     private final LiveData<String> voicePrintLimitTxt;
-    private final MemberRepository memberRepository;
-    private final MutableLiveData<Integer> adFreeGenerateNumber = new MutableLiveData<>();
+    private final AccountRepository accountRepository;
+
     @ColorInt
     private final int freeTxtColor;
     private final VoiceAIRepository voiceAIRepository;
@@ -70,9 +72,11 @@ public class VoiceAIViewModel extends BaseViewModel {
     private String currentTypeId;
     private Disposable voiceListDisposable;
 
+    Observer<VoiceListBean> recommendObserver;
+
     @Inject
-    public VoiceAIViewModel(MemberRepository memberRepository, VoiceAIRepository voiceAIRepository) {
-        this.memberRepository = memberRepository;
+    public VoiceAIViewModel(AccountRepository accountRepository, VoiceAIRepository voiceAIRepository) {
+        this.accountRepository = accountRepository;
         this.voiceAIRepository = voiceAIRepository;
         voicePrintLimitTxt = Transformations.map(voicePrintTxt, txt -> {
             int length = 0;
@@ -82,25 +86,17 @@ public class VoiceAIViewModel extends BaseViewModel {
             return length + "/200";
         });
         freeTxtColor = ContextUtil.getApplication().getResources().getColor(R.color.voice_free_crux_txt_color);
-
-        voiceProbationTxt.addSource(adFreeGenerateNumber, number -> {
-            if (BoxingUtil.boxing(memberRepository.getIsMember().getValue())) {
-                voiceProbationTxt.setValue("");
-                return;
-            }
-            setFreeGenerateText(BoxingUtil.boxing(number));
-        });
         voicePrintTxt.setValue(KVUtils.getDefault().getString(LAST_GENERATE_TEXT, ""));
-        voiceProbationTxt.addSource(memberRepository.getIsMember(), isMember -> {
-            if (BoxingUtil.boxing(isMember)) {
-                voiceProbationTxt.setValue("");
-                return;
+        voiceProbationTxt = Transformations.map(voiceAIRepository.getAdFreeGenerateNumber(), number -> {
+            if (number == null || BoxingUtil.boxing(accountRepository.getIsMember().getValue())) {
+                return "";
             }
-            setFreeGenerateText(BoxingUtil.boxing(adFreeGenerateNumber.getValue()));
+            return SpannableUtil.getSpannableStringBuilder(ContextUtil.getContext().getString(R.string.voice_ai_get_free_number, number), String.valueOf(number), freeTxtColor, false);
         });
         iniVoiceAI();
     }
 
+
     public LiveData<Integer> getScrollToTabPosition() {
         return scrollToTabPosition;
     }
@@ -130,11 +126,11 @@ public class VoiceAIViewModel extends BaseViewModel {
     }
 
     public LiveData<Integer> getAdFreeGenerateNumber() {
-        return adFreeGenerateNumber;
+        return voiceAIRepository.getAdFreeGenerateNumber();
     }
 
     public LiveData<Boolean> isMember() {
-        return memberRepository.getIsMember();
+        return accountRepository.getIsMember();
     }
 
     public LiveData<CharSequence> getVoiceProbationTxt() {
@@ -153,11 +149,6 @@ public class VoiceAIViewModel extends BaseViewModel {
         voicePrintTxt.setValue("");
     }
 
-    private void setFreeGenerateText(int number) {
-        voiceProbationTxt.setValue(SpannableUtil.getSpannableStringBuilder(
-                ContextUtil.getContext().getString(R.string.voice_ai_get_free_number, number),
-                String.valueOf(number), freeTxtColor, false));
-    }
 
     public void onGenerateClick() {
         String content = voicePrintTxt.getValue();
@@ -177,7 +168,7 @@ public class VoiceAIViewModel extends BaseViewModel {
             SubscriptionPageActivity.start(ActivityUtil.getTopActivity());
             return;
         }
-        if (BoxingUtil.boxing(adFreeGenerateNumber.getValue()) > 0) {
+        if (BoxingUtil.boxing(voiceAIRepository.getAdFreeGenerateNumber().getValue()) > 0) {
             showGenerateAd.call();
             return;
         }
@@ -191,13 +182,15 @@ public class VoiceAIViewModel extends BaseViewModel {
 
 
     private void iniVoiceAI() {
-        refreshVoiceInfo();
         requestVoiceTypeList();
         fromOtherRecommendSelectVoice();
     }
 
     private void fromOtherRecommendSelectVoice() {
-        voiceAIRepository.getRecommendClickBean().observeForever(bean -> {
+        if (recommendObserver != null) {
+            voiceAIRepository.getRecommendClickBean().removeObserver(recommendObserver);
+        }
+        recommendObserver = bean -> {
             if (bean == null) {
                 return;
             }
@@ -215,7 +208,8 @@ public class VoiceAIViewModel extends BaseViewModel {
                 scrollToTabPosition.setValue(0);
                 refreshVoiceList(String.valueOf(typeList.get(0).getId()), bean);
             }
-        });
+        };
+        voiceAIRepository.getRecommendClickBean().observeForever(recommendObserver);
     }
 
     public void scrollToListPosition(@NonNull VoiceListBean bean) {
@@ -237,26 +231,6 @@ public class VoiceAIViewModel extends BaseViewModel {
     }
 
 
-    private void refreshVoiceInfo() {
-        voiceAIRepository.requestVoiceInfo().subscribe(new SingleObserver<VoiceInfoResponse>() {
-            @Override
-            public void onSubscribe(@NonNull Disposable d) {
-                addDisposable(d);
-            }
-
-            @Override
-            public void onSuccess(@NonNull VoiceInfoResponse voiceInfoResponse) {
-                adFreeGenerateNumber.setValue(voiceInfoResponse.getVoiceTimes());
-            }
-
-            @Override
-            public void onError(@NonNull Throwable e) {
-                e.printStackTrace();
-            }
-        });
-    }
-
-
     private void requestVoiceTypeList() {
         voiceAIRepository.requestVoiceType().subscribe(new SingleObserver<VoiceTypeResponse>() {
             @Override
@@ -366,6 +340,21 @@ public class VoiceAIViewModel extends BaseViewModel {
     }
 
     public void onSettingClick() {
+        SettingActivity.start(ActivityUtil.getTopActivity());
+    }
+
+
+    public void onGoSubscriptionClick() {
+        SubscriptionPageActivity.start(ActivityUtil.getTopActivity());
+    }
 
+    @Override
+    protected void onCleared() {
+        super.onCleared();
+        if (recommendObserver != null) {
+            voiceAIRepository.getRecommendClickBean().removeObserver(recommendObserver);
+        }
     }
+
+
 }

+ 106 - 0
app/src/main/java/com/atmob/voiceai/utils/AppUtil.java

@@ -0,0 +1,106 @@
+package com.atmob.voiceai.utils;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.net.Uri;
+import android.text.TextUtils;
+
+import com.atmob.common.runtime.ContextUtil;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Objects;
+
+public class AppUtil {
+
+    public static final String GooglePlay = "com.android.vending";
+
+    private AppUtil() {
+
+    }
+
+    public static void launchAppMarketDetail(String appPackageName) {
+        if (TextUtils.isEmpty(appPackageName)) {
+            return;
+        }
+        Uri uri = Uri.parse("market://details?id=" + appPackageName);
+        Intent intent = new Intent(Intent.ACTION_VIEW, uri);
+        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        String existMarketPackage = getExistMarketPackage();
+        if (!TextUtils.isEmpty(existMarketPackage)) {
+            intent.setPackage(existMarketPackage);
+        }
+        try {
+            ContextUtil.getContext().startActivity(intent);
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+
+    public static void launchGooglePlayDetail(String appPackageName) {
+        Intent intent = new Intent(Intent.ACTION_VIEW);
+        intent.setData(Uri.parse("https://play.google.com/store/apps/details?id=" + appPackageName));
+        intent.setPackage(GooglePlay);
+        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        try {
+            ContextUtil.getContext().startActivity(intent);
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+
+    private static String getExistMarketPackage() {
+        List<String> marketList = Arrays.asList(
+                "com.tencent.android.qqdownloader", //应用宝
+                "com.huawei.appmarket", //华为
+                "com.xiaomi.market", //小米
+                "com.oppo.market", //OPPO
+                "com.bbk.appstore", //VIVO
+                "com.oneplus.market", //一加
+                "com.qihoo.appstore", //360
+                "com.baidu.appsearch", //百度
+                GooglePlay
+        );
+        for (String marketPackageName : marketList) {
+            if (isAppExist(marketPackageName)) {
+                return marketPackageName;
+            }
+        }
+        return null;
+    }
+
+    public static boolean isAppExist(String appPackageName) {
+        Context context = ContextUtil.getContext();
+        final PackageManager packageManager = context.getPackageManager();
+        List<PackageInfo> packageInfoList = packageManager.getInstalledPackages(0);
+        List<String> pName = new ArrayList<>();
+        if (packageInfoList != null) {
+            for (int i = 0; i < packageInfoList.size(); i++) {
+                String pf = packageInfoList.get(i).packageName;
+                pName.add(pf);
+            }
+        }
+        return pName.contains(appPackageName);
+    }
+
+    public static List<ApplicationInfo> get3rdPartyApp() {
+        Context context = ContextUtil.getContext();
+        PackageManager packageManager = context.getPackageManager();
+        List<ApplicationInfo> installedApplications = packageManager.getInstalledApplications(PackageManager.GET_META_DATA);
+        Iterator<ApplicationInfo> iterator = installedApplications.iterator();
+        while (iterator.hasNext()) {
+            ApplicationInfo applicationInfo = iterator.next();
+            if ((applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0) {
+                iterator.remove();
+            } else if (Objects.equals(applicationInfo.packageName, context.getPackageName())) {
+                iterator.remove();
+            }
+        }
+        return installedApplications;
+    }
+}

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


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


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


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


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


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


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


+ 9 - 0
app/src/main/res/drawable/bg_ripple_common_mask.xml

@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+    android:color="#94000000">
+    <item android:id="@android:id/mask">
+        <shape android:shape="rectangle">
+            <solid android:color="#ff000000" />
+        </shape>
+    </item>
+</ripple>

+ 11 - 0
app/src/main/res/drawable/bg_setting_pro.xml

@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+    <stroke
+        android:width="3dp"
+        android:color="@color/white10" />
+    <gradient
+        android:endColor="#16C4DC"
+        android:startColor="#03E7AC" />
+
+    <corners android:radius="12dp" />
+</shape>

+ 1 - 0
app/src/main/res/layout/fragment_voice_ai.xml

@@ -47,6 +47,7 @@
                 app:layout_constraintTop_toBottomOf="@+id/space_status_bar" />
 
             <View
+                android:onClick="@{()-> voiceAIViewModel.onGoSubscriptionClick()}"
                 android:id="@+id/v_vip_bg"
                 android:layout_width="0dp"
                 android:layout_height="0dp"

+ 49 - 0
app/src/main/res/layout/layout_item_settings.xml

@@ -0,0 +1,49 @@
+<?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="settingsName"
+            type="String" />
+
+        <variable
+            name="settingsIcon"
+            type="android.graphics.drawable.Drawable" />
+    </data>
+
+    <androidx.constraintlayout.widget.ConstraintLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:background="@drawable/bg_ripple_common_mask"
+        android:paddingVertical="10dp">
+
+        <ImageView
+            android:id="@+id/item_setting_icon"
+            android:layout_width="0dp"
+            android:layout_height="0dp"
+            android:layout_marginStart="24dp"
+            android:src="@{settingsIcon}"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintDimensionRatio="1:1"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toTopOf="parent"
+            app:layout_constraintWidth_percent="0.1111111111111111"
+            tools:src="@drawable/icon_setting_rate" />
+
+        <TextView
+            android:text="@{settingsName}"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginStart="16dp"
+            android:textColor="@color/white"
+            android:textSize="17sp"
+            app:layout_constraintBottom_toBottomOf="@+id/item_setting_icon"
+            app:layout_constraintStart_toEndOf="@+id/item_setting_icon"
+            app:layout_constraintTop_toTopOf="@+id/item_setting_icon"
+            tools:text="Rate App" />
+
+    </androidx.constraintlayout.widget.ConstraintLayout>
+</layout>

+ 139 - 0
app/src/main/res/layout/setting_activity.xml

@@ -0,0 +1,139 @@
+<?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">
+
+    <data>
+
+        <variable
+            name="settingViewModel"
+            type="com.atmob.voiceai.module.setting.SettingViewModel" />
+    </data>
+
+    <androidx.constraintlayout.widget.ConstraintLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent">
+
+        <androidx.appcompat.widget.Toolbar
+            android:id="@+id/toolbar"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            app:layout_constraintTop_toTopOf="parent"
+            app:navigationIcon="@drawable/icon_setting_back">
+
+            <TextView
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_gravity="center"
+                android:text="@string/voice_setting"
+                android:textColor="@color/white"
+                android:textSize="17sp"
+                android:textStyle="bold" />
+        </androidx.appcompat.widget.Toolbar>
+
+        <Space
+            android:id="@+id/space1"
+            android:layout_width="match_parent"
+            android:layout_height="0dp"
+            app:layout_constraintDimensionRatio="360:39"
+            app:layout_constraintTop_toBottomOf="@id/toolbar" />
+
+        <ImageView
+            android:id="@+id/iv_setting_logo"
+            android:layout_width="0dp"
+            android:layout_height="0dp"
+            android:src="@drawable/icon_setting_logo"
+            app:layout_constraintDimensionRatio="148:88"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toBottomOf="@+id/space1"
+            app:layout_constraintWidth_percent="0.4111111111111111" />
+
+        <Space
+            android:id="@+id/space2"
+            android:layout_width="match_parent"
+            android:layout_height="0dp"
+            app:layout_constraintDimensionRatio="360:35"
+            app:layout_constraintTop_toBottomOf="@id/iv_setting_logo" />
+
+        <View
+            android:id="@+id/view_setting_pro"
+            android:layout_width="match_parent"
+            android:layout_height="0dp"
+            android:layout_marginHorizontal="24dp"
+            android:background="@drawable/bg_setting_pro"
+            android:onClick="@{()-> settingViewModel.onSettingProClick()}"
+            app:layout_constraintDimensionRatio="312:60"
+            app:layout_constraintTop_toBottomOf="@id/space2" />
+
+        <TextView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginStart="20dp"
+            android:text="@string/setting_app_name_pro"
+            android:textColor="@color/colorPrimary"
+            android:textSize="17sp"
+            android:textStyle="bold"
+            app:layout_constraintBottom_toBottomOf="@+id/view_setting_pro"
+            app:layout_constraintStart_toStartOf="@+id/view_setting_pro"
+            app:layout_constraintTop_toTopOf="@+id/view_setting_pro" />
+
+        <ImageView
+            android:layout_width="0dp"
+            android:layout_height="0dp"
+            android:layout_marginEnd="20dp"
+            android:src="@drawable/icon_setting_pro_arrow"
+            app:layout_constraintBottom_toBottomOf="@+id/view_setting_pro"
+            app:layout_constraintDimensionRatio="1:1"
+            app:layout_constraintEnd_toEndOf="@+id/view_setting_pro"
+            app:layout_constraintTop_toTopOf="@+id/view_setting_pro"
+            app:layout_constraintWidth_percent="0.1" />
+
+        <Space
+            android:id="@+id/space3"
+            android:layout_width="match_parent"
+            android:layout_height="0dp"
+            app:layout_constraintDimensionRatio="360:22"
+            app:layout_constraintTop_toBottomOf="@+id/view_setting_pro" />
+
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:orientation="vertical"
+            app:layout_constraintTop_toBottomOf="@+id/space3">
+
+            <include
+                layout="@layout/layout_item_settings"
+                settingsIcon="@{@drawable/icon_setting_rate}"
+                settingsName="@{@string/setting_rate_app}"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:onClick="@{()->settingViewModel.onRateClick()}" />
+
+            <include
+                layout="@layout/layout_item_settings"
+                settingsIcon="@{@drawable/icon_setting_email}"
+                settingsName="@{@string/setting_contact}"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:onClick="@{()->settingViewModel.onSendEmailClick()}" />
+
+            <include
+                layout="@layout/layout_item_settings"
+                settingsIcon="@{@drawable/icon_setting_privacy_policy}"
+                settingsName="@{@string/setting_privacy_policy}"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:onClick="@{()->settingViewModel.onPrivacyPolicyClick()}" />
+
+            <include
+                layout="@layout/layout_item_settings"
+                settingsIcon="@{@drawable/icon_setting_terms_of_service}"
+                settingsName="@{@string/setting_terms_of_service}"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:onClick="@{()->settingViewModel.onTermsOfServiceClick()}" />
+        </LinearLayout>
+
+
+    </androidx.constraintlayout.widget.ConstraintLayout>
+</layout>

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

@@ -28,4 +28,11 @@
     <string name="voice_save_error">Save failed.</string>
     <string name="voice_save_success">Save successful.</string>
     <string name="voice_file_get_error">File retrieval exception</string>
+    <string name="voice_setting">Setting</string>
+    <string name="setting_app_name_pro">APP name pro</string>
+    <string name="setting_rate_app">Rate App</string>
+    <string name="setting_contact">Contact</string>
+    <string name="setting_privacy_policy">Privacy Policy</string>
+    <string name="setting_terms_of_service">Terms Of Service</string>
+    <string name="google_play_not_found">Google Play not found</string>
 </resources>