Browse Source

增加本地文件选择、语音预览以及上传功能

zk 2 years ago
parent
commit
5989982b43
42 changed files with 2006 additions and 63 deletions
  1. 4 0
      app/src/main/AndroidManifest.xml
  2. 6 1
      app/src/main/java/com/atmob/voiceai/data/api/GenerateApi.java
  3. 1 8
      app/src/main/java/com/atmob/voiceai/data/api/request/VoiceCloneRequest.java
  4. 132 6
      app/src/main/java/com/atmob/voiceai/data/repositories/CloneRepository.java
  5. 8 0
      app/src/main/java/com/atmob/voiceai/data/repositories/VoiceAIRepository.java
  6. 38 0
      app/src/main/java/com/atmob/voiceai/dialog/CloneDeleteSureDialog.java
  7. 51 0
      app/src/main/java/com/atmob/voiceai/dialog/RecordingInstructionsDialog.java
  8. 1 1
      app/src/main/java/com/atmob/voiceai/helper/ErrorHelper.java
  9. 13 0
      app/src/main/java/com/atmob/voiceai/module/clonevoice/CloneState.java
  10. 211 1
      app/src/main/java/com/atmob/voiceai/module/clonevoice/CloneVoiceFragment.java
  11. 260 42
      app/src/main/java/com/atmob/voiceai/module/clonevoice/CloneVoiceViewModel.java
  12. 75 0
      app/src/main/java/com/atmob/voiceai/module/cloning/VoiceCloningActivity.java
  13. 30 0
      app/src/main/java/com/atmob/voiceai/module/cloning/VoiceCloningViewModel.java
  14. 2 2
      app/src/main/java/com/atmob/voiceai/module/generating/VoiceGeneratingActivity.java
  15. 10 0
      app/src/main/java/com/atmob/voiceai/module/result/VoiceResultViewModel.java
  16. 11 0
      app/src/main/java/com/atmob/voiceai/module/voiceai/VoiceAIViewModel.java
  17. 40 0
      app/src/main/java/com/atmob/voiceai/utils/AudioRecorder.java
  18. 2 2
      app/src/main/java/com/atmob/voiceai/utils/DownloadUtils.java
  19. 99 0
      app/src/main/java/com/atmob/voiceai/utils/FileUtils.java
  20. 47 0
      app/src/main/java/com/atmob/voiceai/utils/PermissionUtil.java
  21. 97 0
      app/src/main/java/com/atmob/voiceai/utils/ReflectionUtil.java
  22. BIN
      app/src/main/res/drawable-xxhdpi/icon_checkbox_no_select.webp
  23. BIN
      app/src/main/res/drawable-xxhdpi/icon_checkbox_selected.webp
  24. BIN
      app/src/main/res/drawable-xxhdpi/icon_clone_record.webp
  25. BIN
      app/src/main/res/drawable-xxhdpi/icon_clone_right_arrow.webp
  26. BIN
      app/src/main/res/drawable-xxhdpi/icon_clone_success.webp
  27. BIN
      app/src/main/res/drawable-xxhdpi/icon_clone_upload.webp
  28. BIN
      app/src/main/res/drawable-xxhdpi/icon_default_avatar.png
  29. BIN
      app/src/main/res/drawable-xxhdpi/icon_recording_close.webp
  30. BIN
      app/src/main/res/drawable-xxhdpi/iv_recording_header.webp
  31. 5 0
      app/src/main/res/drawable/bg_clone_card.xml
  32. 5 0
      app/src/main/res/drawable/bg_clone_voice_delete.xml
  33. 5 0
      app/src/main/res/drawable/bg_delete_clone_voice_dialog.xml
  34. 5 0
      app/src/main/res/drawable/bg_recording_card.xml
  35. 7 0
      app/src/main/res/drawable/bg_use_clone_voice.xml
  36. 20 0
      app/src/main/res/drawable/shape_preview_clone_audio_seekbar.xml
  37. 123 0
      app/src/main/res/layout/activity_voice_clone.xml
  38. 113 0
      app/src/main/res/layout/dialog_clone_delete_sure.xml
  39. 151 0
      app/src/main/res/layout/dialog_recording_instructions.xml
  40. 400 0
      app/src/main/res/layout/fragment_clone_voice.xml
  41. 23 0
      app/src/main/res/values/strings.xml
  42. 11 0
      app/src/main/res/values/style.xml

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

@@ -5,6 +5,7 @@
 
     <!-- 允许访问网络,必选权限 -->
     <uses-permission android:name="android.permission.INTERNET" />
+    <uses-permission android:name="android.permission.RECORD_AUDIO" />
 
     <application
         android:name=".App"
@@ -51,6 +52,9 @@
         <activity
             android:name=".module.setting.SettingActivity"
             android:screenOrientation="portrait" />
+        <activity
+            android:name=".module.cloning.VoiceCloningActivity"
+            android:screenOrientation="portrait" />
 
         <provider
             android:name="androidx.core.content.FileProvider"

+ 6 - 1
app/src/main/java/com/atmob/voiceai/data/api/GenerateApi.java

@@ -6,9 +6,13 @@ import com.atmob.voiceai.data.api.request.VoiceCloneRequest;
 import com.atmob.voiceai.data.api.response.TextToSpeechResponse;
 import com.atmob.voiceai.data.api.response.VoiceCloneResponse;
 
+import atmob.okhttp3.MultipartBody;
+import atmob.okhttp3.RequestBody;
 import atmob.reactivex.rxjava3.core.Single;
 import atmob.retrofit2.http.Body;
+import atmob.retrofit2.http.Multipart;
 import atmob.retrofit2.http.POST;
