From a5e70956e523b0d5cc9813b00f512a77ac827728 Mon Sep 17 00:00:00 2001 From: Junyu Long <877730493@qq.com> Date: Mon, 12 Aug 2024 00:03:15 +0800 Subject: [PATCH] contents: Continue to work on the content manager. --- .../java/com/winlator/ContentsFragment.java | 156 ++++++++++++++++++ .../main/java/com/winlator/MainActivity.java | 3 + .../winlator/contentdialog/ContentDialog.java | 15 ++ .../contentdialog/ContentInfoDialog.java | 31 ++++ .../com/winlator/contents/ContentProfile.java | 40 ++++- .../winlator/contents/ContentsManager.java | 138 +++++++++++++--- .../main/res/layout/content_info_dialog.xml | 106 ++++++++++++ app/src/main/res/layout/contents_fragment.xml | 66 ++++++++ app/src/main/res/menu/main_menu.xml | 1 + app/src/main/res/values-zh-rCN/strings.xml | 19 +++ app/src/main/res/values/strings.xml | 19 +++ 11 files changed, 561 insertions(+), 33 deletions(-) create mode 100644 app/src/main/java/com/winlator/ContentsFragment.java create mode 100644 app/src/main/java/com/winlator/contentdialog/ContentInfoDialog.java create mode 100644 app/src/main/res/layout/content_info_dialog.xml create mode 100644 app/src/main/res/layout/contents_fragment.xml diff --git a/app/src/main/java/com/winlator/ContentsFragment.java b/app/src/main/java/com/winlator/ContentsFragment.java new file mode 100644 index 00000000..52e80042 --- /dev/null +++ b/app/src/main/java/com/winlator/ContentsFragment.java @@ -0,0 +1,156 @@ +package com.winlator; + +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.Spinner; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; +import androidx.fragment.app.Fragment; +import androidx.recyclerview.widget.RecyclerView; + +import com.winlator.contentdialog.ContentDialog; +import com.winlator.contentdialog.ContentInfoDialog; +import com.winlator.contents.ContentProfile; +import com.winlator.contents.ContentsManager; +import com.winlator.core.AppUtils; +import com.winlator.core.PreloaderDialog; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Executors; + +public class ContentsFragment extends Fragment { + private RecyclerView recyclerView; + private View emptyText; + private ContentsManager manager; + private ContentProfile.ContentType currentContentType = ContentProfile.ContentType.CONTENT_TYPE_WINE; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setHasOptionsMenu(false); + manager = new ContentsManager(getContext()); + manager.syncContents(); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + ((AppCompatActivity) getActivity()).getSupportActionBar().setTitle(R.string.contents); + } + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + ViewGroup layout = (ViewGroup) inflater.inflate(R.layout.contents_fragment, container, false); + + Spinner sContentType = layout.findViewById(R.id.SContentType); + updateContentTypeSpinner(sContentType); + + recyclerView = layout.findViewById(R.id.RecyclerView); + emptyText = layout.findViewById(R.id.TVEmptyText); + + View btInstallContent = layout.findViewById(R.id.BTInstallContent); + btInstallContent.setOnClickListener(v -> { + ContentDialog.confirm(getContext(), getString(R.string.do_you_want_to_install_content) + " " + + getString(R.string.pls_make_sure_content_trustworthy) + " " + + getString(R.string.content_suffix_is_wcp_packed_xz_zst), () -> { + Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); + intent.addCategory(Intent.CATEGORY_OPENABLE); + intent.setType("*/*"); + getActivity().startActivityFromFragment(this, intent, MainActivity.OPEN_FILE_REQUEST_CODE); + }); + }); + + return layout; + } + + private void updateContentTypeSpinner(Spinner spinner) { + List typeList = new ArrayList<>(); + for (ContentProfile.ContentType type : ContentProfile.ContentType.values()) + typeList.add(type.toString()); + spinner.setAdapter(new ArrayAdapter<>(getContext(), android.R.layout.simple_spinner_dropdown_item, typeList)); + spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + currentContentType = ContentProfile.ContentType.values()[position]; + updateContentsListView(); + } + + @Override + public void onNothingSelected(AdapterView parent) { + + } + }); + } + + private void updateContentsListView() { + List profiles = manager.getProfiles(currentContentType); + if (profiles.isEmpty()) { + recyclerView.setVisibility(View.GONE); + emptyText.setVisibility(View.VISIBLE); + } + } + + @Override + public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { + if (requestCode == MainActivity.OPEN_FILE_REQUEST_CODE && resultCode == Activity.RESULT_OK) { + PreloaderDialog preloaderDialog = new PreloaderDialog(getActivity()); + preloaderDialog.showOnUiThread(R.string.installing_content); + try { + ContentsManager.OnInstallFinishedCallback callback = new ContentsManager.OnInstallFinishedCallback() { + private boolean isExtracting = true; + + @Override + public void onFailed(ContentsManager.InstallFailedReason reason, Exception e) { + int msgId = switch (reason) { + case ERROR_BADTAR -> R.string.file_cannot_be_recognied; + case ERROR_NOPROFILE -> R.string.profile_not_found_in_content; + case ERROR_BADPROFILE -> R.string.profile_cannot_be_recognized; + case ERROR_EXIST -> R.string.content_already_exist; + default -> R.string.unable_to_install_content; + }; + requireActivity().runOnUiThread(() -> ContentDialog.alert(getContext(), getString(R.string.install_failed) + ": " + + getString(msgId), preloaderDialog::closeOnUiThread)); + } + + @Override + public void onSucceed(ContentProfile profile) { + if (isExtracting) { + ContentsManager.OnInstallFinishedCallback callback1 = this; + requireActivity().runOnUiThread(() -> { + ContentInfoDialog dialog = new ContentInfoDialog(getContext(), profile); + ((TextView) dialog.findViewById(R.id.BTConfirm)).setText(R.string._continue); + dialog.setOnConfirmCallback(() -> { + isExtracting = false; + manager.finishInstallContent(profile, callback1); + // TODO + }); + dialog.show(); + }); + + } else { + preloaderDialog.closeOnUiThread(); + requireActivity().runOnUiThread(() -> ContentDialog.alert(getContext(), R.string.content_installed_success, null)); + } + } + }; + Executors.newSingleThreadExecutor().execute(() -> { + manager.extraContentFile(data.getData(), callback); + }); + } catch (Exception e) { + preloaderDialog.closeOnUiThread(); + AppUtils.showToast(getContext(), R.string.unable_to_import_profile); + } + } + } +} diff --git a/app/src/main/java/com/winlator/MainActivity.java b/app/src/main/java/com/winlator/MainActivity.java index e206138f..2d109c9f 100644 --- a/app/src/main/java/com/winlator/MainActivity.java +++ b/app/src/main/java/com/winlator/MainActivity.java @@ -164,6 +164,9 @@ public boolean onNavigationItemSelected(@NonNull MenuItem item) { case R.id.main_menu_box_rc: show(new Box86_64RCFragment()); break; + case R.id.main_menu_contents: + show(new ContentsFragment()); + break; case R.id.main_menu_settings: show(new SettingsFragment()); break; diff --git a/app/src/main/java/com/winlator/contentdialog/ContentDialog.java b/app/src/main/java/com/winlator/contentdialog/ContentDialog.java index 06ab42e6..260fc289 100644 --- a/app/src/main/java/com/winlator/contentdialog/ContentDialog.java +++ b/app/src/main/java/com/winlator/contentdialog/ContentDialog.java @@ -132,6 +132,14 @@ public static void alert(Context context, int msgResId, Runnable callback) { dialog.show(); } + public static void alert(Context context, String msg, Runnable callback) { + ContentDialog dialog = new ContentDialog(context); + dialog.setMessage(msg); + dialog.setOnConfirmCallback(callback); + dialog.findViewById(R.id.BTCancel).setVisibility(View.GONE); + dialog.show(); + } + public static void confirm(Context context, int msgResId, Runnable callback) { ContentDialog dialog = new ContentDialog(context); dialog.setMessage(msgResId); @@ -139,6 +147,13 @@ public static void confirm(Context context, int msgResId, Runnable callback) { dialog.show(); } + public static void confirm(Context context, String msg, Runnable callback) { + ContentDialog dialog = new ContentDialog(context); + dialog.setMessage(msg); + dialog.setOnConfirmCallback(callback); + dialog.show(); + } + public static void prompt(Context context, int titleResId, String defaultText, Callback callback) { ContentDialog dialog = new ContentDialog(context); diff --git a/app/src/main/java/com/winlator/contentdialog/ContentInfoDialog.java b/app/src/main/java/com/winlator/contentdialog/ContentInfoDialog.java new file mode 100644 index 00000000..474e77ed --- /dev/null +++ b/app/src/main/java/com/winlator/contentdialog/ContentInfoDialog.java @@ -0,0 +1,31 @@ +package com.winlator.contentdialog; + +import android.content.Context; +import android.widget.TextView; + +import com.winlator.R; +import com.winlator.contents.ContentProfile; + +public class ContentInfoDialog extends ContentDialog { + public ContentInfoDialog(Context context, ContentProfile profile) { + super(context, R.layout.content_info_dialog); + setIcon(R.drawable.icon_about); + setTitle(R.string.content_info); + + TextView tvType = findViewById(R.id.TVType); + TextView tvVersion = findViewById(R.id.TVVersion); + TextView tvVersionCode = findViewById(R.id.TVVersionCode); + TextView tvDescription = findViewById(R.id.TVDesc); + TextView tvFiles = findViewById(R.id.TVFiles); + + tvType.setText(profile.type.toString()); + tvVersion.setText(profile.verName); + tvVersionCode.setText(String.valueOf(profile.verCode)); + tvDescription.setText(profile.desc); + + StringBuilder stb = new StringBuilder(); + for (String str : profile.fileList) + stb.append(str).append('\n'); + tvFiles.setText(stb.toString()); + } +} diff --git a/app/src/main/java/com/winlator/contents/ContentProfile.java b/app/src/main/java/com/winlator/contents/ContentProfile.java index df48a630..7b359b80 100644 --- a/app/src/main/java/com/winlator/contents/ContentProfile.java +++ b/app/src/main/java/com/winlator/contents/ContentProfile.java @@ -1,15 +1,43 @@ package com.winlator.contents; +import androidx.annotation.NonNull; + import java.util.List; public class ContentProfile { - public static final String CONTENT_TYPE_WINE = "WINE"; - public static final String CONTENT_TYPE_TURNIP = "TURNIP"; - public static final String CONTENT_TYPE_VIRGL = "VIRGL"; - public static final String CONTENT_TYPE_DXVK = "DXVK"; - public static final String CONTENT_TYPE_VKD3D = "VKD3D"; + public static final String MARK_TYPE = "type"; + public static final String MARK_VERSION_NAME = "versionName"; + public static final String MARK_VERSION_CODE = "versionCode"; + public static final String MARK_DESC = "description"; + public static final String MARK_FILE_LIST = "files"; + + public enum ContentType { + CONTENT_TYPE_WINE ("Wine"), + CONTENT_TYPE_TURNIP ("Turnip"), + CONTENT_TYPE_VIRGL ("VirGL"), + CONTENT_TYPE_DXVK ("DXVK"), + CONTENT_TYPE_VKD3D ("VKD3D"); + + final String typeName; + ContentType(String typeNmae) { + this.typeName = typeNmae; + } + + @NonNull + @Override + public String toString() { + return typeName; + } + + public static ContentType getTypeByName(String name) { + for (ContentType type : ContentType.values()) + if (type.typeName.equals(name)) + return type; + return null; + } + } - public String type; + public ContentType type; public String verName; public int verCode; public String desc; diff --git a/app/src/main/java/com/winlator/contents/ContentsManager.java b/app/src/main/java/com/winlator/contents/ContentsManager.java index cc2fd2e7..27008c23 100644 --- a/app/src/main/java/com/winlator/contents/ContentsManager.java +++ b/app/src/main/java/com/winlator/contents/ContentsManager.java @@ -8,17 +8,25 @@ import com.winlator.core.FileUtils; import com.winlator.core.TarCompressorUtils; +import org.json.JSONArray; import org.json.JSONObject; import java.io.File; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; public class ContentsManager { + public static final String PROFILE_NAME = "profile.json"; + public enum InstallFailedReason { ERROR_NOSPACE, ERROR_BADTAR, ERROR_NOPROFILE, ERROR_BADPROFILE, ERROR_MISSINGFILES, + ERROR_EXIST, ERROR_UNKNOWN } @@ -43,7 +51,9 @@ public String toString() { } } - private Context context; + private final Context context; + + private HashMap> profilesMap; public ContentsManager(Context context) { this.context = context; @@ -55,26 +65,33 @@ public interface OnInstallFinishedCallback { void onSucceed(ContentProfile profile); } - public void syncContents(boolean clean) { - // 创建Profile列表 - // 按类型搜索目录 - // 找到目录后判断是否包含profile - // 若不包含则直接删除目录 - // 若包含在尝试读取 - // 读取失败直接删除目录 - // 读取后验证文件列表 - // 验证失败直接删除目录 - // 全部成功则放入Profile列表 + public void syncContents() { + profilesMap = new HashMap<>(); + for (ContentProfile.ContentType type : ContentProfile.ContentType.values()) { + LinkedList profiles = new LinkedList<>(); + profilesMap.put(type, profiles); + + File typeFile = getContentTypeDir(context, type); + File[] fileList = typeFile.listFiles(); + if (fileList == null) + continue; + + for (File file : fileList) { + File proFile = new File(file, PROFILE_NAME); + if (proFile.exists() && proFile.isFile()) { + ContentProfile profile = readProfile(proFile); + if (profile != null) + profiles.add(profile); + } + } + } } - public void installContentFile(Uri uri, OnInstallFinishedCallback callback) { - // 清理临时文件夹 - // 创建临时文件夹 - File file = new File(context.getFilesDir(), "tmp/" + ContentDirName.CONTENT_MAIN_DIR_NAME); - FileUtils.delete(file); + public void extraContentFile(Uri uri, OnInstallFinishedCallback callback) { + cleanTmpDir(context); + File file = getTmpDir(context); file.mkdirs(); - // 尝试解压文件 - // 若失败则回调 + boolean ret; ret = TarCompressorUtils.extract(TarCompressorUtils.Type.XZ, context, uri, file); if (!ret) @@ -83,21 +100,88 @@ public void installContentFile(Uri uri, OnInstallFinishedCallback callback) { callback.onFailed(InstallFailedReason.ERROR_BADTAR, null); return; } - // 成功则尝试读取 Profile - // 失败则回调 - // 尝试验证 Profile - // 失败则回调 - // 将解压的文件移动到指定位置 - // 执行成功回调 + + File proFile = new File(file, PROFILE_NAME); + if (!proFile.exists()) { + callback.onFailed(InstallFailedReason.ERROR_NOPROFILE, null); + return; + } + + ContentProfile profile = readProfile(proFile); + if (profile == null) + callback.onFailed(InstallFailedReason.ERROR_BADPROFILE, null); + else callback.onSucceed(profile); } - public ContentProfile loadProfile(File file) { - // TODO: + public void finishInstallContent(ContentProfile profile, OnInstallFinishedCallback callback) { + File installPath = getInstallDir(context, profile); + if (installPath.exists()) { + callback.onFailed(InstallFailedReason.ERROR_EXIST, null); + return; + } + + if (!installPath.mkdirs()) { + callback.onFailed(InstallFailedReason.ERROR_UNKNOWN, null); + return; + } + + if (!getTmpDir(context).renameTo(installPath)) { + callback.onFailed(InstallFailedReason.ERROR_UNKNOWN, null); + } + + callback.onSucceed(profile); + } + + public ContentProfile readProfile(File file) { + ContentProfile profile = null; try { JSONObject profileJSONObject = new JSONObject(FileUtils.readString(file)); + String typeName = profileJSONObject.getString(ContentProfile.MARK_TYPE); + String verName = profileJSONObject.getString(ContentProfile.MARK_VERSION_NAME); + int verCode = profileJSONObject.getInt(ContentProfile.MARK_VERSION_CODE); + String desc = profileJSONObject.getString(ContentProfile.MARK_DESC); + + JSONArray fileJSONArray = profileJSONObject.getJSONArray(ContentProfile.MARK_FILE_LIST); + List fileList = new ArrayList<>(); + for (int i = 0; i < fileJSONArray.length(); i++) + fileList.add(fileJSONArray.getString(i)); + + profile = new ContentProfile(); + profile.type = ContentProfile.ContentType.getTypeByName(typeName); + profile.verName = verName; + profile.verCode = verCode; + profile.desc = desc; + profile.fileList = fileList; } catch (Exception e) { - + e.printStackTrace(); } + return profile; + } + + public List getProfiles(ContentProfile.ContentType type) { + if (profilesMap != null) + return profilesMap.get(type); return null; } + + public static File getInstallDir(Context context, ContentProfile profile) { + return new File(getContentTypeDir(context, profile.type), profile.verName + "-" + profile.verCode); + } + + public static File getContentDir(Context context) { + return new File(context.getFilesDir(), ContentDirName.CONTENT_MAIN_DIR_NAME.toString()); + } + + public static File getContentTypeDir(Context context, ContentProfile.ContentType type) { + return new File(getContentDir(context), type.toString()); + } + + public static File getTmpDir(Context context) { + return new File(context.getFilesDir(), "tmp/" + ContentDirName.CONTENT_MAIN_DIR_NAME); + } + + public static boolean cleanTmpDir(Context context) { + File file = getTmpDir(context); + return FileUtils.delete(file); + } } diff --git a/app/src/main/res/layout/content_info_dialog.xml b/app/src/main/res/layout/content_info_dialog.xml new file mode 100644 index 00000000..97422a99 --- /dev/null +++ b/app/src/main/res/layout/content_info_dialog.xml @@ -0,0 +1,106 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/contents_fragment.xml b/app/src/main/res/layout/contents_fragment.xml new file mode 100644 index 00000000..ddfb27cb --- /dev/null +++ b/app/src/main/res/layout/contents_fragment.xml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + +