diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/ImportControlActivity.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/ImportControlActivity.java index 253f3397f..a3739dbd5 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/ImportControlActivity.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/ImportControlActivity.java @@ -4,8 +4,6 @@ import android.app.Activity; import android.content.Intent; import android.net.Uri; import android.os.Bundle; -import android.os.Handler; -import android.os.Looper; import android.view.View; import android.view.inputmethod.InputMethodManager; import android.widget.EditText; @@ -89,7 +87,7 @@ public class ImportControlActivity extends Activity { }).start(); //Auto show the keyboard - new Handler(Looper.getMainLooper()).postDelayed(() -> { + Tools.MAIN_HANDLER.postDelayed(() -> { InputMethodManager imm = (InputMethodManager) getApplicationContext().getSystemService(INPUT_METHOD_SERVICE); imm.toggleSoftInput(InputMethodManager.SHOW_IMPLICIT, 0); mEditText.setSelection(mEditText.getText().length()); diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/JavaGUILauncherActivity.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/JavaGUILauncherActivity.java index 2b73bb817..7d4ac75e2 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/JavaGUILauncherActivity.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/JavaGUILauncherActivity.java @@ -153,6 +153,7 @@ public class JavaGUILauncherActivity extends BaseActivity implements View.OnTouc final Runtime runtime = MultiRTUtils.forceReread(jreName); mSkipDetectMod = getIntent().getExtras().getBoolean("skipDetectMod", false); + if(getIntent().getExtras().getBoolean("openLogOutput", false)) openLogOutput(null); if (mSkipDetectMod) { new Thread(() -> launchJavaRuntime(runtime, modFile, javaArgs), "JREMainThread").start(); return; diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/MainActivity.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/MainActivity.java index dab0f67b8..bb45cebf4 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/MainActivity.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/MainActivity.java @@ -21,8 +21,6 @@ import android.graphics.drawable.ColorDrawable; import android.net.Uri; import android.os.Build; import android.os.Bundle; -import android.os.Handler; -import android.os.Looper; import android.provider.DocumentsContract; import android.util.Log; import android.view.KeyEvent; @@ -287,7 +285,7 @@ public class MainActivity extends BaseActivity implements ControlButtonMenuListe protected void onPostResume() { super.onPostResume(); if(minecraftGLView != null) // Useful when backing out of the app - new Handler(Looper.getMainLooper()).postDelayed(() -> minecraftGLView.refreshSize(), 500); + Tools.MAIN_HANDLER.postDelayed(() -> minecraftGLView.refreshSize(), 500); } @Override diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/Tools.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/Tools.java index 115486537..778732a7f 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/Tools.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/Tools.java @@ -22,6 +22,8 @@ import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.Environment; +import android.os.Handler; +import android.os.Looper; import android.provider.DocumentsContract; import android.provider.OpenableColumns; import android.util.ArrayMap; @@ -77,6 +79,7 @@ import java.util.Map; @SuppressWarnings("IOStreamConstructor") public final class Tools { + public static final Handler MAIN_HANDLER = new Handler(Looper.getMainLooper()); public static String APP_NAME = "null"; public static final Gson GLOBAL_GSON = new GsonBuilder().setPrettyPrinting().create(); @@ -959,6 +962,10 @@ public final class Tools { return runtime; } + public static void runOnUiThread(Runnable runnable) { + MAIN_HANDLER.post(runnable); + } + public static @NonNull String pickRuntime(MinecraftProfile minecraftProfile, int targetJavaVersion) { String runtime = getSelectedRuntime(minecraftProfile); String profileRuntime = getRuntimeName(minecraftProfile.javaDir); diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/authenticator/microsoft/MicrosoftBackgroundLogin.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/authenticator/microsoft/MicrosoftBackgroundLogin.java index bfa10ee92..b4191803b 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/authenticator/microsoft/MicrosoftBackgroundLogin.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/authenticator/microsoft/MicrosoftBackgroundLogin.java @@ -2,7 +2,6 @@ package net.kdt.pojavlaunch.authenticator.microsoft; import static net.kdt.pojavlaunch.PojavApplication.sExecutorService; -import android.os.Looper; import android.util.ArrayMap; import android.util.Log; @@ -13,8 +12,10 @@ import com.kdt.mcgui.ProgressLayout; import net.kdt.pojavlaunch.R; import net.kdt.pojavlaunch.Tools; +import net.kdt.pojavlaunch.authenticator.listener.DoneListener; +import net.kdt.pojavlaunch.authenticator.listener.ErrorListener; +import net.kdt.pojavlaunch.authenticator.listener.ProgressListener; import net.kdt.pojavlaunch.value.MinecraftAccount; -import net.kdt.pojavlaunch.authenticator.listener.*; import org.json.JSONArray; import org.json.JSONException; @@ -42,7 +43,6 @@ public class MicrosoftBackgroundLogin { private final boolean mIsRefresh; private final String mAuthCode; - private final android.os.Handler mHandler = new android.os.Handler(Looper.getMainLooper()); private static final Map XSTS_ERRORS; static { XSTS_ERRORS = new ArrayMap<>(); @@ -100,13 +100,13 @@ public class MicrosoftBackgroundLogin { if(doneListener != null) { MinecraftAccount finalAcc = acc; - mHandler.post(() -> doneListener.onLoginDone(finalAcc)); + Tools.runOnUiThread(() -> doneListener.onLoginDone(finalAcc)); } }catch (Exception e){ Log.e("MicroAuth", e.toString()); if(errorListener != null) - mHandler.post(() -> errorListener.onLoginError(e)); + Tools.runOnUiThread(() -> errorListener.onLoginError(e)); } ProgressLayout.clearProgress(ProgressLayout.AUTHENTICATE_MICROSOFT); }); @@ -289,7 +289,7 @@ public class MicrosoftBackgroundLogin { /** Wrapper to ease notifying the listener */ private void notifyProgress(@Nullable ProgressListener listener, int step){ if(listener != null){ - mHandler.post(() -> listener.onLoginProgress(step)); + Tools.runOnUiThread(() -> listener.onLoginProgress(step)); } ProgressLayout.setProgress(ProgressLayout.AUTHENTICATE_MICROSOFT, step*20); } diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/FabricInstallFragment.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/FabricInstallFragment.java new file mode 100644 index 000000000..8980f1061 --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/FabricInstallFragment.java @@ -0,0 +1,180 @@ +package net.kdt.pojavlaunch.fragments; + +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.view.View; +import android.widget.Adapter; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.Button; +import android.widget.ProgressBar; +import android.widget.Spinner; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; + +import net.kdt.pojavlaunch.JavaGUILauncherActivity; +import net.kdt.pojavlaunch.R; +import net.kdt.pojavlaunch.Tools; +import net.kdt.pojavlaunch.modloaders.FabricDownloadTask; +import net.kdt.pojavlaunch.modloaders.FabricUtils; +import net.kdt.pojavlaunch.modloaders.ModloaderDownloadListener; +import net.kdt.pojavlaunch.modloaders.ModloaderListenerProxy; +import net.kdt.pojavlaunch.profiles.VersionSelectorDialog; + +import java.io.File; +import java.io.IOException; +import java.util.List; + +public class FabricInstallFragment extends Fragment implements AdapterView.OnItemSelectedListener, ModloaderDownloadListener, Runnable { + public static final String TAG = "FabricInstallTarget"; + private static ModloaderListenerProxy sTaskProxy; + private TextView mSelectedVersionLabel; + private String mSelectedLoaderVersion; + private Spinner mLoaderVersionSpinner; + private String mSelectedGameVersion; + private boolean mSelectedSnapshot; + private ProgressBar mProgressBar; + private File mDestinationDir; + private Button mStartButton; + private View mRetryView; + public FabricInstallFragment() { + super(R.layout.fragment_fabric_install); + } + + @Override + public void onAttach(@NonNull Context context) { + super.onAttach(context); + this.mDestinationDir = new File(context.getCacheDir(), "fabric-installer"); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + mStartButton = view.findViewById(R.id.fabric_installer_start_button); + mStartButton.setOnClickListener(this::onClickStart); + mSelectedVersionLabel = view.findViewById(R.id.fabric_installer_version_select_label); + view.findViewById(R.id.fabric_installer_game_version_change).setOnClickListener(this::onClickSelect); + mLoaderVersionSpinner = view.findViewById(R.id.fabric_installer_loader_ver_spinner); + mLoaderVersionSpinner.setOnItemSelectedListener(this); + mProgressBar = view.findViewById(R.id.fabric_installer_progress_bar); + mRetryView = view.findViewById(R.id.fabric_installer_retry_layout); + view.findViewById(R.id.fabric_installer_retry_button).setOnClickListener(this::onClickRetry); + if(sTaskProxy != null) { + mStartButton.setEnabled(false); + sTaskProxy.attachListener(this); + } + new Thread(this).start(); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + if(sTaskProxy != null) { + sTaskProxy.detachListener(); + } + } + + private void onClickStart(View v) { + sTaskProxy = new ModloaderListenerProxy(); + FabricDownloadTask fabricDownloadTask = new FabricDownloadTask(sTaskProxy, mDestinationDir); + sTaskProxy.attachListener(this); + mStartButton.setEnabled(false); + new Thread(fabricDownloadTask).start(); + } + + private void onClickSelect(View v) { + VersionSelectorDialog.open(v.getContext(), true, (id, snapshot)->{ + mSelectedGameVersion = id; + mSelectedVersionLabel.setText(mSelectedGameVersion); + mSelectedSnapshot = snapshot; + if(mSelectedLoaderVersion != null && sTaskProxy == null) mStartButton.setEnabled(true); + }); + } + + private void onClickRetry(View v) { + mLoaderVersionSpinner.setAdapter(null); + mStartButton.setEnabled(false); + mProgressBar.setVisibility(View.VISIBLE); + mRetryView.setVisibility(View.GONE); + new Thread(this).start(); + } + + @Override + public void onItemSelected(AdapterView adapterView, View view, int i, long l) { + Adapter adapter = (Adapter) adapterView.getAdapter(); + mSelectedLoaderVersion = (String) adapter.getItem(i); + } + + @Override + public void onNothingSelected(AdapterView adapterView) { + mSelectedLoaderVersion = null; + } + + @Override + public void onDownloadFinished(File downloadedFile) { + Tools.runOnUiThread(()->{ + Context context = requireContext(); + sTaskProxy.detachListener(); + sTaskProxy = null; + mStartButton.setEnabled(true); + Intent intent = new Intent(context, JavaGUILauncherActivity.class); + FabricUtils.addAutoInstallArgs(intent, downloadedFile, mSelectedGameVersion, mSelectedLoaderVersion, mSelectedSnapshot, true); + context.startActivity(intent); + }); + } + + @Override + public void onDataNotAvailable() { + Tools.runOnUiThread(()->{ + Context context = requireContext(); + sTaskProxy.detachListener(); + sTaskProxy = null; + mStartButton.setEnabled(true); + Tools.dialog(context, + context.getString(R.string.global_error), + context.getString(R.string.fabric_dl_cant_read_meta)); + }); + } + + @Override + public void onDownloadError(Exception e) { + Tools.runOnUiThread(()-> { + Context context = requireContext(); + sTaskProxy.detachListener(); + sTaskProxy = null; + mStartButton.setEnabled(true); + Tools.showError(context, e); + }); + } + + @Override + public void run() { + try { + List mLoaderVersions = FabricUtils.downloadLoaderVersionList(false); + if (mLoaderVersions != null) { + Tools.runOnUiThread(()->{ + Context context = getContext(); + if(context == null) return; + ArrayAdapter arrayAdapter = new ArrayAdapter<>(context, R.layout.support_simple_spinner_dropdown_item, mLoaderVersions); + mLoaderVersionSpinner.setAdapter(arrayAdapter); + mProgressBar.setVisibility(View.GONE); + }); + }else{ + Tools.runOnUiThread(()-> { + mRetryView.setVisibility(View.VISIBLE); + mProgressBar.setVisibility(View.GONE); + }); + } + }catch (IOException e) { + Tools.runOnUiThread(()-> { + if(getContext() != null) Tools.showError(getContext(), e); + mRetryView.setVisibility(View.VISIBLE); + mProgressBar.setVisibility(View.GONE); + }); + } + } +} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/ForgeInstallFragment.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/ForgeInstallFragment.java new file mode 100644 index 000000000..81e56b5aa --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/ForgeInstallFragment.java @@ -0,0 +1,146 @@ +package net.kdt.pojavlaunch.fragments; + +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.ExpandableListView; +import android.widget.ProgressBar; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; + +import net.kdt.pojavlaunch.JavaGUILauncherActivity; +import net.kdt.pojavlaunch.R; +import net.kdt.pojavlaunch.Tools; +import net.kdt.pojavlaunch.modloaders.ForgeDownloadTask; +import net.kdt.pojavlaunch.modloaders.ForgeUtils; +import net.kdt.pojavlaunch.modloaders.ForgeVersionListAdapter; +import net.kdt.pojavlaunch.modloaders.ModloaderDownloadListener; +import net.kdt.pojavlaunch.modloaders.ModloaderListenerProxy; + +import java.io.File; +import java.io.IOException; +import java.util.List; + +public class ForgeInstallFragment extends Fragment implements Runnable, View.OnClickListener, ExpandableListView.OnChildClickListener, ModloaderDownloadListener { + public static final String TAG = "ForgeInstallFragment"; + private static ModloaderListenerProxy sTaskProxy; + private ExpandableListView mExpandableListView; + private ProgressBar mProgressBar; + private File mDestinationFile; + private LayoutInflater mInflater; + private View mRetryView; + + public ForgeInstallFragment() { + super(R.layout.fragment_forge_installer); + } + + @Override + public void onAttach(@NonNull Context context) { + super.onAttach(context); + this.mInflater = LayoutInflater.from(context); + this.mDestinationFile = new File(context.getCacheDir(), "forge-installer.jar"); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + mProgressBar = view.findViewById(R.id.forge_list_progress_bar); + mExpandableListView = view.findViewById(R.id.forge_expandable_version_list); + mExpandableListView.setOnChildClickListener(this); + mRetryView = view.findViewById(R.id.forge_installer_retry_layout); + view.findViewById(R.id.forge_installer_retry_button).setOnClickListener(this); + if(sTaskProxy != null) { + mExpandableListView.setEnabled(false); + sTaskProxy.attachListener(this); + } + new Thread(this).start(); + } + + @Override + public void onDestroyView() { + if(sTaskProxy != null) sTaskProxy.detachListener(); + super.onDestroyView(); + } + + @Override + public void run() { + try { + List forgeVersions = ForgeUtils.downloadForgeVersions(); + Tools.runOnUiThread(()->{ + if(forgeVersions != null) { + mExpandableListView.setAdapter(new ForgeVersionListAdapter(forgeVersions, mInflater)); + }else{ + mRetryView.setVisibility(View.VISIBLE); + } + mProgressBar.setVisibility(View.GONE); + }); + }catch (IOException e) { + Tools.runOnUiThread(()-> { + if (getContext() != null) { + Tools.showError(getContext(), e); + mRetryView.setVisibility(View.VISIBLE); + mProgressBar.setVisibility(View.GONE); + } + }); + } + } + + @Override + public void onClick(View view) { + mRetryView.setVisibility(View.GONE); + mProgressBar.setVisibility(View.VISIBLE); + new Thread(this).start(); + } + + @Override + public boolean onChildClick(ExpandableListView expandableListView, View view, int i, int i1, long l) { + String forgeVersion = (String)expandableListView.getExpandableListAdapter().getChild(i, i1); + sTaskProxy = new ModloaderListenerProxy(); + ForgeDownloadTask downloadTask = new ForgeDownloadTask(sTaskProxy, forgeVersion, mDestinationFile); + sTaskProxy.attachListener(this); + mExpandableListView.setEnabled(false); + new Thread(downloadTask).start(); + return true; + } + + @Override + public void onDownloadFinished(File downloadedFile) { + Tools.runOnUiThread(()->{ + Context context = requireContext(); + sTaskProxy.detachListener(); + sTaskProxy = null; + mExpandableListView.setEnabled(true); + Intent modInstallerStartIntent = new Intent(context, JavaGUILauncherActivity.class); + ForgeUtils.addAutoInstallArgs(modInstallerStartIntent, downloadedFile, true); + context.startActivity(modInstallerStartIntent); + }); + } + + @Override + public void onDataNotAvailable() { + Tools.runOnUiThread(()->{ + Context context = requireContext(); + sTaskProxy.detachListener(); + sTaskProxy = null; + mExpandableListView.setEnabled(true); + Tools.dialog(context, + context.getString(R.string.global_error), + context.getString(R.string.forge_dl_no_installer)); + }); + } + + @Override + public void onDownloadError(Exception e) { + Tools.runOnUiThread(()->{ + Context context = requireContext(); + sTaskProxy.detachListener(); + sTaskProxy = null; + mExpandableListView.setEnabled(true); + Tools.showError(context, e); + }); + } +} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/MainMenuFragment.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/MainMenuFragment.java index edc715fa9..29e5b4d33 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/MainMenuFragment.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/MainMenuFragment.java @@ -5,7 +5,6 @@ import static net.kdt.pojavlaunch.Tools.shareLog; import android.content.Intent; import android.os.Bundle; import android.view.View; -import android.view.ViewGroup; import android.widget.Button; import android.widget.ImageButton; import android.widget.Toast; @@ -19,7 +18,6 @@ import net.kdt.pojavlaunch.R; import net.kdt.pojavlaunch.Tools; import net.kdt.pojavlaunch.extra.ExtraConstants; import net.kdt.pojavlaunch.extra.ExtraCore; -import net.kdt.pojavlaunch.modloaders.ForgeDownloaderDialog; import net.kdt.pojavlaunch.progresskeeper.ProgressKeeper; public class MainMenuFragment extends Fragment { @@ -53,7 +51,7 @@ public class MainMenuFragment extends Fragment { mShareLogsButton.setOnClickListener((v) -> shareLog(requireContext())); mNewsButton.setOnLongClickListener((v)->{ - new ForgeDownloaderDialog().show(view.getContext(), (ViewGroup) view); + Tools.swapFragment(requireActivity(), FabricInstallFragment.class, FabricInstallFragment.TAG, true, null); return true; }); } diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/ProfileEditorFragment.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/ProfileEditorFragment.java index a2c195f52..85545cb53 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/ProfileEditorFragment.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/ProfileEditorFragment.java @@ -1,6 +1,5 @@ package net.kdt.pojavlaunch.fragments; -import static net.kdt.pojavlaunch.extra.ExtraCore.getValue; import static net.kdt.pojavlaunch.profiles.ProfileAdapter.CREATE_PROFILE_MAGIC; import android.content.Context; @@ -11,17 +10,13 @@ import android.view.ViewGroup; import android.widget.ArrayAdapter; import android.widget.Button; import android.widget.EditText; -import android.widget.ExpandableListAdapter; -import android.widget.ExpandableListView; import android.widget.Spinner; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.appcompat.app.AlertDialog; import androidx.fragment.app.Fragment; -import net.kdt.pojavlaunch.JMinecraftVersionList; import net.kdt.pojavlaunch.R; import net.kdt.pojavlaunch.Tools; import net.kdt.pojavlaunch.extra.ExtraConstants; @@ -30,7 +25,7 @@ import net.kdt.pojavlaunch.multirt.MultiRTUtils; import net.kdt.pojavlaunch.multirt.RTSpinnerAdapter; import net.kdt.pojavlaunch.multirt.Runtime; import net.kdt.pojavlaunch.prefs.LauncherPreferences; -import net.kdt.pojavlaunch.profiles.VersionListAdapter; +import net.kdt.pojavlaunch.profiles.VersionSelectorDialog; import net.kdt.pojavlaunch.value.launcherprofiles.LauncherProfiles; import net.kdt.pojavlaunch.value.launcherprofiles.MinecraftProfile; @@ -117,28 +112,10 @@ public class ProfileEditorFragment extends Fragment { }); // Setup the expendable list behavior - mVersionSelectButton.setOnClickListener(v -> { - AlertDialog.Builder builder = new AlertDialog.Builder(mDefaultVersion.getContext()); - ExpandableListView expandableListView = (ExpandableListView) LayoutInflater.from(mDefaultVersion.getContext()) - .inflate(R.layout.dialog_expendable_list_view , null); - JMinecraftVersionList jMinecraftVersionList = (JMinecraftVersionList) getValue(ExtraConstants.RELEASE_TABLE); - JMinecraftVersionList.Version[] versionArray; - if(jMinecraftVersionList == null || jMinecraftVersionList.versions == null) versionArray = new JMinecraftVersionList.Version[0]; - else versionArray = jMinecraftVersionList.versions; - ExpandableListAdapter adapter = new VersionListAdapter(versionArray, mDefaultVersion.getContext()); - - expandableListView.setAdapter(adapter); - builder.setView(expandableListView); - AlertDialog dialog = builder.show(); - - expandableListView.setOnChildClickListener((parent, v1, groupPosition, childPosition, id) -> { - String version = ((String) adapter.getChild(groupPosition, childPosition)); - mTempProfile.lastVersionId = version; - mDefaultVersion.setText(version); - dialog.dismiss(); - return true; - }); - }); + mVersionSelectButton.setOnClickListener(v -> VersionSelectorDialog.open(v.getContext(), false, (id, snapshot)->{ + mTempProfile.lastVersionId = id; + mDefaultVersion.setText(id); + })); diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/FabricDownloadTask.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/FabricDownloadTask.java new file mode 100644 index 000000000..03ea723ad --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/FabricDownloadTask.java @@ -0,0 +1,77 @@ +package net.kdt.pojavlaunch.modloaders; + +import com.kdt.mcgui.ProgressLayout; + +import net.kdt.pojavlaunch.R; +import net.kdt.pojavlaunch.Tools; +import net.kdt.pojavlaunch.progresskeeper.ProgressKeeper; +import net.kdt.pojavlaunch.utils.DownloadUtils; + +import java.io.File; +import java.io.IOException; + +public class FabricDownloadTask implements Runnable, Tools.DownloaderFeedback{ + private final File mDestinationDir; + private final File mDestinationFile; + private final ModloaderDownloadListener mModloaderDownloadListener; + + public FabricDownloadTask(ModloaderDownloadListener modloaderDownloadListener, File mDestinationDir) { + this.mModloaderDownloadListener = modloaderDownloadListener; + this.mDestinationDir = mDestinationDir; + this.mDestinationFile = new File(mDestinationDir, "fabric-installer.jar"); + } + + @Override + public void run() { + ProgressKeeper.submitProgress(ProgressLayout.INSTALL_MODPACK, 0, R.string.fabric_dl_progress); + try { + if(runCatching()) mModloaderDownloadListener.onDownloadFinished(mDestinationFile); + }catch (IOException e) { + mModloaderDownloadListener.onDownloadError(e); + } + ProgressKeeper.submitProgress(ProgressLayout.INSTALL_MODPACK, -1, -1); + } + + private boolean runCatching() throws IOException { + if(!mDestinationDir.exists() && !mDestinationDir.mkdirs()) throw new IOException("Failed to create cache directory"); + String[] urlAndVersion = FabricUtils.getInstallerUrlAndVersion(); + if(urlAndVersion == null) { + mModloaderDownloadListener.onDataNotAvailable(); + return false; + } + File versionFile = new File(mDestinationDir, "fabric-installer-version"); + boolean shouldDownloadInstaller = true; + if(urlAndVersion[1] != null && versionFile.canRead()) { // if we know the latest version that we have and the server has + try { + shouldDownloadInstaller = !urlAndVersion[1].equals(Tools.read(versionFile.getAbsolutePath())); + }catch (IOException e) { + e.printStackTrace(); + } + } + if(shouldDownloadInstaller) { + if (urlAndVersion[0] != null) { + byte[] buffer = new byte[8192]; + DownloadUtils.downloadFileMonitored(urlAndVersion[0], mDestinationFile, buffer, this); + if(urlAndVersion[1] != null) { + try { + Tools.write(versionFile.getAbsolutePath(), urlAndVersion[1]); + }catch (IOException e) { + e.printStackTrace(); + } + } + return true; + } else { + mModloaderDownloadListener.onDataNotAvailable(); + return false; + } + }else{ + return true; + } + } + + @Override + public void updateProgress(int curr, int max) { + int progress100 = (int)(((float)curr / (float)max)*100f); + ProgressKeeper.submitProgress(ProgressLayout.INSTALL_MODPACK, progress100, R.string.fabric_dl_progress); + } +} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/FabricMetaReader.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/FabricMetaReader.java new file mode 100644 index 000000000..151c61c2a --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/FabricMetaReader.java @@ -0,0 +1,8 @@ +package net.kdt.pojavlaunch.modloaders; + +import org.json.JSONException; +import org.json.JSONObject; + +public interface FabricMetaReader { + boolean processMetadata(JSONObject jsonObject) throws JSONException; +} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/FabricUtils.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/FabricUtils.java new file mode 100644 index 000000000..bc4a60d6c --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/FabricUtils.java @@ -0,0 +1,63 @@ +package net.kdt.pojavlaunch.modloaders; + +import android.content.Intent; + +import net.kdt.pojavlaunch.Tools; +import net.kdt.pojavlaunch.utils.DownloadUtils; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +public class FabricUtils { + private static final String FABRIC_INSTALLER_METADATA_URL = "https://meta.fabricmc.net/v2/versions/installer"; + private static final String FABRIC_LOADER_METADATA_URL = "https://meta.fabricmc.net/v2/versions/loader"; + public static List downloadLoaderVersionList(boolean onlyStable) throws IOException { + String loaderMetadata = DownloadUtils.downloadString(FABRIC_LOADER_METADATA_URL); + List loaderList = new ArrayList<>(); + if(enumerateMetadata(loaderMetadata, (object)->{ + if(onlyStable && !object.getBoolean("stable")) return false; + loaderList.add(object.getString("version")); + return false; + }) == null) return null; + return loaderList; + } + + public static String[] getInstallerUrlAndVersion() throws IOException{ + String installerMetadata = DownloadUtils.downloadString(FABRIC_INSTALLER_METADATA_URL); + JSONObject selectedMetadata = enumerateMetadata(installerMetadata, (object)-> object.getBoolean("stable")); + if(selectedMetadata == null) return null; + return new String[] {selectedMetadata.optString("url"), selectedMetadata.optString("version")}; + } + + public static void addAutoInstallArgs(Intent intent, File modInstalllerJar, + String gameVersion, String loaderVersion, + boolean isSnapshot, boolean createProfile) { + intent.putExtra("javaArgs", "-jar " + modInstalllerJar.getAbsolutePath() + " client -dir "+ Tools.DIR_GAME_NEW + + " -mcversion "+gameVersion +" -loader "+loaderVersion + + (isSnapshot ? " -snapshot" : "") + + (createProfile ? "" : " -noprofile")); + intent.putExtra("openLogOutput", true); + + } + + private static JSONObject enumerateMetadata(String inputMetadata, FabricMetaReader metaReader) { + try { + JSONArray fullMetadata = new JSONArray(inputMetadata); + JSONObject metadataObject = null; + for(int i = 0; i < fullMetadata.length(); i++) { + metadataObject = fullMetadata.getJSONObject(i); + if(metaReader.processMetadata(metadataObject)) return metadataObject; + } + return metadataObject; + }catch (JSONException e) { + e.printStackTrace(); + return null; + } + } +} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/ForgeDownloadListener.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/ForgeDownloadListener.java deleted file mode 100644 index 085e227e8..000000000 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/ForgeDownloadListener.java +++ /dev/null @@ -1,7 +0,0 @@ -package net.kdt.pojavlaunch.modloaders; - -public interface ForgeDownloadListener { - void onDownloadFinished(); - void onInstallerNotAvailable(); - void onDownloadError(Exception e); -} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/ForgeDownloadTask.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/ForgeDownloadTask.java index 52c9f539a..0f8c1a4cf 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/ForgeDownloadTask.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/ForgeDownloadTask.java @@ -14,9 +14,9 @@ import java.io.IOException; public class ForgeDownloadTask implements Runnable, Tools.DownloaderFeedback { private final String mForgeUrl; private final String mForgeVersion; - private final File mDestinationFile; - private final ForgeDownloadListener mListener; - public ForgeDownloadTask(ForgeDownloadListener listener, String forgeVersion, File destinationFile) { + public final File mDestinationFile; + private final ModloaderDownloadListener mListener; + public ForgeDownloadTask(ModloaderDownloadListener listener, String forgeVersion, File destinationFile) { this.mListener = listener; this.mForgeUrl = ForgeUtils.getInstallerUrl(forgeVersion); this.mForgeVersion = forgeVersion; @@ -28,10 +28,10 @@ public class ForgeDownloadTask implements Runnable, Tools.DownloaderFeedback { try { byte[] buffer = new byte[8192]; DownloadUtils.downloadFileMonitored(mForgeUrl, mDestinationFile, buffer, this); - mListener.onDownloadFinished(); + mListener.onDownloadFinished(mDestinationFile); }catch (IOException e) { if(e instanceof FileNotFoundException) { - mListener.onInstallerNotAvailable(); + mListener.onDataNotAvailable(); }else{ mListener.onDownloadError(e); } diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/ForgeDownloaderDialog.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/ForgeDownloaderDialog.java deleted file mode 100644 index 26a96bec6..000000000 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/ForgeDownloaderDialog.java +++ /dev/null @@ -1,94 +0,0 @@ -package net.kdt.pojavlaunch.modloaders; - -import android.app.AlertDialog; -import android.content.Context; -import android.content.Intent; -import android.os.Handler; -import android.os.Looper; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ExpandableListView; -import android.widget.ProgressBar; - -import net.kdt.pojavlaunch.JavaGUILauncherActivity; -import net.kdt.pojavlaunch.R; -import net.kdt.pojavlaunch.Tools; - -import java.io.File; -import java.io.IOException; -import java.util.List; - -public class ForgeDownloaderDialog implements Runnable, ExpandableListView.OnChildClickListener, ForgeDownloadListener { - private final Handler mHandler = new Handler(Looper.getMainLooper()); - private ExpandableListView mExpandableListView; - private ProgressBar mProgressBar; - private AlertDialog mAlertDialog; - private File mDestinationFile; - private Context mContext; - public void show(Context context, ViewGroup root) { - this.mContext = context; - this.mDestinationFile = new File(context.getCacheDir(), "forge-installer.jar"); - AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(context); - View dialogView = LayoutInflater.from(context).inflate(R.layout.dialog_expandable_forge_list, root, false); - mProgressBar = dialogView.findViewById(R.id.forge_list_progress_bar); - mExpandableListView = dialogView.findViewById(R.id.forge_expandable_version_list); - mExpandableListView.setOnChildClickListener(this); - dialogBuilder.setView(dialogView); - mAlertDialog = dialogBuilder.show(); - new Thread(this).start(); - } - - @Override - public void run() { - try { - List forgeVersions = ForgeUtils.downloadForgeVersions(); - mHandler.post(()->{ - if(forgeVersions != null) { - mProgressBar.setVisibility(View.GONE); - mExpandableListView.setAdapter(new ForgeVersionListAdapter(forgeVersions, LayoutInflater.from(mContext))); - }else{ - mAlertDialog.dismiss(); - } - }); - }catch (IOException e) { - mHandler.post(()->{ - mAlertDialog.dismiss(); - Tools.showError(mContext, e); - }); - } - } - - - - @Override - public boolean onChildClick(ExpandableListView expandableListView, View view, int i, int i1, long l) { - String forgeVersion = (String)expandableListView.getExpandableListAdapter().getChild(i, i1); - new Thread(new ForgeDownloadTask(this, forgeVersion, mDestinationFile)).start(); - mAlertDialog.dismiss(); - return true; - } - - @Override - public void onDownloadFinished() { - Intent intent = new Intent(mContext, JavaGUILauncherActivity.class); - ForgeUtils.addAutoInstallArgs(intent, mDestinationFile, true); // since it's a user-invoked install, we want to create a new profile - mContext.startActivity(intent); - } - - @Override - public void onInstallerNotAvailable() { - mHandler.post(()-> { - mAlertDialog.dismiss(); - Tools.dialog(mContext, - mContext.getString(R.string.global_error), - mContext.getString(R.string.forge_dl_no_installer)); - }); - } - - @Override - public void onDownloadError(Exception e) { - mHandler.post(mAlertDialog::dismiss); - Tools.showError(mContext, e); - } -} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/ModloaderDownloadListener.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/ModloaderDownloadListener.java new file mode 100644 index 000000000..9260d4cbc --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/ModloaderDownloadListener.java @@ -0,0 +1,9 @@ +package net.kdt.pojavlaunch.modloaders; + +import java.io.File; + +public interface ModloaderDownloadListener { + void onDownloadFinished(File downloadedFile); + void onDataNotAvailable(); + void onDownloadError(Exception e); +} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/ModloaderListenerProxy.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/ModloaderListenerProxy.java new file mode 100644 index 000000000..97713c9e4 --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/ModloaderListenerProxy.java @@ -0,0 +1,61 @@ +package net.kdt.pojavlaunch.modloaders; + +import java.io.File; + +public class ModloaderListenerProxy implements ModloaderDownloadListener { + public static final int PROXY_RESULT_NONE = -1; + public static final int PROXY_RESULT_FINISHED = 0; + public static final int PROXY_RESULT_NOT_AVAILABLE = 1; + public static final int PROXY_RESULT_ERROR = 2; + private ModloaderDownloadListener mDestinationListener; + private Object mProxyResultObject; + private int mProxyResult = PROXY_RESULT_NONE; + + @Override + public synchronized void onDownloadFinished(File downloadedFile) { + if(mDestinationListener != null) { + mDestinationListener.onDownloadFinished(downloadedFile); + }else{ + mProxyResult = PROXY_RESULT_FINISHED; + mProxyResultObject = downloadedFile; + } + } + + @Override + public synchronized void onDataNotAvailable() { + if(mDestinationListener != null) { + mDestinationListener.onDataNotAvailable(); + }else{ + mProxyResult = PROXY_RESULT_NOT_AVAILABLE; + mProxyResultObject = null; + } + } + + @Override + public synchronized void onDownloadError(Exception e) { + if(mDestinationListener != null) { + mDestinationListener.onDownloadError(e); + }else { + mProxyResult = PROXY_RESULT_ERROR; + mProxyResultObject = e; + } + } + + public synchronized void attachListener(ModloaderDownloadListener listener) { + switch(mProxyResult) { + case PROXY_RESULT_FINISHED: + listener.onDownloadFinished((File) mProxyResultObject); + break; + case PROXY_RESULT_NOT_AVAILABLE: + listener.onDataNotAvailable(); + break; + case PROXY_RESULT_ERROR: + listener.onDownloadError((Exception) mProxyResultObject); + break; + } + mDestinationListener = listener; + } + public synchronized void detachListener() { + mDestinationListener = null; + } +} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/profiles/VersionListAdapter.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/profiles/VersionListAdapter.java index 28c1ab700..6b54d0474 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/profiles/VersionListAdapter.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/profiles/VersionListAdapter.java @@ -24,8 +24,11 @@ public class VersionListAdapter extends BaseExpandableListAdapter implements Exp private final String[] mGroups; private final String[] mInstalledVersions; private final List[] mData; + private final boolean mHideCustomVersions; + private final int mSnapshotListPosition; - public VersionListAdapter(JMinecraftVersionList.Version[] versionList, Context ctx){ + public VersionListAdapter(JMinecraftVersionList.Version[] versionList, boolean hideCustomVersions, Context ctx){ + mHideCustomVersions = hideCustomVersions; mLayoutInflater = (LayoutInflater) ctx.getSystemService(Context.LAYOUT_INFLATER_SERVICE); List releaseList = new FilteredSubList<>(versionList, item -> item.type.equals("release")); @@ -43,6 +46,7 @@ public class VersionListAdapter extends BaseExpandableListAdapter implements Exp ctx.getString(R.string.mcl_setting_veroption_oldalpha) }; mData = new List[]{ releaseList, snapshotList, betaList, alphaList}; + mSnapshotListPosition = 1; }else{ mGroups = new String[]{ ctx.getString(R.string.mcl_setting_veroption_installed), @@ -52,6 +56,7 @@ public class VersionListAdapter extends BaseExpandableListAdapter implements Exp ctx.getString(R.string.mcl_setting_veroption_oldalpha) }; mData = new List[]{Arrays.asList(mInstalledVersions), releaseList, snapshotList, betaList, alphaList}; + mSnapshotListPosition = 2; } } @@ -116,7 +121,12 @@ public class VersionListAdapter extends BaseExpandableListAdapter implements Exp return true; } + public boolean isSnapshotSelected(int groupPosition) { + return groupPosition == mSnapshotListPosition; + } + private boolean areInstalledVersionsAvailable(){ + if(mHideCustomVersions) return false; return !(mInstalledVersions == null || mInstalledVersions.length == 0); } diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/profiles/VersionSelectorDialog.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/profiles/VersionSelectorDialog.java new file mode 100644 index 000000000..c88714c7f --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/profiles/VersionSelectorDialog.java @@ -0,0 +1,37 @@ +package net.kdt.pojavlaunch.profiles; + +import static net.kdt.pojavlaunch.extra.ExtraCore.getValue; + +import android.content.Context; +import android.view.LayoutInflater; +import android.widget.ExpandableListView; + +import androidx.appcompat.app.AlertDialog; + +import net.kdt.pojavlaunch.JMinecraftVersionList; +import net.kdt.pojavlaunch.R; +import net.kdt.pojavlaunch.extra.ExtraConstants; + +public class VersionSelectorDialog { + public static void open(Context context, boolean hideCustomVersions, VersionSelectorListener listener) { + AlertDialog.Builder builder = new AlertDialog.Builder(context); + ExpandableListView expandableListView = (ExpandableListView) LayoutInflater.from(context) + .inflate(R.layout.dialog_expendable_list_view , null); + JMinecraftVersionList jMinecraftVersionList = (JMinecraftVersionList) getValue(ExtraConstants.RELEASE_TABLE); + JMinecraftVersionList.Version[] versionArray; + if(jMinecraftVersionList == null || jMinecraftVersionList.versions == null) versionArray = new JMinecraftVersionList.Version[0]; + else versionArray = jMinecraftVersionList.versions; + VersionListAdapter adapter = new VersionListAdapter(versionArray, hideCustomVersions, context); + + expandableListView.setAdapter(adapter); + builder.setView(expandableListView); + AlertDialog dialog = builder.show(); + + expandableListView.setOnChildClickListener((parent, v1, groupPosition, childPosition, id) -> { + String version = adapter.getChild(groupPosition, childPosition); + listener.onVersionSelected(version, adapter.isSnapshotSelected(groupPosition)); + dialog.dismiss(); + return true; + }); + } +} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/profiles/VersionSelectorListener.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/profiles/VersionSelectorListener.java new file mode 100644 index 000000000..0d48a9207 --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/profiles/VersionSelectorListener.java @@ -0,0 +1,5 @@ +package net.kdt.pojavlaunch.profiles; + +public interface VersionSelectorListener { + void onVersionSelected(String versionId, boolean isSnapshot); +} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/services/ProgressService.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/services/ProgressService.java index a086701d7..b5fc6396b 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/services/ProgressService.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/services/ProgressService.java @@ -6,9 +6,7 @@ import android.app.Service; import android.content.Context; import android.content.Intent; import android.os.Build; -import android.os.Handler; import android.os.IBinder; -import android.os.Looper; import android.os.Process; import android.util.Log; @@ -28,7 +26,6 @@ import net.kdt.pojavlaunch.progresskeeper.TaskCountListener; */ public class ProgressService extends Service implements TaskCountListener { - private final Handler mainThreadHandler = new Handler(Looper.getMainLooper()); private NotificationManagerCompat notificationManagerCompat; /** Simple wrapper to start the service */ @@ -85,7 +82,7 @@ public class ProgressService extends Service implements TaskCountListener { @Override public void onUpdateTaskCount(int taskCount) { - mainThreadHandler.post(()->{ + Tools.MAIN_HANDLER.post(()->{ if(taskCount > 0) { mNotificationBuilder.setContentText(getString(R.string.progresslayout_tasks_in_progress, taskCount)); notificationManagerCompat.notify(1, mNotificationBuilder.build()); diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/tasks/AsyncMinecraftDownloader.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/tasks/AsyncMinecraftDownloader.java index 1f43db675..b04c54055 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/tasks/AsyncMinecraftDownloader.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/tasks/AsyncMinecraftDownloader.java @@ -183,6 +183,8 @@ public class AsyncMinecraftDownloader { os.close(); } } + } catch (DownloaderException e) { + throw e; } catch (Throwable e) { Log.e("AsyncMcDownloader", e.toString(),e ); ProgressKeeper.submitProgress(ProgressLayout.DOWNLOAD_MINECRAFT, -1, -1); diff --git a/app_pojavlauncher/src/main/res/layout/dialog_expandable_forge_list.xml b/app_pojavlauncher/src/main/res/layout/dialog_expandable_forge_list.xml deleted file mode 100644 index 39f67a91e..000000000 --- a/app_pojavlauncher/src/main/res/layout/dialog_expandable_forge_list.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/app_pojavlauncher/src/main/res/layout/fragment_fabric_install.xml b/app_pojavlauncher/src/main/res/layout/fragment_fabric_install.xml new file mode 100644 index 000000000..50f2d1388 --- /dev/null +++ b/app_pojavlauncher/src/main/res/layout/fragment_fabric_install.xml @@ -0,0 +1,135 @@ + + + + + + + + + + + + + +