+import atmob.retrofit2.http.Part;
 
 public interface GenerateApi {
 
@@ -18,5 +22,6 @@ public interface GenerateApi {
 
 
     @POST("/project/voice/v1/voice/clone")
-    Single<BaseResponse<VoiceCloneResponse>> voiceClone(@Body VoiceCloneRequest request);
+    Single<BaseResponse<VoiceCloneResponse>> voiceClone(@Body RequestBody requestBody);
+
 }

+ 1 - 8
app/src/main/java/com/atmob/voiceai/data/api/request/VoiceCloneRequest.java

@@ -10,20 +10,13 @@ public class VoiceCloneRequest extends BaseRequest {
     @SerializedName("type")
     private int type;
 
-    @SerializedName("file")
-    private File file;
 
-
-    public VoiceCloneRequest(int type, File file) {
+    public VoiceCloneRequest(int type) {
         this.type = type;
-        this.file = file;
     }
 
     public int getType() {
         return type;
     }
 
-    public File getFile() {
-        return file;
-    }
 }

+ 132 - 6
app/src/main/java/com/atmob/voiceai/data/repositories/CloneRepository.java

@@ -1,26 +1,48 @@
 package com.atmob.voiceai.data.repositories;
 
 
+import android.net.Uri;
+import android.text.TextUtils;
+
+import androidx.annotation.IntDef;
 import androidx.lifecycle.LiveData;
 import androidx.lifecycle.MutableLiveData;
 
 import com.atmob.app.lib.handler.RxHttpHandler;
 import com.atmob.app.lib.livedata.SingleLiveEvent;
+import com.atmob.common.runtime.ContextUtil;
 import com.atmob.voiceai.data.api.AtmobApi;
 import com.atmob.voiceai.data.api.GenerateApi;
+import com.atmob.voiceai.data.api.bean.CloneVoiceListBean;
 import com.atmob.voiceai.data.api.request.BaseRequest;
 import com.atmob.voiceai.data.api.request.CloneDeleteRequest;
 import com.atmob.voiceai.data.api.request.VoiceCloneRequest;
 import com.atmob.voiceai.data.api.response.VoiceCloneListResponse;
 import com.atmob.voiceai.data.api.response.VoiceCloneResponse;
+import com.atmob.voiceai.helper.ErrorHelper;
+import com.atmob.voiceai.utils.FileUtils;
+import com.atmob.voiceai.utils.ReflectionUtil;
+import com.atmob.voiceai.utils.ToastUtil;
+import com.google.gson.Gson;
+import com.google.gson.annotations.SerializedName;
 
 import java.io.File;
+import java.lang.reflect.Field;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
 import java.util.concurrent.TimeUnit;
 
 import javax.inject.Inject;
 import javax.inject.Singleton;
 
+import atmob.okhttp3.MediaType;
+import atmob.okhttp3.MultipartBody;
+import atmob.okhttp3.RequestBody;
+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
@@ -31,32 +53,136 @@ public class CloneRepository {
     private final VoiceAIRepository voiceAIRepository;
     private final GenerateApi generateApi;
 
+    private final MutableLiveData<List<CloneVoiceListBean>> cloneVoiceList = new MutableLiveData<>();
+    private final MutableLiveData<Integer> cloneUploadState = new MutableLiveData<>();
+
+    private boolean requestListFlag = false;
+    private boolean requestCloneFlag = false;
+
+    @IntDef({UploadState.CLONING, UploadState.UPLOAD_SUCCESS, UploadState.ERROR})
+    public @interface UploadState {
+        int CLONING = 1;
+        int UPLOAD_SUCCESS = 2;
+        int ERROR = 3;
+    }
+
     @Inject
     public CloneRepository(AtmobApi atmobApi, GenerateApi generateApi, VoiceAIRepository voiceAIRepository) {
         this.atmobApi = atmobApi;
         this.generateApi = generateApi;
         this.voiceAIRepository = voiceAIRepository;
+        refreshCloneList();
+    }
+
+    public LiveData<Integer> getCloneUploadState() {
+        return cloneUploadState;
+    }
+
+    public LiveData<List<CloneVoiceListBean>> getCloneVoiceList() {
+        return cloneVoiceList;
+    }
+
+    public void refreshCloneList() {
+        if (requestListFlag) {
+            return;
+        }
+        getCloneList().subscribe(new SingleObserver<VoiceCloneListResponse>() {
+            @Override
+            public void onSubscribe(@NonNull Disposable d) {
+                requestListFlag = true;
+            }
+
+            @Override
+            public void onSuccess(@NonNull VoiceCloneListResponse voiceCloneListResponse) {
+                requestListFlag = false;
+                cloneVoiceList.setValue(voiceCloneListResponse.getCloneVoiceList());
+            }
+
+            @Override
+            public void onError(@NonNull Throwable e) {
+                requestListFlag = false;
+                ErrorHelper.errorThrowableToast(e, ToastUtil.LENGTH_SHORT);
+            }
+        });
+    }
+
+    public void requestVoiceClone(int type, @NonNull Uri uri) {
+        if (requestCloneFlag) {
+            return;
+        }
+        File file = FileUtils.uriToFileApiQ(uri, ContextUtil.getContext());
+        voiceClone(type, file).subscribe(new SingleObserver<VoiceCloneResponse>() {
+            @Override
+            public void onSubscribe(@NonNull Disposable d) {
+                requestCloneFlag = true;
+                cloneUploadState.setValue(UploadState.CLONING);
+            }
+
+            @Override
+            public void onSuccess(@NonNull VoiceCloneResponse voiceCloneResponse) {
+                requestCloneFlag = false;
+                cloneUploadState.setValue(UploadState.UPLOAD_SUCCESS);
+            }
+
+            @Override
+            public void onError(@NonNull Throwable e) {
+                requestCloneFlag = false;
+                cloneUploadState.setValue(UploadState.ERROR);
+                ErrorHelper.errorThrowableToast(e, ToastUtil.LENGTH_SHORT);
+            }
+        });
     }
 
-    public Single<VoiceCloneListResponse> requestCloneList() {
+    private Single<VoiceCloneListResponse> getCloneList() {
         return atmobApi.getCloneList(new BaseRequest())
                 .compose(RxHttpHandler.handle(true))
                 .retryWhen(RxJavaUtil.exponentialBackOff(null, 3, TimeUnit.SECONDS))
                 .compose(RxJavaUtil.SingleSchedule.io2Main());
     }
 
-    public Single<Object> requestCloneDelete(int id) {
+    public Single<Object> cloneDelete(int id) {
         return atmobApi.cloneDelete(new CloneDeleteRequest(id))
                 .compose(RxHttpHandler.handle(true))
                 .compose(RxJavaUtil.SingleSchedule.io2Main())
-                .doOnSuccess(response -> voiceAIRepository.setCloneExecuteSuccessEvent());
+                .doOnSuccess(response -> {
+                    refreshCloneList();
+                    voiceAIRepository.setCloneExecuteSuccessEvent();
+                });
 
     }
 
-    public Single<VoiceCloneResponse> requestVoiceClone(int type, File file) {
-        return generateApi.voiceClone(new VoiceCloneRequest(type, file))
+    private Single<VoiceCloneResponse> voiceClone(int type, File file) {
+        RequestBody fileRQ = RequestBody.create(file, MediaType.parse("audio/*"));
+        MultipartBody.Builder builder = new MultipartBody.Builder();
+        VoiceCloneRequest voiceCloneRequest = new VoiceCloneRequest(type);
+        List<Field> allField = ReflectionUtil.getAllField(VoiceCloneRequest.class);
+        for (Field field : allField) {
+            try {
+                field.setAccessible(true);
+                Object value = field.get(voiceCloneRequest);
+                SerializedName serializedName = field.getAnnotation(SerializedName.class);
+                String name = null;
+                if (serializedName != null && serializedName.value() != null) {
+                    name = serializedName.value();
+                } else {
+                    name = field.getName();
+                }
+                if (value != null) {
+                    builder.addFormDataPart(name, value.toString());
+                }
+            } catch (IllegalAccessException e) {
+                e.printStackTrace();
+            }
+        }
+        builder.addFormDataPart("file", file.getName(), fileRQ);
+        builder.setType(MultipartBody.FORM);
+        RequestBody body = builder.build();
+        return generateApi.voiceClone(body)
                 .compose(RxHttpHandler.handle(false))
                 .compose(RxJavaUtil.SingleSchedule.io2Main())
-                .doOnSuccess(response -> voiceAIRepository.setCloneExecuteSuccessEvent());
+                .doOnSuccess(response -> {
+                    refreshCloneList();
+                    voiceAIRepository.setCloneExecuteSuccessEvent();
+                });
     }
 }

+ 8 - 0
app/src/main/java/com/atmob/voiceai/data/repositories/VoiceAIRepository.java

@@ -47,6 +47,7 @@ public class VoiceAIRepository {
     private final HistoryRepository historyRepository;
     private final MemberRepository memberRepository;
 
+    private final SingleLiveEvent<?> cloneExecuteSuccess = new SingleLiveEvent<>();
     private boolean textToSpeechRequest;
     private final MutableLiveData<Integer> adFreeGenerateNumber = new MutableLiveData<>();
 
@@ -76,6 +77,13 @@ public class VoiceAIRepository {
         refreshVoiceInfo();
     }
 
+    public SingleLiveEvent<?> getCloneExecuteSuccess() {
+        return cloneExecuteSuccess;
+    }
+
+    public void setCloneExecuteSuccessEvent() {
+        cloneExecuteSuccess.call();
+    }
 
     public LiveData<Integer> getAdFreeGenerateNumber() {
         return adFreeGenerateNumber;

+ 38 - 0
app/src/main/java/com/atmob/voiceai/dialog/CloneDeleteSureDialog.java

@@ -0,0 +1,38 @@
+package com.atmob.voiceai.dialog;
+
+import android.content.Context;
+
+import androidx.annotation.NonNull;
+
+import com.atmob.app.lib.base.BaseDialog;
+import com.atmob.voiceai.R;
+import com.atmob.voiceai.databinding.DialogCloneDeleteSureBinding;
+
+@BaseDialog.FullScreen(height = false)
+public class CloneDeleteSureDialog extends BaseDialog<DialogCloneDeleteSureBinding> {
+
+    private OnSureClickListener onSureClickListener;
+
+
+    public void setOnSureClickListener(OnSureClickListener onSureClickListener) {
+        this.onSureClickListener = onSureClickListener;
+    }
+
+    public CloneDeleteSureDialog(@NonNull Context context) {
+        super(context, R.style.Theme_Common_Dialog);
+        binding.setCancelClick(v -> {
+            if (onSureClickListener != null)
+                onSureClickListener.onSureClick();
+            dismiss();
+        });
+        binding.setDeleteClick(v -> {
+            if (onSureClickListener != null)
+                onSureClickListener.onSureClick();
+            dismiss();
+        });
+    }
+
+    public interface OnSureClickListener {
+        void onSureClick();
+    }
+}

+ 51 - 0
app/src/main/java/com/atmob/voiceai/dialog/RecordingInstructionsDialog.java

@@ -0,0 +1,51 @@
+package com.atmob.voiceai.dialog;
+
+import android.content.Context;
+import android.view.Gravity;
+
+import androidx.annotation.NonNull;
+
+import com.atmob.app.lib.base.BaseDialog;
+import com.atmob.voiceai.R;
+import com.atmob.voiceai.databinding.DialogRecordingInstructionsBinding;
+
+
+@BaseDialog.FullScreen(height = false, gravity = Gravity.BOTTOM)
+public class RecordingInstructionsDialog extends BaseDialog<DialogRecordingInstructionsBinding> {
+
+    boolean isCheck;
+
+    private OnRecordingListener onRecordingListener;
+
+    public RecordingInstructionsDialog(@NonNull Context context) {
+        super(context, R.style.Theme_Common_BottomSheetDialog);
+        setCheckBox();
+        binding.setDoNotShowAgainClick(v -> {
+            isCheck = !isCheck;
+            setCheckBox();
+        });
+        binding.setOnCloseClick(v -> dismiss());
+        binding.setKnowClick(v -> {
+            if (onRecordingListener != null) {
+                onRecordingListener.onKnowClick();
+            }
+            dismiss();
+        });
+    }
+
+    public void setOnRecordingListener(OnRecordingListener onRecordingListener) {
+        this.onRecordingListener = onRecordingListener;
+    }
+
+    private void setCheckBox() {
+        if (isCheck) {
+            binding.ivCheckbox.setImageResource(R.drawable.icon_checkbox_selected);
+        } else {
+            binding.ivCheckbox.setImageResource(R.drawable.icon_checkbox_no_select);
+        }
+    }
+
+    public interface OnRecordingListener {
+        void onKnowClick();
+    }
+}

+ 1 - 1
app/src/main/java/com/atmob/voiceai/helper/ErrorHelper.java

@@ -13,7 +13,7 @@ public class ErrorHelper {
             RxHttpHandler.ServerErrorException serverErrorException = (RxHttpHandler.ServerErrorException) throwable;
             int errorResId = getErrorResId(serverErrorException.getCode());
             if (errorResId == -1) {
-                ToastUtil.show(serverErrorException.getMessage(), duration);
+                ToastUtil.show(serverErrorException.getMsg(), duration);
             } else {
                 ToastUtil.show(errorResId, duration);
             }

+ 13 - 0
app/src/main/java/com/atmob/voiceai/module/clonevoice/CloneState.java

@@ -0,0 +1,13 @@
+package com.atmob.voiceai.module.clonevoice;
+
+import androidx.annotation.IntDef;
+
+@IntDef({CloneState.NO_UPLOAD_DATA, CloneState.UPLOAD_CHOICE_LOCAL, CloneState.UPLOAD_CHOICE_RECORDING, CloneState.RECORDING_VOICE, CloneState.UPLOADING, CloneState.CLONE_SUCCESS})
+public @interface CloneState {
+    int NO_UPLOAD_DATA = 1;
+    int UPLOAD_CHOICE_LOCAL = 2;
+    int UPLOAD_CHOICE_RECORDING = 3;
+    int RECORDING_VOICE = 4;
+    int UPLOADING = 5;
+    int CLONE_SUCCESS = 6;
+}

+ 211 - 1
app/src/main/java/com/atmob/voiceai/module/clonevoice/CloneVoiceFragment.java

@@ -1,17 +1,36 @@
 package com.atmob.voiceai.module.clonevoice;
 
+import android.app.Activity;
+import android.content.Intent;
+import android.net.Uri;
 import android.os.Bundle;
 import android.view.View;
+import android.widget.SeekBar;
 
+import androidx.activity.result.ActivityResult;
+import androidx.activity.result.contract.ActivityResultContracts;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.core.widget.NestedScrollView;
+import androidx.media3.common.MediaItem;
+import androidx.media3.common.PlaybackException;
+import androidx.media3.common.Player;
+import androidx.media3.exoplayer.ExoPlayer;
 
 import com.atmob.app.lib.base.BaseFragment;
+import com.atmob.common.logging.AtmobLog;
 import com.atmob.common.ui.SizeUtil;
+import com.atmob.voiceai.R;
 import com.atmob.voiceai.databinding.FragmentCloneVoiceBinding;
+import com.atmob.voiceai.dialog.CloneDeleteSureDialog;
+import com.atmob.voiceai.dialog.RecordingInstructionsDialog;
+import com.atmob.voiceai.utils.ActivityForResultUtil;
+import com.atmob.voiceai.utils.AudioRecorder;
+import com.atmob.voiceai.utils.ToastUtil;
 import com.gyf.immersionbar.ImmersionBar;
 
+import java.io.IOException;
+
 import dagger.hilt.android.AndroidEntryPoint;
 
 @AndroidEntryPoint
@@ -19,8 +38,11 @@ public class CloneVoiceFragment extends BaseFragment<FragmentCloneVoiceBinding>
 
 
     private CloneVoiceViewModel cloneVoiceViewModel;
-
     private int maxScrollY;
+    private ExoPlayer player;
+    private CloneDeleteSureDialog cloneDeleteSureDialog;
+    private RecordingInstructionsDialog recordingInstructionsDialog;
+    private AudioRecorder audioRecorder;
 
     @Override
     public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
@@ -30,7 +52,104 @@ public class CloneVoiceFragment extends BaseFragment<FragmentCloneVoiceBinding>
     }
 
     private void initView() {
+        initMedia3ExoPlayer();
         initScrollView();
+        initSeekBar();
+        initPlay();
+    }
+
+
+    public void showRecordingInstructionsDialog() {
+        if (recordingInstructionsDialog == null) {
+            recordingInstructionsDialog = new RecordingInstructionsDialog(requireContext());
+            recordingInstructionsDialog.setOnRecordingListener(() -> {
+                cloneVoiceViewModel.onDialogKnowClick();
+            });
+        }
+        recordingInstructionsDialog.show();
+    }
+
+    private void initSeekBar() {
+        binding.voiceSeekBar.setProgress(0);
+        binding.voiceSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
+            @Override
+            public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+                if (fromUser) {
+                    player.seekTo(progress);
+                }
+            }
+
+            @Override
+            public void onStartTrackingTouch(SeekBar seekBar) {
+                cloneVoiceViewModel.setSeekbarChanging(true);
+            }
+
+            @Override
+            public void onStopTrackingTouch(SeekBar seekBar) {
+                cloneVoiceViewModel.setSeekbarChanging(false);
+                cloneVoiceViewModel.setSeekbarTo(seekBar.getProgress());
+            }
+        });
+        cloneVoiceViewModel.startMediaTimer();
+    }
+
+
+    private void initMedia3ExoPlayer() {
+        player = new ExoPlayer.Builder(requireContext()).build();
+        player.addListener(new Player.Listener() {
+
+            @Override
+            public void onPlayerError(PlaybackException error) {
+                AtmobLog.d("zk", "onPlayerError: " + error.getMessage());
+                ToastUtil.show(R.string.voice_play_error, ToastUtil.LENGTH_SHORT);
+                setVoicePlayEnd();
+            }
+
+            @Override
+            public void onPlaybackStateChanged(int state) {
+                AtmobLog.d("zk", "onPlaybackStateChanged: " + state);
+                if (Player.STATE_READY == state) {
+                    cloneVoiceViewModel.setTotalDuration(player.getDuration());
+                    binding.voiceSeekBar.setMax((int) player.getDuration());
+                } else if (Player.STATE_ENDED == state) {
+                    setVoicePlayEnd();
+                }
+            }
+        });
+    }
+
+    private void setVoicePlayEnd() {
+        player.seekTo(0);
+        cloneVoiceViewModel.setCurrentDuration(0);
+        player.pause();
+        cloneVoiceViewModel.setVoicePlay(false);
+    }
+
+    private void initPlay() {
+        binding.ivPlay.setOnClickListener(view -> {
+            if (player.isLoading()) {
+                return;
+            }
+            if (player.isPlaying()) {
+                player.pause();
+                cloneVoiceViewModel.setVoicePlay(false);
+            } else {
+                player.play();
+                cloneVoiceViewModel.setVoicePlay(true);
+            }
+        });
+        binding.ivVoiceReduce.setOnClickListener(view -> {
+            if (player.isLoading()) {
+                return;
+            }
+            player.seekTo(player.getCurrentPosition() - 5000);
+        });
+        binding.ivVoiceSpeed.setOnClickListener(view -> {
+            if (player.isLoading()) {
+                return;
+            }
+            player.seekTo(player.getCurrentPosition() + 5000);
+        });
     }
 
     private void initScrollView() {
@@ -47,7 +166,98 @@ public class CloneVoiceFragment extends BaseFragment<FragmentCloneVoiceBinding>
     }
 
     private void initObserver() {
+        cloneVoiceViewModel.getStopRecordAudioEvent().observe(getViewLifecycleOwner(), o -> stopRecordAudio());
+        cloneVoiceViewModel.getStartRecordAudioEvent().observe(getViewLifecycleOwner(), o -> startRecordAudio());
+        cloneVoiceViewModel.getRequestAudioPermission().observe(getViewLifecycleOwner(), o -> requestAudioPermission());
+        cloneVoiceViewModel.getShowRecordDialogEvent().observe(getViewLifecycleOwner(), o -> showRecordingInstructionsDialog());
+        cloneVoiceViewModel.getDeleteEvent().observe(getViewLifecycleOwner(), id -> {
+            if (id == null) {
+                return;
+            }
+            showDeleteSureDialog(id);
+        });
+        cloneVoiceViewModel.getRefreshAudioCurrentProgress().observe(getViewLifecycleOwner(), o -> {
+            long currentPosition = player.getCurrentPosition();
+            cloneVoiceViewModel.setCurrentDuration(currentPosition);
+            binding.voiceSeekBar.setProgress((int) currentPosition);
+        });
+        cloneVoiceViewModel.getUploadVoiceUri().observe(getViewLifecycleOwner(), uri -> {
+            if (uri != null) {
+                MediaItem mediaItem = MediaItem.fromUri(uri);
+                player.setMediaItem(mediaItem);
+                player.prepare();
+                player.setPlayWhenReady(false);
+            } else {
+                player.pause();
+            }
+        });
+        cloneVoiceViewModel.getClickLocalUpload().observe(getViewLifecycleOwner(), o -> choiceLocalFile());
+    }
+
+    private void startRecordAudio() {
+        if (audioRecorder == null) {
+            audioRecorder = new AudioRecorder();
+            audioRecorder.setMaxDuration(CloneVoiceViewModel.RECORD_AUDIO_MAX_DURATION);
+//            audioRecorder.setupMediaRecorder();
+        }
+        try {
+            audioRecorder.startRecording();
+        } catch (IOException e) {
+            ToastUtil.show(e.getMessage(), ToastUtil.LENGTH_SHORT);
+        }
+    }
 
+    private void stopRecordAudio() {
+        if (audioRecorder != null) {
+            audioRecorder.stopRecording();
+        }
+    }
+
+    private void requestAudioPermission() {
+        new ActivityForResultUtil<String, Boolean>().startActivityForResult(requireActivity(), new ActivityResultContracts.RequestPermission(), CloneVoiceViewModel.PERMISSION_AUDIO, result -> {
+            if (result) {
+                cloneVoiceViewModel.startRecordAudio();
+            }
+        });
+    }
+
+    private void showDeleteSureDialog(int id) {
+        if (cloneDeleteSureDialog == null) {
+            cloneDeleteSureDialog = new CloneDeleteSureDialog(requireContext());
+        }
+        cloneDeleteSureDialog.setOnSureClickListener(() -> cloneVoiceViewModel.deleteClone(id));
+        cloneDeleteSureDialog.show();
+    }
+
+    @Override
+    public void onPause() {
+        super.onPause();
+        cloneVoiceViewModel.setVoicePlay(false);
+        if (player != null) {
+            player.pause();
+        }
+    }
+
+
+    @Override
+    public void onDestroy() {
+        super.onDestroy();
+        if (player != null) {
+            player.stop();
+            player.release();
+        }
+    }
+
+    private void choiceLocalFile() {
+        Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
+        intent.addCategory(Intent.CATEGORY_OPENABLE);
+        intent.setType("audio/*");
+        new ActivityForResultUtil<Intent, ActivityResult>().startActivityForResult(requireActivity(), new ActivityResultContracts.StartActivityForResult(), intent, result -> {
+            Uri uri;
+            if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null && (uri = result.getData().getData()) != null) {
+                cloneVoiceViewModel.setLocalVoiceUri(uri);
+            }
+        });
     }
 
     @Override

+ 260 - 42
app/src/main/java/com/atmob/voiceai/module/clonevoice/CloneVoiceViewModel.java

@@ -1,22 +1,36 @@
 package com.atmob.voiceai.module.clonevoice;
 
+import android.Manifest;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.Looper;
+
 import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MediatorLiveData;
 import androidx.lifecycle.MutableLiveData;
 
 import com.atmob.app.lib.base.BaseViewModel;
+import com.atmob.app.lib.livedata.SingleLiveEvent;
+import com.atmob.common.data.KVUtils;
 import com.atmob.common.runtime.ActivityUtil;
 import com.atmob.common.runtime.ContextUtil;
 import com.atmob.voiceai.R;
-import com.atmob.voiceai.data.api.response.VoiceCloneListResponse;
-import com.atmob.voiceai.data.api.response.VoiceCloneResponse;
+import com.atmob.voiceai.data.api.bean.CloneVoiceListBean;
 import com.atmob.voiceai.data.repositories.CloneRepository;
 import com.atmob.voiceai.data.repositories.MemberRepository;
 import com.atmob.voiceai.helper.ErrorHelper;
+import com.atmob.voiceai.module.cloning.VoiceCloningActivity;
 import com.atmob.voiceai.module.setting.SettingActivity;
 import com.atmob.voiceai.module.subscription.SubscriptionPageActivity;
+import com.atmob.voiceai.utils.FileUtils;
+import com.atmob.voiceai.utils.PermissionUtil;
 import com.atmob.voiceai.utils.SpannableUtil;
 import com.atmob.voiceai.utils.ToastUtil;
 
+import java.util.List;
+import java.util.Timer;
+import java.util.TimerTask;
+
 import javax.inject.Inject;
 
 import atmob.reactivex.rxjava3.annotations.NonNull;
@@ -29,20 +43,112 @@ import dagger.hilt.android.lifecycle.HiltViewModel;
 public class CloneVoiceViewModel extends BaseViewModel {
 
 
+    public static final int RECORD_AUDIO_MAX_DURATION = 25 * 1000;
+    public static final String PERMISSION_AUDIO = Manifest.permission.RECORD_AUDIO;
+    public final String RECORD_DIALOG_SHOW = "record_dialog_show";
+
+    public static final int FILE_UPLOAD = 1;
+    public static final int RECORD_UPLOAD = 2;
+
+    private final MutableLiveData<String> uploadPreviewName = new MutableLiveData<>();
+    private final MutableLiveData<String> uploadPreviewSize = new MutableLiveData<>();
+    private final SingleLiveEvent<Integer> deleteEvent = new SingleLiveEvent<>();
+    private final SingleLiveEvent<?> refreshAudioCurrentProgress = new SingleLiveEvent<>();
+    private final SingleLiveEvent<?> requestAudioPermission = new SingleLiveEvent<>();
+    private final SingleLiveEvent<?> startRecordAudioEvent = new SingleLiveEvent<>();
+    private final SingleLiveEvent<?> stopRecordAudioEvent = new SingleLiveEvent<>();
+    private final SingleLiveEvent<?> showRecordDialogEvent = new SingleLiveEvent<>();
+    private final MutableLiveData<Integer> seekBarProgress = new MutableLiveData<>();
+    private final MutableLiveData<Boolean> isPlay = new MutableLiveData<>();
+    private final MutableLiveData<Long> totalDuration = new MutableLiveData<>();
+    private final MutableLiveData<Long> currentDuration = new MutableLiveData<>();
     private final MutableLiveData<CharSequence> cloneTip = new MutableLiveData<>();
 
+    private final MediatorLiveData<Integer> cloneState = new MediatorLiveData<>();
 
+    private final SingleLiveEvent<?> clickLocalUpload = new SingleLiveEvent<>();
+
+    private final MutableLiveData<Uri> uploadVoiceUri = new MutableLiveData<>();
     private final MemberRepository memberRepository;
     private final CloneRepository cloneRepository;
 
-    private boolean requestCloneFlag = false;
+    private boolean isSeekbarChanging;
+    private Timer timer;
+
+    private Handler countDownHandler;
+
 
     @Inject
     public CloneVoiceViewModel(MemberRepository memberRepository, CloneRepository cloneRepository) {
         this.memberRepository = memberRepository;
         this.cloneRepository = cloneRepository;
         initCloneTipTxt();
-        refreshCloneList();
+        initCloneState();
+    }
+
+    public LiveData<?> getStopRecordAudioEvent() {
+        return stopRecordAudioEvent;
+    }
+
+    public LiveData<?> getStartRecordAudioEvent() {
+        return startRecordAudioEvent;
+    }
+
+    public LiveData<?> getRequestAudioPermission() {
+        return requestAudioPermission;
+    }
+
+    public LiveData<?> getShowRecordDialogEvent() {
+        return showRecordDialogEvent;
+    }
+
+    public LiveData<Integer> getDeleteEvent() {
+        return deleteEvent;
+    }
+
+    public LiveData<String> getUploadPreviewName() {
+        return uploadPreviewName;
+    }
+
+    public LiveData<String> getUploadPreviewSize() {
+        return uploadPreviewSize;
+    }
+
+    public LiveData<?> getRefreshAudioCurrentProgress() {
+        return refreshAudioCurrentProgress;
+    }
+
+    public LiveData<Integer> getSeekBarProgress() {
+        return seekBarProgress;
+    }
+
+    public LiveData<Boolean> getIsPlay() {
+        return isPlay;
+    }
+
+
+    public LiveData<Long> getCurrentDuration() {
+        return currentDuration;
+    }
+
+    public LiveData<Long> getTotalDuration() {
+        return totalDuration;
+    }
+
+    public LiveData<Uri> getUploadVoiceUri() {
+        return uploadVoiceUri;
+    }
+
+    public LiveData<?> getClickLocalUpload() {
+        return clickLocalUpload;
+    }
+
+    public LiveData<Integer> getCloneState() {
+        return cloneState;
+    }
+
+    public LiveData<List<CloneVoiceListBean>> getCloneVoiceList() {
+        return cloneRepository.getCloneVoiceList();
     }
 
 
@@ -55,59 +161,77 @@ public class CloneVoiceViewModel extends BaseViewModel {
     }
 
 
-    private void initCloneTipTxt() {
-        String allTxt = ContextUtil.getContext().getString(R.string.clone_voice_tip);
-        String targetTxt = ContextUtil.getContext().getString(R.string.clone_voice_tip_create);
-        cloneTip.setValue(SpannableUtil.getSpannableStringBuilder(allTxt, targetTxt, ContextUtil.getContext().getResources().getColor(R.color.colorPrimaryVariant), false));
+    public void setSeekbarChanging(boolean seekbarChanging) {
+        isSeekbarChanging = seekbarChanging;
     }
 
-    public void refreshCloneList() {
-        if (requestCloneFlag) {
-            return;
+
+    public void startMediaTimer() {
+        if (timer != null) {
+            timer.cancel();
         }
-        cloneRepository.requestCloneList().subscribe(new SingleObserver<VoiceCloneListResponse>() {
+        timer = new Timer();
+        timer.schedule(new TimerTask() {
             @Override
-            public void onSubscribe(@NonNull Disposable d) {
-                addDisposable(d);
-                requestCloneFlag = true;
+            public void run() {
+                if (isSeekbarChanging) {
+                    return;
+                }
+                refreshAudioCurrentProgress.postValue(null);
             }
+        }, 0, 25);
+    }
 
-            @Override
-            public void onSuccess(@NonNull VoiceCloneListResponse voiceCloneListResponse) {
-                requestCloneFlag = false;
+    public void setSeekbarTo(int progress) {
+        this.seekBarProgress.setValue(progress);
+    }
 
-            }
 
-            @Override
-            public void onError(@NonNull Throwable e) {
-                requestCloneFlag = false;
-                ErrorHelper.errorThrowableToast(e, ToastUtil.LENGTH_SHORT);
+    public void setCurrentDuration(long duration) {
+        if (duration >= 0) {
+            currentDuration.setValue(duration);
+        }
+    }
+
+    public void setVoicePlay(boolean isPlay) {
+        this.isPlay.setValue(isPlay);
+    }
+
+    public void setTotalDuration(long duration) {
+        if (duration >= 0) {
+            totalDuration.setValue(duration);
+        }
+    }
+
+    private void initCloneState() {
+        cloneState.addSource(cloneRepository.getCloneVoiceList(), list -> {
+            uploadVoiceUri.setValue(null);
+            if (list == null || list.isEmpty()) {
+                cloneState.setValue(CloneState.NO_UPLOAD_DATA);
+            } else {
+                cloneState.setValue(CloneState.CLONE_SUCCESS);
             }
         });
     }
 
-    public void requestCloneVoice() {
+    private void initCloneTipTxt() {
+        String allTxt = ContextUtil.getContext().getString(R.string.clone_voice_tip);
+        String targetTxt = ContextUtil.getContext().getString(R.string.clone_voice_tip_create);
+        cloneTip.setValue(SpannableUtil.getSpannableStringBuilder(allTxt, targetTxt, ContextUtil.getContext().getResources().getColor(R.color.colorPrimaryVariant), false));
+    }
+
 
-//        cloneRepository.requestVoiceClone().subscribe(new SingleObserver<VoiceCloneResponse>() {
-//            @Override
-//            public void onSubscribe(@NonNull Disposable d) {
-//                addDisposable(d);
-//            }
-//
-//            @Override
-//            public void onSuccess(@NonNull VoiceCloneResponse voiceCloneResponse) {
-//                  refreshCloneList();
-//            }
-//
-//            @Override
-//            public void onError(@NonNull Throwable e) {
-//                  ErrorHelper.errorThrowableToast(e, ToastUtil.LENGTH_SHORT);
-//            }
-//        });
+    public void requestCloneVoice(int type) {
+        Uri value = uploadVoiceUri.getValue();
+        if (value == null) {
+            return;
+        }
+        VoiceCloningActivity.start(ActivityUtil.getTopActivity());
+        cloneRepository.requestVoiceClone(type, value);
     }
 
     public void deleteClone(int id) {
-        cloneRepository.requestCloneDelete(id).subscribe(new SingleObserver<Object>() {
+        cloneRepository.cloneDelete(id).subscribe(new SingleObserver<Object>() {
             @Override
             public void onSubscribe(@NonNull Disposable d) {
                 addDisposable(d);
@@ -115,7 +239,7 @@ public class CloneVoiceViewModel extends BaseViewModel {
 
             @Override
             public void onSuccess(@NonNull Object o) {
-                refreshCloneList();
+
             }
 
             @Override
@@ -132,4 +256,98 @@ public class CloneVoiceViewModel extends BaseViewModel {
     public void onSettingClick() {
         SettingActivity.start(ActivityUtil.getTopActivity());
     }
+
+    public void onLocalUploadClick() {
+        clickLocalUpload.call();
+    }
+
+    public void setLocalVoiceUri(@NonNull Uri uri) {
+        //判断是否符合要求
+        //文件小于3M  时长大于20s且小于30s
+        long durationLong = FileUtils.getAudioDurationFromUri(ContextUtil.getContext(), uri);
+        if (durationLong < 20000 || durationLong > 30000) {
+            ToastUtil.show(R.string.clone_voice_duration_error, ToastUtil.LENGTH_SHORT);
+            return;
+        }
+        long fileSize = FileUtils.getFileSizeFromUri(ContextUtil.getContext(), uri);
+        if (fileSize > 3 * 1024 * 1024) {
+            ToastUtil.show(R.string.clone_voice_size_error, ToastUtil.LENGTH_SHORT);
+            return;
+        }
+        String name = FileUtils.getFileNameFromUri(ContextUtil.getContext(), uri);
+        uploadPreviewName.setValue(name);
+        uploadPreviewSize.setValue(FileUtils.formatShortBytes(fileSize));
+        this.uploadVoiceUri.setValue(uri);
+        cloneState.setValue(CloneState.UPLOAD_CHOICE_LOCAL);
+    }
+
+    public void onLocalReSelectClick() {
+        this.uploadVoiceUri.setValue(null);
+        cloneState.setValue(CloneState.NO_UPLOAD_DATA);
+    }
+
+    public void onReRecordClick() {
+
+    }
+
+    public void onCloneVoiceClick(int state) {
+        if (state == CloneState.UPLOAD_CHOICE_LOCAL) {
+            requestCloneVoice(FILE_UPLOAD);
+        } else {
+            requestCloneVoice(RECORD_UPLOAD);
+        }
+    }
+
+
+    public void useCloneVoiceClick() {
+
+    }
+
+    public void onDeleteClick() {
+        List<CloneVoiceListBean> list = cloneRepository.getCloneVoiceList().getValue();
+        if (list == null || list.isEmpty()) {
+            ToastUtil.show(R.string.no_clone_data, ToastUtil.LENGTH_SHORT);
+            return;
+        }
+        deleteEvent.setValue(list.get(0).getId());
+    }
+
+    public void onRecordClick() {
+        if (!PermissionUtil.hasPermission(PERMISSION_AUDIO)) {
+            requestAudioPermission.call();
+        } else if (KVUtils.getDefault().getBoolean(RECORD_DIALOG_SHOW, true)) {
+            showRecordDialogEvent.call();
+        } else {
+            startRecordAudio();
+        }
+    }
+
+    public void startRecordAudio() {
+        startRecordAudioEvent.call();
+        startCountDown();
+    }
+
+    private void startCountDown() {
+        if (countDownHandler == null) {
+            countDownHandler = new Handler(Looper.getMainLooper());
+        }
+        countDownHandler.postDelayed(stopRecordAudioEvent::call, RECORD_AUDIO_MAX_DURATION);
+    }
+
+    public void onDialogKnowClick() {
+        KVUtils.getDefault().putBoolean(RECORD_DIALOG_SHOW, false);
+        startRecordAudio();
+    }
+
+    @Override
+    protected void onCleared() {
+        super.onCleared();
+        if (timer != null) {
+            timer.cancel();
+            timer = null;
+        }
+        if (countDownHandler != null) {
+            countDownHandler.removeCallbacksAndMessages(null);
+        }
+    }
 }

+ 75 - 0
app/src/main/java/com/atmob/voiceai/module/cloning/VoiceCloningActivity.java

@@ -0,0 +1,75 @@
+package com.atmob.voiceai.module.cloning;
+
+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.ActivityVoiceCloneBinding;
+import com.tencent.qgame.animplayer.util.ScaleType;
+
+import dagger.hilt.android.AndroidEntryPoint;
+
+@AndroidEntryPoint
+public class VoiceCloningActivity extends BaseActivity<ActivityVoiceCloneBinding> {
+
+    private VoiceCloningViewModel voiceCloningViewModel;
+
+    public static void start(Context context) {
+        Intent intent = new Intent(context, VoiceCloningActivity.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();
+        startAnim();
+    }
+
+    private void initView() {
+
+    }
+
+    private void initObserver() {
+        voiceCloningViewModel.getCloneUploadState().observe(this, state -> {
+            if (state == null) {
+                return;
+            }
+            finish();
+        });
+    }
+
+    private void startAnim() {
+        binding.animView.setScaleType(ScaleType.FIT_XY);
+        binding.animView.setLoop(Integer.MAX_VALUE);
+        binding.animView.startPlay(this.getAssets(), "anim/voice_ai_clone_loading.mp4");
+    }
+
+    @Override
+    protected void initViewModel() {
+        super.initViewModel();
+        voiceCloningViewModel = getViewModelProvider().get(VoiceCloningViewModel.class);
+        binding.setVoiceViewModel(voiceCloningViewModel);
+    }
+
+
+    @Override
+    protected boolean shouldImmersion() {
+        return true;
+    }
+
+
+    @Override
+    public void onBackPressed() {
+
+    }
+}

+ 30 - 0
app/src/main/java/com/atmob/voiceai/module/cloning/VoiceCloningViewModel.java

@@ -0,0 +1,30 @@
+package com.atmob.voiceai.module.cloning;
+
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.Observer;
+
+import com.atmob.app.lib.base.BaseViewModel;
+import com.atmob.voiceai.data.repositories.CloneRepository;
+
+import javax.inject.Inject;
+
+import dagger.hilt.android.lifecycle.HiltViewModel;
+
+
+@HiltViewModel
+public class VoiceCloningViewModel extends BaseViewModel {
+
+    private final CloneRepository cloneRepository;
+
+
+    @Inject
+    public VoiceCloningViewModel(CloneRepository cloneRepository) {
+        this.cloneRepository = cloneRepository;
+    }
+
+    public LiveData<Integer> getCloneUploadState() {
+        return cloneRepository.getCloneUploadState();
+    }
+
+
+}

+ 2 - 2
app/src/main/java/com/atmob/voiceai/module/generating/VoiceGeneratingActivity.java

@@ -35,7 +35,7 @@ public class VoiceGeneratingActivity extends BaseActivity<ActivityVoiceGeneratin
         super.onCreate(savedInstanceState);
         initView();
         initObserver();
-        startGenerating();
+        startAnim();
     }
 
     private void initObserver() {
@@ -49,7 +49,7 @@ public class VoiceGeneratingActivity extends BaseActivity<ActivityVoiceGeneratin
         });
     }
 
-    private void startGenerating() {
+    private void startAnim() {
         binding.animView.setScaleType(ScaleType.FIT_XY);
         binding.animView.setLoop(Integer.MAX_VALUE);
         binding.animView.startPlay(this.getAssets(), "anim/voice_ai_clone_loading.mp4");

+ 10 - 0
app/src/main/java/com/atmob/voiceai/module/result/VoiceResultViewModel.java

@@ -237,4 +237,14 @@ public class VoiceResultViewModel extends BaseViewModel {
     public void setVoiceBean(UserVoiceBean userVoiceBean) {
         this.userVoiceBean.setValue(userVoiceBean);
     }
+
+
+    @Override
+    protected void onCleared() {
+        super.onCleared();
+        if (timer != null) {
+            timer.cancel();
+            timer = null;
+        }
+    }
 }

+ 11 - 0
app/src/main/java/com/atmob/voiceai/module/voiceai/VoiceAIViewModel.java

@@ -71,6 +71,7 @@ public class VoiceAIViewModel extends BaseViewModel {
     private Disposable voiceListDisposable;
 
     Observer<VoiceListBean> recommendObserver;
+    Observer<Object> cloneObserver;
 
     @Inject
     public VoiceAIViewModel(MemberRepository memberRepository, VoiceAIRepository voiceAIRepository) {
@@ -207,6 +208,13 @@ public class VoiceAIViewModel extends BaseViewModel {
             }
         };
         voiceAIRepository.getRecommendClickBean().observeForever(recommendObserver);
+        if (cloneObserver != null) {
+            voiceAIRepository.getCloneExecuteSuccess().removeObserver(cloneObserver);
+        }
+        cloneObserver = o -> {
+            refreshVoiceList(currentTypeId);
+        };
+        voiceAIRepository.getCloneExecuteSuccess().observeForever(cloneObserver);
     }
 
     public void scrollToListPosition(@NonNull VoiceListBean bean) {
@@ -355,6 +363,9 @@ public class VoiceAIViewModel extends BaseViewModel {
         if (recommendObserver != null) {
             voiceAIRepository.getRecommendClickBean().removeObserver(recommendObserver);
         }
+        if (cloneObserver != null) {
+            voiceAIRepository.getCloneExecuteSuccess().removeObserver(cloneObserver);
+        }
     }
 
 

+ 40 - 0
app/src/main/java/com/atmob/voiceai/utils/AudioRecorder.java

@@ -0,0 +1,40 @@
+package com.atmob.voiceai.utils;
+
+import android.media.MediaRecorder;
+
+import java.io.IOException;
+
+public class AudioRecorder {
+
+    private MediaRecorder mediaRecorder;
+    private String filePath;
+
+    public AudioRecorder() {
+        mediaRecorder = new MediaRecorder();
+        mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
+        mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.AAC_ADTS);
+        mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
+    }
+
+    public void setupMediaRecorder(String outputPath) {
+        filePath = outputPath;
+        mediaRecorder.setOutputFile(filePath);
+    }
+
+
+    public void startRecording() throws IOException {
+        mediaRecorder.prepare();
+        mediaRecorder.start();
+    }
+
+    public void setMaxDuration(int maxDuration) {
+        mediaRecorder.setMaxDuration(maxDuration);
+    }
+
+    public void stopRecording() {
+        mediaRecorder.stop();
+        mediaRecorder.release();
+    }
+
+
+}

+ 2 - 2
app/src/main/java/com/atmob/voiceai/utils/DownloadUtils.java

@@ -32,8 +32,8 @@ public class DownloadUtils {
                 && Environment.getExternalStorageDirectory().canWrite()) {
             cacheDir = ContextUtil.getApplication().getExternalCacheDir();
         } else {
-            cacheDir = ContextUtil.getApplication().getExternalCacheDir();
-//            cacheDir = ContextUtil.getApplication().getCacheDir();
+//            cacheDir = ContextUtil.getApplication().getExternalCacheDir();
+            cacheDir = ContextUtil.getApplication().getCacheDir();
         }
         return cacheDir;
     }

+ 99 - 0
app/src/main/java/com/atmob/voiceai/utils/FileUtils.java

@@ -0,0 +1,99 @@
+package com.atmob.voiceai.utils;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.Cursor;
+import android.media.MediaMetadataRetriever;
+import android.net.Uri;
+import android.provider.DocumentsContract;
+import android.text.format.Formatter;
+import android.webkit.MimeTypeMap;
+
+import com.atmob.common.runtime.ContextUtil;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+import atmob.reactivex.rxjava3.annotations.NonNull;
+
+public class FileUtils {
+
+
+    public static File uriToFileApiQ(@NonNull Uri uri, Context context) {
+        File file = null;
+        if (uri == null) return file;
+        if (uri.getScheme().equals(ContentResolver.SCHEME_FILE)) {
+            file = new File(uri.getPath());
+        } else if (uri.getScheme().equals(ContentResolver.SCHEME_CONTENT)) {
+            ContentResolver contentResolver = context.getContentResolver();
+            String displayName = System.currentTimeMillis() + Math.round((Math.random() + 1) * 1000)
+                    + "." + MimeTypeMap.getSingleton().getExtensionFromMimeType(contentResolver.getType(uri));
+            try {
+                InputStream is = contentResolver.openInputStream(uri);
+                File cache = new File(context.getCacheDir().getAbsolutePath(), displayName);
+                FileOutputStream fos = new FileOutputStream(cache);
+                FileUtils.copy(is, fos);
+                file = cache;
+                fos.close();
+                is.close();
+            } catch (IOException e) {
+                e.printStackTrace();
+            }
+        }
+        return file;
+    }
+
+    public static void copy(InputStream is, FileOutputStream fos) {
+        byte[] buffer = new byte[1024];
+        int len;
+        try {
+            while ((len = is.read(buffer)) != -1) {
+                fos.write(buffer, 0, len);
+            }
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+    }
+
+    public static String formatShortBytes(long bytes) {
+        Context context = ContextUtil.getContext();
+        return Formatter.formatShortFileSize(context, bytes);
+    }
+
+    public static long getAudioDurationFromUri(Context context, Uri uri) {
+        try {
+            MediaMetadataRetriever retriever = new MediaMetadataRetriever();
+            retriever.setDataSource(context, uri);
+            String duration = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION);
+            long durationLong = Long.parseLong(duration);
+            return durationLong;
+        } catch (Exception ignore) {
+
+        }
+        return 0;
+    }
+
+    public static long getFileSizeFromUri(Context context, Uri uri) {
+        try (Cursor cursor = context.getContentResolver().query(uri, new String[]{DocumentsContract.Document.COLUMN_SIZE}, null, null, null)) {
+            if (cursor != null && cursor.moveToFirst()) {
+                return cursor.getLong(0);
+            }
+        } catch (Exception ignore) {
+
+        }
+        return 0;
+    }
+
+    public static String getFileNameFromUri(Context context, Uri uri) {
+        try (Cursor cursor = context.getContentResolver().query(uri, new String[]{DocumentsContract.Document.COLUMN_DISPLAY_NAME}, null, null, null)) {
+            if (cursor != null && cursor.moveToFirst()) {
+                return cursor.getString(0);
+            }
+        } catch (Exception ignore) {
+
+        }
+        return "";
+    }
+}

+ 47 - 0
app/src/main/java/com/atmob/voiceai/utils/PermissionUtil.java

@@ -0,0 +1,47 @@
+package com.atmob.voiceai.utils;
+
+import android.Manifest;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Environment;
+import android.provider.Settings;
+
+import androidx.activity.ComponentActivity;
+import androidx.activity.result.ActivityResult;
+import androidx.activity.result.contract.ActivityResultContracts;
+import androidx.core.content.ContextCompat;
+
+import com.atmob.common.runtime.ContextUtil;
+
+public class PermissionUtil {
+
+
+    public static boolean hasFilePermission() {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+            return Environment.isExternalStorageManager();
+        } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+            return PermissionUtil.hasPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE);
+        } else {
+            return true;
+        }
+    }
+
+    public static boolean hasPermission(String... permissions) {
+        if (permissions == null) {
+            return true;
+        }
+        Context context = ContextUtil.getContext();
+        for (String permission : permissions) {
+            boolean hasPermission = ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED;
+            if (!hasPermission) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+
+}

+ 97 - 0
app/src/main/java/com/atmob/voiceai/utils/ReflectionUtil.java

@@ -0,0 +1,97 @@
+package com.atmob.voiceai.utils;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+
+public final class ReflectionUtil {
+    public static List<Field> getAllField(Class<?> clazz) {
+        ArrayList<Field> fields = new ArrayList<>();
+        while (clazz != null) {
+            fields.addAll(Arrays.asList(clazz.getDeclaredFields()));
+            clazz = clazz.getSuperclass();
+        }
+        return fields;
+    }
+
+    public static Object findField(Object origin, Class<?>... targetClass) {
+        if (origin == null || targetClass == null || targetClass.length == 0) {
+            return null;
+        }
+        HashSet<Class<?>> targetClasses = new HashSet<>(Arrays.asList(targetClass));
+        List<Field> fields = getAllField(origin.getClass());
+        for (Field field : fields) {
+            if (targetClasses.contains(field.getType())) {
+                try {
+                    field.setAccessible(true);
+                    return field.get(origin);
+                } catch (IllegalAccessException e) {
+                    e.printStackTrace();
+                }
+            }
+        }
+        return null;
+    }
+
+    public static Object findField(Object origin, String... targetName) {
+        if (origin == null || targetName == null || targetName.length == 0) {
+            return null;
+        }
+        HashSet<String> targetNames = new HashSet<>(Arrays.asList(targetName));
+        List<Field> fields = getAllField(origin.getClass());
+        for (Field field : fields) {
+            if (targetNames.contains(field.getName())) {
+                try {
+                    field.setAccessible(true);
+                    return field.get(origin);
+                } catch (IllegalAccessException e) {
+                    e.printStackTrace();
+                }
+            }
+        }
+        return null;
+    }
+
+    public static List<Method> getAllMethod(Class<?> clazz) {
+        ArrayList<Method> methods = new ArrayList<>();
+        while (clazz != null) {
+            methods.addAll(Arrays.asList(clazz.getDeclaredMethods()));
+            clazz = clazz.getSuperclass();
+        }
+        return methods;
+    }
+
+    public static Method findMethod(Object origin, String methodName, Class<?>... parameterTypes) {
+        List<Method> allMethod = getAllMethod(origin.getClass());
+        for (Method method : allMethod) {
+            if (method.getName().equals(methodName) && Arrays.equals(method.getParameterTypes(), parameterTypes)) {
+                method.setAccessible(true);
+                return method;
+            }
+        }
+        return null;
+    }
+
+    public static Object getFieldValue(Field field, Object origin) {
+        try {
+            field.setAccessible(true);
+            return field.get(origin);
+        } catch (IllegalAccessException e) {
+            e.printStackTrace();
+        }
+        return null;
+    }
+
+    public static Object invokeMethod(Method method, Object origin) {
+        try {
+            method.setAccessible(true);
+            return method.invoke(origin);
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return null;
+    }
+}

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


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


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


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


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


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


BIN
app/src/main/res/drawable-xxhdpi/icon_default_avatar.png


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


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


+ 5 - 0
app/src/main/res/drawable/bg_clone_card.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="12dp" />
+    <solid android:color="#32333C" />
+</shape>

+ 5 - 0
app/src/main/res/drawable/bg_clone_voice_delete.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="100dp" />
+    <solid android:color="#FF5656" />
+</shape>

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

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

+ 5 - 0
app/src/main/res/drawable/bg_recording_card.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="20dp" />
+    <solid android:color="@color/white" />
+</shape>

+ 7 - 0
app/src/main/res/drawable/bg_use_clone_voice.xml

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

+ 20 - 0
app/src/main/res/drawable/shape_preview_clone_audio_seekbar.xml

@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <!-- Background -->
+    <item android:id="@android:id/background">
+        <shape>
+            <solid android:color="#7C7D88" />
+            <size android:height="1dp" />
+        </shape>
+    </item>
+
+    <!-- Progress -->
+    <item android:id="@android:id/progress">
+        <clip>
+            <shape>
+                <solid android:color="@color/colorPrimaryVariant" />
+            </shape>
+        </clip>
+    </item>
+</layer-list>

+ 123 - 0
app/src/main/res/layout/activity_voice_clone.xml

@@ -0,0 +1,123 @@
+<?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="voiceViewModel"
+            type="com.atmob.voiceai.module.cloning.VoiceCloningViewModel" />
+
+        <import type="com.atmob.common.ui.SizeUtil" />
+    </data>
+
+    <androidx.constraintlayout.widget.ConstraintLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:background="@color/colorPrimary">
+
+
+        <ImageView
+            android:layout_width="match_parent"
+            android:layout_height="0dp"
+            android:src="@drawable/bg_generating_background1"
+            app:layout_constraintDimensionRatio="1080:1722"
+            app:layout_constraintTop_toTopOf="parent" />
+
+        <ImageView
+            android:layout_width="match_parent"
+            android:layout_height="0dp"
+            android:src="@drawable/bg_generating_background2"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintDimensionRatio="1080:1515"
+            app:layout_constraintEnd_toEndOf="parent" />
+
+        <Space
+            android:id="@+id/v_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:74"
+            app:layout_constraintTop_toBottomOf="@+id/v_status_bar" />
+
+        <ImageView
+            android:id="@+id/iv_avatar"
+            radius="@{100}"
+            android:layout_width="0dp"
+            android:layout_height="0dp"
+            android:layout_marginStart="23dp"
+            android:src="@{@drawable/icon_default_avatar}"
+            app:layout_constraintDimensionRatio="1:1"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toBottomOf="@+id/space1"
+            app:layout_constraintWidth_percent="0.2" />
+
+        <TextView
+            android:id="@+id/tv_name"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="8dp"
+            android:text="@string/cloning_default_name"
+            android:textColor="@color/white"
+            android:textSize="20sp"
+            app:layout_constraintLeft_toLeftOf="@+id/iv_avatar"
+            app:layout_constraintTop_toBottomOf="@+id/iv_avatar" />
+
+        <Space
+            android:id="@+id/space2"
+            android:layout_width="match_parent"
+            android:layout_height="0dp"
+            app:layout_constraintDimensionRatio="360:32"
+            app:layout_constraintTop_toBottomOf="@id/tv_name" />
+
+        <TextView
+            android:id="@+id/tv_generating_hint"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@string/voice_generating_hint"
+            android:textColor="@color/white"
+            android:textSize="30sp"
+            android:textStyle="bold"
+            app:layout_constraintLeft_toLeftOf="@id/iv_avatar"
+            app:layout_constraintTop_toBottomOf="@+id/space2" />
+
+        <TextView
+            android:id="@+id/tv_generating_hint2"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="12dp"
+            android:text="@string/voice_generating_hint2"
+            android:textColor="@color/white80"
+            android:textSize="14sp"
+            app:layout_constraintLeft_toLeftOf="@id/iv_avatar"
+            app:layout_constraintTop_toBottomOf="@+id/tv_generating_hint" />
+
+
+        <com.tencent.qgame.animplayer.AnimView
+            android:id="@+id/anim_view"
+            android:layout_width="match_parent"
+            android:layout_height="0dp"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintDimensionRatio="360:200.5"
+            app:layout_constraintTop_toBottomOf="@+id/tv_generating_hint2"
+            app:layout_constraintVertical_bias="0.184390243902439" />
+
+        <TextView
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:gravity="center"
+            android:textColor="@color/white"
+            android:textSize="14sp"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintTop_toBottomOf="@+id/anim_view"
+            app:layout_constraintVertical_bias="0.5274825174825175"
+            tools:text="Generating ~ 15 sec" />
+
+    </androidx.constraintlayout.widget.ConstraintLayout>
+</layout>

+ 113 - 0
app/src/main/res/layout/dialog_clone_delete_sure.xml

@@ -0,0 +1,113 @@
+<?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="cancelClick"
+            type="android.view.View.OnClickListener" />
+
+        <variable
+            name="deleteClick"
+            type="android.view.View.OnClickListener" />
+    </data>
+
+    <androidx.constraintlayout.widget.ConstraintLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content">
+
+        <View
+            android:id="@+id/v_delete_bg"
+            android:layout_width="0dp"
+            android:layout_height="0dp"
+            android:background="@drawable/bg_delete_clone_voice_dialog"
+            app:layout_constraintBottom_toBottomOf="@+id/space_bottom"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toTopOf="@+id/space1"
+            app:layout_constraintWidth_percent="0.8333333333333333" />
+
+        <Space
+            android:id="@+id/space1"
+            android:layout_width="match_parent"
+            android:layout_height="0dp"
+            app:layout_constraintDimensionRatio="360:20"
+            app:layout_constraintTop_toTopOf="parent" />
+
+        <TextView
+            android:id="@+id/tv_title"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:gravity="center"
+            android:text="@string/dialog_delete_clone_voice_title"
+            android:textColor="@color/colorPrimary"
+            android:textSize="17sp"
+            android:textStyle="bold"
+            app:layout_constraintEnd_toEndOf="@+id/v_delete_bg"
+            app:layout_constraintStart_toStartOf="@+id/v_delete_bg"
+            app:layout_constraintTop_toBottomOf="@+id/space1"
+            app:layout_constraintWidth_percent="0.75" />
+
+        <TextView
+            android:id="@+id/tv_content"
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="8dp"
+            android:text="@string/dialog_delete_clone_voice_content"
+            android:textColor="#32333C"
+            android:textSize="14sp"
+            app:layout_constraintEnd_toEndOf="@+id/v_delete_bg"
+            app:layout_constraintStart_toStartOf="@+id/v_delete_bg"
+            app:layout_constraintTop_toBottomOf="@+id/tv_title"
+            app:layout_constraintWidth_percent="0.7" />
+
+        <Space
+            android:id="@+id/space2"
+            android:layout_width="match_parent"
+            android:layout_height="0dp"
+            app:layout_constraintDimensionRatio="360:16"
+            app:layout_constraintTop_toBottomOf="@+id/tv_content" />
+
+        <TextView
+            android:id="@+id/tv_cancel"
+            expandTouchSize="@{15}"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginEnd="12dp"
+            android:gravity="center"
+            android:onClick="@{cancelClick}"
+            android:text="@string/dialog_delete_clone_voice_cancel"
+            android:textColor="#A3A3A8"
+            android:textSize="15sp"
+            android:textStyle="bold"
+            app:layout_constraintBottom_toBottomOf="@+id/tv_delete"
+            app:layout_constraintEnd_toStartOf="@+id/tv_delete"
+            app:layout_constraintStart_toStartOf="@+id/tv_content"
+            app:layout_constraintTop_toTopOf="@+id/tv_delete" />
+
+        <TextView
+            android:id="@+id/tv_delete"
+            android:layout_width="0dp"
+            android:layout_height="0dp"
+            android:background="@drawable/bg_clone_voice_delete"
+            android:gravity="center"
+            android:onClick="@{deleteClick}"
+            android:text="@string/dialog_delete_clone_voice_delete"
+            android:textColor="@color/colorPrimary"
+            android:textSize="15sp"
+            android:textStyle="bold"
+            app:layout_constraintDimensionRatio="120:36"
+            app:layout_constraintEnd_toEndOf="@+id/tv_content"
+            app:layout_constraintTop_toBottomOf="@+id/space2"
+            app:layout_constraintWidth_percent="0.3333333333333333" />
+
+        <Space
+            android:id="@+id/space_bottom"
+            android:layout_width="match_parent"
+            android:layout_height="0dp"
+            app:layout_constraintDimensionRatio="360:20"
+            app:layout_constraintTop_toBottomOf="@+id/tv_delete" />
+
+    </androidx.constraintlayout.widget.ConstraintLayout>
+</layout>

+ 151 - 0
app/src/main/res/layout/dialog_recording_instructions.xml

@@ -0,0 +1,151 @@
+<?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="onCloseClick"
+            type="android.view.View.OnClickListener" />
+
+        <variable
+            name="doNotShowAgainClick"
+            type="android.view.View.OnClickListener" />
+
+        <variable
+            name="knowClick"
+            type="android.view.View.OnClickListener" />
+    </data>
+
+    <androidx.constraintlayout.widget.ConstraintLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content">
+
+        <ImageView
+            android:id="@+id/iv_close"
+            android:layout_width="0dp"
+            android:layout_height="0dp"
+            android:layout_marginTop="@dimen/app_common_page_horizontal_padding"
+            android:layout_marginEnd="@dimen/app_common_page_horizontal_padding"
+            android:onClick="@{onCloseClick}"
+            android:src="@drawable/icon_recording_close"
+            app:layout_constraintDimensionRatio="1:1"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintTop_toTopOf="parent"
+            app:layout_constraintWidth_percent="0.0666666666666667" />
+
+        <View
+            android:id="@+id/v_recording_bg"
+            android:layout_width="match_parent"
+            android:layout_height="0dp"
+            android:layout_marginHorizontal="@dimen/app_common_page_horizontal_padding"
+            android:layout_marginTop="8dp"
+            android:background="@drawable/bg_recording_card"
+            app:layout_constraintDimensionRatio="336:408"
+            app:layout_constraintTop_toBottomOf="@+id/iv_close" />
+
+        <ImageView
+            android:id="@+id/iv_recording_header"
+            android:layout_width="0dp"
+            android:layout_height="0dp"
+            android:layout_marginTop="@dimen/app_common_page_horizontal_padding"
+            android:src="@drawable/iv_recording_header"
+            app:layout_constraintDimensionRatio="312:162"
+            app:layout_constraintEnd_toEndOf="@+id/v_recording_bg"
+            app:layout_constraintStart_toStartOf="@+id/v_recording_bg"
+            app:layout_constraintTop_toTopOf="@+id/v_recording_bg"
+            app:layout_constraintWidth_percent="0.8666666666666667" />
+
+        <Space
+            android:id="@+id/space1"
+            android:layout_width="match_parent"
+            android:layout_height="0dp"
+            app:layout_constraintDimensionRatio="360:24"
+            app:layout_constraintTop_toBottomOf="@+id/iv_recording_header" />
+
+        <TextView
+            android:id="@+id/tv_recording_title"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@string/dialog_recording_title"
+            android:textColor="@color/colorPrimary"
+            android:textSize="20sp"
+            android:textStyle="bold"
+            app:layout_constraintEnd_toEndOf="@id/v_recording_bg"
+            app:layout_constraintStart_toStartOf="@id/v_recording_bg"
+            app:layout_constraintTop_toBottomOf="@+id/space1" />
+
+        <TextView
+            android:id="@+id/tv_recording_desc"
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:layout_marginHorizontal="@dimen/app_common_page_horizontal_padding"
+            android:layout_marginTop="8dp"
+            android:gravity="center"
+            android:text="@string/dialog_recording_desc"
+            android:textColor="#32333C"
+            android:textSize="14sp"
+            app:layout_constraintEnd_toEndOf="@+id/v_recording_bg"
+            app:layout_constraintStart_toStartOf="@id/v_recording_bg"
+            app:layout_constraintTop_toBottomOf="@+id/tv_recording_title" />
+
+        <Space
+            android:id="@+id/space2"
+            android:layout_width="match_parent"
+            android:layout_height="0dp"
+            app:layout_constraintDimensionRatio="360:16"
+            app:layout_constraintTop_toBottomOf="@+id/tv_recording_desc" />
+
+        <ImageView
+            android:id="@+id/iv_checkbox"
+            android:layout_width="0dp"
+            android:layout_height="0dp"
+            android:onClick="@{doNotShowAgainClick}"
+            app:layout_constraintDimensionRatio="1:1"
+            app:layout_constraintHorizontal_chainStyle="packed"
+            app:layout_constraintLeft_toLeftOf="@id/v_recording_bg"
+            app:layout_constraintRight_toLeftOf="@+id/tv_checkbox_text"
+            app:layout_constraintTop_toBottomOf="@+id/space2"
+            app:layout_constraintWidth_percent="0.05"
+            tools:src="@drawable/icon_checkbox_no_select" />
+
+        <TextView
+            android:id="@+id/tv_checkbox_text"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginStart="10dp"
+            android:onClick="@{doNotShowAgainClick}"
+            android:text="@string/dialog_recording_checkbox_text"
+            android:textColor="#32333C"
+            android:textSize="14sp"
+            app:layout_constraintBottom_toBottomOf="@+id/iv_checkbox"
+            app:layout_constraintLeft_toRightOf="@+id/iv_checkbox"
+            app:layout_constraintRight_toRightOf="@+id/v_recording_bg"
+            app:layout_constraintTop_toTopOf="@+id/iv_checkbox" />
+
+        <TextView
+            android:layout_width="0dp"
+            android:textSize="17sp"
+            android:textColor="@color/colorPrimary"
+            android:textStyle="bold"
+            android:onClick="@{knowClick}"
+            android:gravity="center"
+            android:text="@string/dialog_recording_know"
+            android:background="@drawable/bg_voice_ai_btn"
+            app:layout_constraintVertical_bias="0.9416666666666667"
+            app:layout_constraintWidth_percent="0.6666666666666667"
+            android:layout_height="0dp"
+            app:layout_constraintBottom_toBottomOf="@id/v_recording_bg"
+            app:layout_constraintDimensionRatio="240:48"
+            app:layout_constraintEnd_toEndOf="@id/v_recording_bg"
+            app:layout_constraintStart_toStartOf="@id/v_recording_bg"
+            app:layout_constraintTop_toTopOf="@id/v_recording_bg" />
+
+        <Space
+            android:layout_width="match_parent"
+            android:layout_height="@dimen/app_common_page_horizontal_padding"
+            app:layout_constraintTop_toBottomOf="@+id/v_recording_bg" />
+
+    </androidx.constraintlayout.widget.ConstraintLayout>
+</layout>

+ 400 - 0
app/src/main/res/layout/fragment_clone_voice.xml

@@ -9,6 +9,9 @@
             name="cloneVoiceViewModel"
             type="com.atmob.voiceai.module.clonevoice.CloneVoiceViewModel" />
 
+        <import type="com.atmob.voiceai.module.clonevoice.CloneState" />
+
+        <import type="com.atmob.voiceai.utils.DateUtil" />
 
         <import type="com.atmob.common.ui.SizeUtil" />
     </data>
@@ -99,6 +102,7 @@
 
                 <TextView
                     android:id="@+id/tv_clone_hint2"
+                    isGone="@{cloneVoiceViewModel.cloneState == CloneState.CLONE_SUCCESS}"
                     android:layout_width="match_parent"
                     android:layout_height="wrap_content"
                     android:layout_marginHorizontal="@dimen/clone_voice_padding"
@@ -109,11 +113,407 @@
 
                 <Space
                     android:id="@+id/space4"
+                    isGone="@{cloneVoiceViewModel.cloneState == CloneState.CLONE_SUCCESS}"
                     android:layout_width="match_parent"
                     android:layout_height="0dp"
                     app:layout_constraintDimensionRatio="360:12"
                     app:layout_constraintTop_toBottomOf="@+id/tv_clone_hint2" />
 
+                <androidx.constraintlayout.widget.ConstraintLayout
+                    isGone="@{cloneVoiceViewModel.cloneState != CloneState.NO_UPLOAD_DATA}"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:paddingBottom="@dimen/clone_voice_padding"
+                    app:layout_constraintTop_toBottomOf="@+id/space4"
+                    tools:visibility="gone">
+
+                    <View
+                        android:id="@+id/v_clone_upload"
+                        android:layout_width="match_parent"
+                        android:layout_height="0dp"
+                        android:layout_marginHorizontal="@dimen/clone_voice_padding"
+                        android:background="@drawable/bg_clone_card"
+                        android:onClick="@{()-> cloneVoiceViewModel.onLocalUploadClick()}"
+                        app:layout_constraintDimensionRatio="312:80"
+                        app:layout_constraintTop_toTopOf="parent" />
+
+                    <ImageView
+                        android:id="@+id/iv_clone_upload"
+                        android:layout_width="0dp"
+                        android:layout_height="0dp"
+                        android:layout_marginStart="16dp"
+                        android:src="@drawable/icon_clone_upload"
+                        app:layout_constraintBottom_toBottomOf="@+id/v_clone_upload"
+                        app:layout_constraintDimensionRatio="1:1"
+                        app:layout_constraintStart_toStartOf="@+id/v_clone_upload"
+                        app:layout_constraintTop_toTopOf="@+id/v_clone_upload"
+                        app:layout_constraintWidth_percent="0.1" />
+
+                    <TextView
+                        android:id="@+id/tv_clone_upload"
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:layout_marginStart="10dp"
+                        android:text="@string/clone_voice_upload"
+                        android:textColor="@color/white"
+                        android:textSize="17sp"
+                        android:textStyle="bold"
+                        app:layout_constraintBottom_toTopOf="@+id/tv_clone_upload_tips"
+                        app:layout_constraintStart_toEndOf="@+id/iv_clone_upload"
+                        app:layout_constraintTop_toTopOf="@+id/iv_clone_upload"
+                        app:layout_constraintVertical_chainStyle="packed" />
+
+                    <TextView
+                        android:id="@+id/tv_clone_upload_tips"
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:layout_marginTop="4dp"
+                        android:text="@string/clone_voice_upload_tips"
+                        android:textColor="@color/white80"
+                        app:layout_constraintBottom_toBottomOf="@+id/iv_clone_upload"
+                        app:layout_constraintStart_toStartOf="@+id/tv_clone_upload"
+                        app:layout_constraintTop_toBottomOf="@+id/tv_clone_upload" />
+
+                    <ImageView
+                        android:layout_width="0dp"
+                        android:layout_height="0dp"
+                        android:layout_marginEnd="16dp"
+                        android:src="@drawable/icon_clone_right_arrow"
+                        app:layout_constraintBottom_toBottomOf="@+id/v_clone_upload"
+                        app:layout_constraintDimensionRatio="1:1"
+                        app:layout_constraintEnd_toEndOf="@+id/v_clone_upload"
+                        app:layout_constraintTop_toTopOf="@+id/v_clone_upload"
+                        app:layout_constraintWidth_percent="0.1" />
+
+                    <View
+                        android:onClick="@{()-> cloneVoiceViewModel.onRecordClick()}"
+                        android:id="@+id/v_clone_record"
+                        android:layout_width="match_parent"
+                        android:layout_height="0dp"
+                        android:layout_marginHorizontal="@dimen/clone_voice_padding"
+                        android:layout_marginTop="8dp"
+                        android:background="@drawable/bg_clone_card"
+                        app:layout_constraintDimensionRatio="312:80"
+                        app:layout_constraintTop_toBottomOf="@+id/v_clone_upload" />
+
+                    <ImageView
+                        android:id="@+id/iv_clone_record"
+                        android:layout_width="0dp"
+                        android:layout_height="0dp"
+                        android:layout_marginStart="16dp"
+                        android:src="@drawable/icon_clone_record"
+                        app:layout_constraintBottom_toBottomOf="@+id/v_clone_record"
+                        app:layout_constraintDimensionRatio="1:1"
+                        app:layout_constraintStart_toStartOf="@+id/v_clone_record"
+                        app:layout_constraintTop_toTopOf="@+id/v_clone_record"
+                        app:layout_constraintWidth_percent="0.1" />
+
+                    <TextView
+                        android:id="@+id/tv_clone_record"
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:layout_marginStart="10dp"
+                        android:text="@string/clone_voice_record"
+                        android:textColor="@color/white"
+                        android:textSize="17sp"
+                        android:textStyle="bold"
+                        app:layout_constraintBottom_toTopOf="@+id/tv_clone_record_tips"
+                        app:layout_constraintStart_toEndOf="@+id/iv_clone_record"
+                        app:layout_constraintTop_toTopOf="@+id/iv_clone_record"
+                        app:layout_constraintVertical_chainStyle="packed" />
+
+                    <TextView
+                        android:id="@+id/tv_clone_record_tips"
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:layout_marginTop="4dp"
+                        android:text="@string/clone_voice_record_tips"
+                        android:textColor="@color/white80"
+                        app:layout_constraintBottom_toBottomOf="@+id/iv_clone_record"
+                        app:layout_constraintStart_toStartOf="@+id/tv_clone_record"
+                        app:layout_constraintTop_toBottomOf="@+id/tv_clone_record" />
+
+                    <ImageView
+                        android:layout_width="0dp"
+                        android:layout_height="0dp"
+                        android:layout_marginEnd="16dp"
+                        android:src="@drawable/icon_clone_right_arrow"
+                        app:layout_constraintBottom_toBottomOf="@+id/v_clone_record"
+                        app:layout_constraintDimensionRatio="1:1"
+                        app:layout_constraintEnd_toEndOf="@+id/v_clone_record"
+                        app:layout_constraintTop_toTopOf="@+id/v_clone_record"
+                        app:layout_constraintWidth_percent="0.1" />
+
+                </androidx.constraintlayout.widget.ConstraintLayout>
+
+
+                <androidx.constraintlayout.widget.ConstraintLayout
+                    isGone="@{cloneVoiceViewModel.cloneState != CloneState.UPLOAD_CHOICE_LOCAL &amp;&amp; cloneVoiceViewModel.cloneState != CloneState.UPLOAD_CHOICE_RECORDING}"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:paddingBottom="@dimen/clone_voice_padding"
+                    app:layout_constraintTop_toBottomOf="@+id/space4"
+                    tools:visibility="visible">
+
+                    <View
+                        android:id="@+id/v_clone_upload_preview"
+                        android:layout_width="match_parent"
+                        android:layout_height="0dp"
+                        android:layout_marginHorizontal="@dimen/clone_voice_padding"
+                        android:background="@drawable/bg_clone_card"
+                        app:layout_constraintDimensionRatio="312:298"
+                        app:layout_constraintTop_toTopOf="parent" />
+
+                    <TextView
+                        android:id="@+id/tv_clone_upload_preview"
+                        android:layout_width="0dp"
+                        android:layout_height="wrap_content"
+                        android:layout_marginHorizontal="@dimen/clone_voice_padding"
+                        android:ellipsize="middle"
+                        android:gravity="center"
+                        android:singleLine="true"
+                        android:text="@{cloneVoiceViewModel.uploadPreviewName}"
+                        android:textColor="@color/white"
+                        android:textSize="18sp"
+                        android:textStyle="bold"
+                        app:layout_constraintBottom_toBottomOf="@+id/v_clone_upload_preview"
+                        app:layout_constraintEnd_toEndOf="@+id/v_clone_upload_preview"
+                        app:layout_constraintStart_toStartOf="@+id/v_clone_upload_preview"
+                        app:layout_constraintTop_toTopOf="@+id/v_clone_upload_preview"
+                        app:layout_constraintVertical_bias="0.1083032490974729"
+                        tools:text="Love_story.MP3" />
+
+                    <TextView
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:layout_marginTop="4dp"
+                        android:text="@{cloneVoiceViewModel.uploadPreviewSize}"
+                        android:textColor="@color/white50"
+                        android:textSize="14sp"
+                        app:layout_constraintEnd_toEndOf="@+id/tv_clone_upload_preview"
+                        app:layout_constraintStart_toStartOf="@+id/tv_clone_upload_preview"
+                        app:layout_constraintTop_toBottomOf="@+id/tv_clone_upload_preview"
+                        tools:text="2.5MB" />
+
+                    <SeekBar
+                        android:id="@+id/voice_seek_bar"
+                        android:layout_width="0dp"
+                        android:layout_height="wrap_content"
+                        android:layout_marginHorizontal="10dp"
+                        android:max="@{(int)cloneVoiceViewModel.totalDuration}"
+                        android:maxHeight="2dp"
+                        android:minHeight="2dp"
+                        android:paddingStart="6dp"
+                        android:paddingEnd="6dp"
+                        android:progress="@{(int)cloneVoiceViewModel.currentDuration}"
+                        android:progressDrawable="@drawable/shape_preview_clone_audio_seekbar"
+                        android:splitTrack="false"
+                        android:thumb="@drawable/shape_preview_audio_seekbar_thumb"
+                        app:layout_constraintBottom_toBottomOf="@+id/v_clone_upload_preview"
+                        app:layout_constraintEnd_toEndOf="@+id/v_clone_upload_preview"
+                        app:layout_constraintStart_toStartOf="@+id/v_clone_upload_preview"
+                        app:layout_constraintTop_toTopOf="@+id/v_clone_upload_preview"
+                        app:layout_constraintVertical_bias="0.3146853146853147"
+                        tools:progress="30" />
+
+                    <TextView
+                        android:id="@+id/tv_current_time"
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:layout_marginStart="6dp"
+                        android:layout_marginTop="3dp"
+                        android:text="@{DateUtil.formatDuration(cloneVoiceViewModel.currentDuration)}"
+                        android:textColor="@color/white50"
+                        android:textSize="12sp"
+                        app:layout_constraintLeft_toLeftOf="@+id/voice_seek_bar"
+                        app:layout_constraintTop_toBottomOf="@+id/voice_seek_bar"
+                        tools:text="00:53" />
+
+                    <TextView
+                        android:id="@+id/tv_total_time"
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:layout_marginEnd="6dp"
+                        android:text="@{DateUtil.formatDuration(cloneVoiceViewModel.totalDuration)}"
+                        android:textColor="@color/white50"
+                        android:textSize="12sp"
+                        app:layout_constraintEnd_toEndOf="@id/voice_seek_bar"
+                        app:layout_constraintTop_toBottomOf="@+id/voice_seek_bar"
+                        app:layout_constraintTop_toTopOf="@+id/tv_current_time"
+                        tools:text="02:53" />
+
+
+                    <ImageView
+                        android:id="@+id/iv_voice_reduce"
+                        android:layout_width="0dp"
+                        android:layout_height="0dp"
+                        android:src="@drawable/icon_voice_reduce"
+                        app:layout_constraintBottom_toBottomOf="@+id/iv_play"
+                        app:layout_constraintDimensionRatio="1:1"
+                        app:layout_constraintEnd_toEndOf="@+id/v_clone_upload_preview"
+                        app:layout_constraintHorizontal_bias="0.1739130434782609"
+                        app:layout_constraintStart_toStartOf="@+id/v_clone_upload_preview"
+                        app:layout_constraintTop_toTopOf="@+id/iv_play" />
+
+                    <ImageView
+                        android:id="@+id/iv_play"
+                        android:layout_width="0dp"
+                        android:layout_height="0dp"
+                        android:src="@{cloneVoiceViewModel.isPlay ? @drawable/icon_voice_playing: @drawable/icon_voice_suspend}"
+                        app:layout_constraintBottom_toBottomOf="@+id/v_clone_upload_preview"
+                        app:layout_constraintDimensionRatio="1:1"
+                        app:layout_constraintEnd_toEndOf="@+id/v_clone_upload_preview"
+                        app:layout_constraintStart_toStartOf="@+id/v_clone_upload_preview"
+                        app:layout_constraintTop_toTopOf="@+id/v_clone_upload_preview"
+                        app:layout_constraintVertical_bias="0.4961832061068702"
+                        app:layout_constraintWidth_percent="0.1"
+                        tools:src="@drawable/icon_voice_playing" />
+
+                    <ImageView
+                        android:id="@+id/iv_voice_speed"
+                        android:layout_width="0dp"
+                        android:layout_height="0dp"
+                        android:src="@drawable/icon_voice_speed_up"
+                        app:layout_constraintBottom_toBottomOf="@+id/iv_play"
+                        app:layout_constraintDimensionRatio="1:1"
+                        app:layout_constraintEnd_toEndOf="@+id/v_clone_upload_preview"
+                        app:layout_constraintHorizontal_bias="0.8260869565217391"
+                        app:layout_constraintStart_toStartOf="@+id/v_clone_upload_preview"
+                        app:layout_constraintTop_toTopOf="@+id/iv_play" />
+
+                    <TextView
+                        android:id="@+id/tv_voice_speed"
+                        android:layout_width="0dp"
+                        android:layout_height="0dp"
+                        android:background="@drawable/bg_voice_ai_btn"
+                        android:gravity="center"
+                        android:onClick="@{()-> cloneVoiceViewModel.onCloneVoiceClick(cloneVoiceViewModel.cloneState)}"
+                        android:text="@string/clone_voice_btn_txt"
+                        android:textColor="@color/colorPrimary"
+                        android:textSize="17sp"
+                        android:textStyle="bold"
+                        app:layout_constraintBottom_toBottomOf="@+id/v_clone_upload_preview"
+                        app:layout_constraintDimensionRatio="256:48"
+                        app:layout_constraintEnd_toEndOf="@+id/v_clone_upload_preview"
+                        app:layout_constraintStart_toStartOf="@+id/v_clone_upload_preview"
+                        app:layout_constraintTop_toTopOf="@+id/v_clone_upload_preview"
+                        app:layout_constraintVertical_bias="0.808"
+                        app:layout_constraintWidth_percent="0.7111111111111111" />
+
+                    <TextView
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:onClick="@{()-> cloneVoiceViewModel.cloneState == CloneState.UPLOAD_CHOICE_LOCAL ? cloneVoiceViewModel.onLocalReSelectClick() : cloneVoiceViewModel.onReRecordClick()}"
+                        android:text="@{cloneVoiceViewModel.cloneState == CloneState.UPLOAD_CHOICE_LOCAL ?  @string/clone_voice_reselect: @string/clone_voice_start_over }"
+                        android:textColor="@color/colorPrimaryVariant"
+                        android:textSize="17sp"
+                        app:layout_constraintBottom_toBottomOf="@+id/v_clone_upload_preview"
+                        app:layout_constraintEnd_toEndOf="@+id/v_clone_upload_preview"
+                        app:layout_constraintStart_toStartOf="@+id/v_clone_upload_preview"
+                        app:layout_constraintTop_toTopOf="@+id/v_clone_upload_preview"
+                        app:layout_constraintVertical_bias="0.960431654676259"
+                        tools:text="Reselect" />
+
+
+                </androidx.constraintlayout.widget.ConstraintLayout>
+
+                <androidx.constraintlayout.widget.ConstraintLayout
+                    tools:visibility="gone"
+                    isGone="@{cloneVoiceViewModel.cloneState != CloneState.CLONE_SUCCESS}"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    app:layout_constraintTop_toBottomOf="@+id/space4">
+
+                    <View
+                        android:id="@+id/v_clone_success_preview"
+                        android:layout_width="match_parent"
+                        android:layout_height="0dp"
+                        android:layout_marginHorizontal="@dimen/clone_voice_padding"
+                        android:background="@drawable/bg_clone_card"
+                        app:layout_constraintDimensionRatio="312:154"
+                        app:layout_constraintTop_toTopOf="parent" />
+
+                    <ImageView
+                        android:id="@+id/iv_clone_success"
+                        android:layout_width="0dp"
+                        android:layout_height="0dp"
+                        android:src="@drawable/icon_clone_success"
+                        app:layout_constraintBottom_toBottomOf="@+id/v_clone_success_preview"
+                        app:layout_constraintDimensionRatio="1:1"
+                        app:layout_constraintEnd_toEndOf="@id/v_clone_success_preview"
+                        app:layout_constraintHorizontal_bias="0.072463768115942"
+                        app:layout_constraintStart_toStartOf="@+id/v_clone_success_preview"
+                        app:layout_constraintTop_toTopOf="@+id/v_clone_success_preview"
+                        app:layout_constraintVertical_bias="0.2542372881355932"
+                        app:layout_constraintWidth_percent="0.1" />
+
+                    <TextView
+                        android:id="@+id/tv_clone_success_title"
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:layout_marginStart="12dp"
+                        android:text="@string/clone_voice_success_title"
+                        android:textColor="@color/white"
+                        android:textSize="17sp"
+                        android:textStyle="bold"
+                        app:layout_constraintBottom_toTopOf="@+id/tv_clone_success_hint"
+                        app:layout_constraintStart_toEndOf="@+id/iv_clone_success"
+                        app:layout_constraintTop_toTopOf="@+id/iv_clone_success"
+                        app:layout_constraintVertical_chainStyle="packed" />
+
+                    <TextView
+                        android:id="@+id/tv_clone_success_hint"
+                        android:layout_width="0dp"
+                        android:layout_height="wrap_content"
+                        android:layout_marginEnd="20dp"
+                        android:text="@string/clone_voice_success_desc"
+                        android:textColor="@color/white80"
+                        android:textSize="12sp"
+                        app:layout_constraintBottom_toBottomOf="@id/iv_clone_success"
+                        app:layout_constraintEnd_toEndOf="@+id/v_clone_success_preview"
+                        app:layout_constraintStart_toStartOf="@+id/tv_clone_success_title"
+                        app:layout_constraintTop_toBottomOf="@+id/tv_clone_success_title" />
+
+                    <TextView
+                        android:onClick="@{()->cloneVoiceViewModel.useCloneVoiceClick()}"
+                        android:id="@+id/tv_clone_used"
+                        android:layout_width="0dp"
+                        android:layout_height="0dp"
+                        android:background="@drawable/bg_use_clone_voice"
+                        android:gravity="center"
+                        android:text="@string/clone_voice_used_txt"
+                        android:textColor="@color/white"
+                        android:textSize="15sp"
+                        android:textStyle="bold"
+                        app:layout_constraintBottom_toBottomOf="@+id/v_clone_success_preview"
+                        app:layout_constraintDimensionRatio="162:36"
+                        app:layout_constraintHorizontal_chainStyle="packed"
+                        app:layout_constraintLeft_toLeftOf="@+id/v_clone_success_preview"
+                        app:layout_constraintRight_toLeftOf="@+id/tv_clone_delete"
+                        app:layout_constraintTop_toTopOf="@+id/v_clone_success_preview"
+                        app:layout_constraintVertical_bias="0.8305084745762712"
+                        app:layout_constraintWidth_percent="0.45" />
+
+                    <TextView
+                        android:onClick="@{()-> cloneVoiceViewModel.onDeleteClick()}"
+                        android:text="@string/clone_voice_delete_txt"
+                        android:id="@+id/tv_clone_delete"
+                        android:layout_width="0dp"
+                        android:gravity="center"
+                        android:textStyle="bold"
+                        android:textSize="15sp"
+                        android:background="@drawable/bg_clone_voice_delete"
+                        android:textColor="@color/colorPrimary"
+                        android:layout_height="0dp"
+                        android:layout_marginStart="12dp"
+                        app:layout_constraintBottom_toBottomOf="@+id/tv_clone_used"
+                        app:layout_constraintDimensionRatio="98:36"
+                        app:layout_constraintLeft_toRightOf="@+id/tv_clone_used"
+                        app:layout_constraintRight_toRightOf="@+id/v_clone_success_preview"
+                        app:layout_constraintTop_toTopOf="@+id/tv_clone_used"
+                        app:layout_constraintWidth_percent="0.2722222222222222" />
+
+                </androidx.constraintlayout.widget.ConstraintLayout>
 
             </androidx.constraintlayout.widget.ConstraintLayout>
 

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

@@ -41,4 +41,27 @@
     <string name="clone_voice_tip_create">CREATE</string>
     <string name="clone_voice_hint">Record or upload an audio file to create AI model with your own voice</string>
     <string name="clone_voice_hint2">#Clearing speech without background noise produces the best results.</string>
+    <string name="clone_voice_upload">Upload file</string>
+    <string name="clone_voice_upload_tips">20-30 seconds, Max 3 MB</string>
+    <string name="clone_voice_record">Record</string>
+    <string name="clone_voice_record_tips">speaking to reach the minmum \nrecording length</string>
+    <string name="clone_voice_btn_txt">Clone Voice</string>
+    <string name="clone_voice_duration_error">The file duration must be 20-30 seconds</string>
+    <string name="clone_voice_size_error">The file size cannot exceed 3MB</string>
+    <string name="clone_voice_reselect">Reselect</string>
+    <string name="clone_voice_start_over">Start Over</string>
+    <string name="cloning_default_name">Your clone</string>
+    <string name="clone_voice_success_title">successfully cloned</string>
+    <string name="clone_voice_success_desc">You have completed the cloning of the sound, use it now!</string>
+    <string name="clone_voice_used_txt">Use cloned voice</string>
+    <string name="clone_voice_delete_txt">Delete</string>
+    <string name="dialog_delete_clone_voice_title">Are you sure to delete it?</string>
+    <string name="dialog_delete_clone_voice_content">If you are not satisfied with the cloning effect, please delete it, otherwise please keep it.</string>
+    <string name="dialog_delete_clone_voice_cancel">Cancle</string>
+    <string name="dialog_delete_clone_voice_delete">Delete</string>
+    <string name="no_clone_data">No Data!</string>
+    <string name="dialog_recording_title">Recording instructions</string>
+    <string name="dialog_recording_desc">Please be sure to speak when recording, otherwise the cloned voice will not be generated.</string>
+    <string name="dialog_recording_checkbox_text">Do not show again</string>
+    <string name="dialog_recording_know">I Know</string>
 </resources>

+ 11 - 0
app/src/main/res/values/style.xml

@@ -35,6 +35,17 @@
         <item name="android:windowAnimationStyle">@style/Animation.CommonDialog</item>
     </style>
 
+    <style name="Theme.Common.BottomSheetDialog" parent="Theme.MaterialComponents.Light.BottomSheetDialog">
+        <!-- Primary brand color. -->
+        <item name="colorPrimary">@color/colorPrimary</item>
+        <item name="colorPrimaryVariant">@color/colorPrimaryVariant</item>
+        <item name="colorOnPrimary">@color/white</item>
+
+        <!-- Customize your theme here. -->
+        <item name="android:windowFullscreen">true</item>
+        <item name="android:windowBackground">@android:color/transparent</item>
+        <item name="android:backgroundDimAmount">0.7</item>
+    </style>
 
     <style name="Animation.CommonDialog" parent="Animation.AppCompat.Dialog">
         <item name="android:windowEnterAnimation">@anim/anim_alpha_in</item>