Merge pull request #6508 from SachinVin/not-the-cia
Android: Offload CIA installation to background thread
This commit is contained in:
commit
06db4ffb17
|
@ -198,7 +198,7 @@ jobs:
|
||||||
sudo apt-get update -y
|
sudo apt-get update -y
|
||||||
sudo apt-get install ccache glslang-dev glslang-tools apksigner -y
|
sudo apt-get install ccache glslang-dev glslang-tools apksigner -y
|
||||||
- name: Build
|
- name: Build
|
||||||
run: ./.ci/android/build.sh
|
run: JAVA_HOME=$JAVA_HOME_17_X64 ./.ci/android/build.sh
|
||||||
- name: Copy and sign artifacts
|
- name: Copy and sign artifacts
|
||||||
env:
|
env:
|
||||||
ANDROID_KEYSTORE_B64: ${{ secrets.ANDROID_KEYSTORE_B64 }}
|
ANDROID_KEYSTORE_B64: ${{ secrets.ANDROID_KEYSTORE_B64 }}
|
||||||
|
|
|
@ -10,7 +10,7 @@ def buildType
|
||||||
def abiFilter = "arm64-v8a" //, "x86"
|
def abiFilter = "arm64-v8a" //, "x86"
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 32
|
compileSdkVersion 33
|
||||||
ndkVersion "25.1.8937393"
|
ndkVersion "25.1.8937393"
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
|
@ -100,6 +100,7 @@ android {
|
||||||
path "../../../CMakeLists.txt"
|
path "../../../CMakeLists.txt"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
namespace 'org.citra.citra_emu'
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
externalNativeBuild {
|
externalNativeBuild {
|
||||||
|
@ -129,6 +130,7 @@ dependencies {
|
||||||
implementation "androidx.slidingpanelayout:slidingpanelayout:1.2.0"
|
implementation "androidx.slidingpanelayout:slidingpanelayout:1.2.0"
|
||||||
implementation 'com.google.android.material:material:1.6.1'
|
implementation 'com.google.android.material:material:1.6.1'
|
||||||
implementation 'androidx.core:core-splashscreen:1.0.0'
|
implementation 'androidx.core:core-splashscreen:1.0.0'
|
||||||
|
implementation "androidx.work:work-runtime:2.8.1"
|
||||||
|
|
||||||
// For loading huge screenshots from the disk.
|
// For loading huge screenshots from the disk.
|
||||||
implementation 'com.squareup.picasso:picasso:2.71828'
|
implementation 'com.squareup.picasso:picasso:2.71828'
|
||||||
|
|
|
@ -23,16 +23,33 @@ public class CitraApplication extends Application {
|
||||||
private void createNotificationChannel() {
|
private void createNotificationChannel() {
|
||||||
// Create the NotificationChannel, but only on API 26+ because
|
// Create the NotificationChannel, but only on API 26+ because
|
||||||
// the NotificationChannel class is new and not in the support library
|
// the NotificationChannel class is new and not in the support library
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
NotificationManager notificationManager = getSystemService(NotificationManager.class);
|
||||||
|
{
|
||||||
|
// General notification
|
||||||
CharSequence name = getString(R.string.app_notification_channel_name);
|
CharSequence name = getString(R.string.app_notification_channel_name);
|
||||||
String description = getString(R.string.app_notification_channel_description);
|
String description = getString(R.string.app_notification_channel_description);
|
||||||
NotificationChannel channel = new NotificationChannel(getString(R.string.app_notification_channel_id), name, NotificationManager.IMPORTANCE_LOW);
|
NotificationChannel channel = new NotificationChannel(
|
||||||
|
getString(R.string.app_notification_channel_id), name,
|
||||||
|
NotificationManager.IMPORTANCE_LOW);
|
||||||
channel.setDescription(description);
|
channel.setDescription(description);
|
||||||
channel.setSound(null, null);
|
channel.setSound(null, null);
|
||||||
channel.setVibrationPattern(null);
|
channel.setVibrationPattern(null);
|
||||||
// Register the channel with the system; you can't change the importance
|
|
||||||
// or other notification behaviors after this
|
notificationManager.createNotificationChannel(channel);
|
||||||
NotificationManager notificationManager = getSystemService(NotificationManager.class);
|
}
|
||||||
|
{
|
||||||
|
// CIA Install notifications
|
||||||
|
NotificationChannel channel = new NotificationChannel(
|
||||||
|
getString(R.string.cia_install_notification_channel_id),
|
||||||
|
getString(R.string.cia_install_notification_channel_name),
|
||||||
|
NotificationManager.IMPORTANCE_DEFAULT);
|
||||||
|
channel.setDescription(getString(R.string.cia_install_notification_channel_description));
|
||||||
|
channel.setSound(null, null);
|
||||||
|
channel.setVibrationPattern(null);
|
||||||
|
|
||||||
notificationManager.createNotificationChannel(channel);
|
notificationManager.createNotificationChannel(channel);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -589,8 +589,6 @@ public final class NativeLibrary {
|
||||||
|
|
||||||
public static native void RemoveAmiibo();
|
public static native void RemoveAmiibo();
|
||||||
|
|
||||||
public static native void InstallCIAS(String[] path);
|
|
||||||
|
|
||||||
public static final int SAVESTATE_SLOT_COUNT = 10;
|
public static final int SAVESTATE_SLOT_COUNT = 10;
|
||||||
|
|
||||||
public static final class SavestateInfo {
|
public static final class SavestateInfo {
|
||||||
|
|
|
@ -20,10 +20,15 @@ import androidx.core.graphics.Insets;
|
||||||
import androidx.core.view.ViewCompat;
|
import androidx.core.view.ViewCompat;
|
||||||
import androidx.core.view.WindowCompat;
|
import androidx.core.view.WindowCompat;
|
||||||
import androidx.core.view.WindowInsetsCompat;
|
import androidx.core.view.WindowInsetsCompat;
|
||||||
|
import androidx.work.Data;
|
||||||
|
import androidx.work.ExistingWorkPolicy;
|
||||||
|
import androidx.work.OneTimeWorkRequest;
|
||||||
|
import androidx.work.OutOfQuotaPolicy;
|
||||||
|
import androidx.work.WorkManager;
|
||||||
|
import androidx.work.WorkRequest;
|
||||||
|
|
||||||
import com.google.android.material.appbar.AppBarLayout;
|
import com.google.android.material.appbar.AppBarLayout;
|
||||||
|
|
||||||
import org.citra.citra_emu.NativeLibrary;
|
|
||||||
import org.citra.citra_emu.R;
|
import org.citra.citra_emu.R;
|
||||||
import org.citra.citra_emu.activities.EmulationActivity;
|
import org.citra.citra_emu.activities.EmulationActivity;
|
||||||
import org.citra.citra_emu.contracts.OpenFileResultContract;
|
import org.citra.citra_emu.contracts.OpenFileResultContract;
|
||||||
|
@ -32,6 +37,7 @@ import org.citra.citra_emu.model.GameProvider;
|
||||||
import org.citra.citra_emu.ui.platform.PlatformGamesFragment;
|
import org.citra.citra_emu.ui.platform.PlatformGamesFragment;
|
||||||
import org.citra.citra_emu.utils.AddDirectoryHelper;
|
import org.citra.citra_emu.utils.AddDirectoryHelper;
|
||||||
import org.citra.citra_emu.utils.BillingManager;
|
import org.citra.citra_emu.utils.BillingManager;
|
||||||
|
import org.citra.citra_emu.utils.CiaInstallWorker;
|
||||||
import org.citra.citra_emu.utils.CitraDirectoryHelper;
|
import org.citra.citra_emu.utils.CitraDirectoryHelper;
|
||||||
import org.citra.citra_emu.utils.DirectoryInitialization;
|
import org.citra.citra_emu.utils.DirectoryInitialization;
|
||||||
import org.citra.citra_emu.utils.FileBrowserHelper;
|
import org.citra.citra_emu.utils.FileBrowserHelper;
|
||||||
|
@ -50,7 +56,9 @@ public final class MainActivity extends AppCompatActivity implements MainView {
|
||||||
private int mFrameLayoutId;
|
private int mFrameLayoutId;
|
||||||
private PlatformGamesFragment mPlatformGamesFragment;
|
private PlatformGamesFragment mPlatformGamesFragment;
|
||||||
|
|
||||||
private MainPresenter mPresenter = new MainPresenter(this);
|
private final MainPresenter mPresenter = new MainPresenter(this);
|
||||||
|
|
||||||
|
// private final CiaInstallWorker mCiaInstallWorker = new CiaInstallWorker();
|
||||||
|
|
||||||
// Singleton to manage user billing state
|
// Singleton to manage user billing state
|
||||||
private static BillingManager mBillingManager;
|
private static BillingManager mBillingManager;
|
||||||
|
@ -91,7 +99,7 @@ public final class MainActivity extends AppCompatActivity implements MainView {
|
||||||
mPresenter.onDirectorySelected(result.toString());
|
mPresenter.onDirectorySelected(result.toString());
|
||||||
});
|
});
|
||||||
|
|
||||||
private final ActivityResultLauncher<Boolean> mOpenFileLauncher =
|
private final ActivityResultLauncher<Boolean> mInstallCiaFileLauncher =
|
||||||
registerForActivityResult(new OpenFileResultContract(), result -> {
|
registerForActivityResult(new OpenFileResultContract(), result -> {
|
||||||
if (result == null)
|
if (result == null)
|
||||||
return;
|
return;
|
||||||
|
@ -104,8 +112,16 @@ public final class MainActivity extends AppCompatActivity implements MainView {
|
||||||
.show();
|
.show();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
NativeLibrary.InstallCIAS(selectedFiles);
|
WorkManager workManager = WorkManager.getInstance(getApplicationContext());
|
||||||
mPresenter.refreshGameList();
|
workManager.enqueueUniqueWork("installCiaWork", ExistingWorkPolicy.APPEND_OR_REPLACE,
|
||||||
|
new OneTimeWorkRequest.Builder(CiaInstallWorker.class)
|
||||||
|
.setInputData(
|
||||||
|
new Data.Builder().putStringArray("CIA_FILES", selectedFiles)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
|
||||||
|
.build()
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -233,7 +249,7 @@ public final class MainActivity extends AppCompatActivity implements MainView {
|
||||||
mOpenGameListLauncher.launch(null);
|
mOpenGameListLauncher.launch(null);
|
||||||
break;
|
break;
|
||||||
case MainPresenter.REQUEST_INSTALL_CIA:
|
case MainPresenter.REQUEST_INSTALL_CIA:
|
||||||
mOpenFileLauncher.launch(true);
|
mInstallCiaFileLauncher.launch(true);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -0,0 +1,160 @@
|
||||||
|
package org.citra.citra_emu.utils;
|
||||||
|
|
||||||
|
import android.app.Notification;
|
||||||
|
import android.app.NotificationManager;
|
||||||
|
import android.app.PendingIntent;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.core.app.NotificationCompat;
|
||||||
|
import androidx.work.ForegroundInfo;
|
||||||
|
import androidx.work.Worker;
|
||||||
|
import androidx.work.WorkerParameters;
|
||||||
|
|
||||||
|
import org.citra.citra_emu.R;
|
||||||
|
|
||||||
|
public class CiaInstallWorker extends Worker {
|
||||||
|
private final Context mContext = getApplicationContext();
|
||||||
|
|
||||||
|
private final NotificationManager mNotificationManager =
|
||||||
|
mContext.getSystemService(NotificationManager.class);
|
||||||
|
|
||||||
|
static final String GROUP_KEY_CIA_INSTALL_STATUS = "org.citra.citra_emu.CIA_INSTALL_STATUS";
|
||||||
|
|
||||||
|
private final NotificationCompat.Builder mInstallProgressBuilder = new NotificationCompat.Builder(
|
||||||
|
mContext, mContext.getString(R.string.cia_install_notification_channel_id))
|
||||||
|
.setContentTitle(mContext.getString(R.string.install_cia_title))
|
||||||
|
.setContentIntent(PendingIntent.getBroadcast(mContext, 0,
|
||||||
|
new Intent("CitraDoNothing"), PendingIntent.FLAG_IMMUTABLE))
|
||||||
|
.setSmallIcon(R.drawable.ic_stat_notification_logo);
|
||||||
|
|
||||||
|
private final NotificationCompat.Builder mInstallStatusBuilder = new NotificationCompat.Builder(
|
||||||
|
mContext, mContext.getString(R.string.cia_install_notification_channel_id))
|
||||||
|
.setContentTitle(mContext.getString(R.string.install_cia_title))
|
||||||
|
.setSmallIcon(R.drawable.ic_stat_notification_logo)
|
||||||
|
.setGroup(GROUP_KEY_CIA_INSTALL_STATUS);
|
||||||
|
|
||||||
|
private final Notification mSummaryNotification =
|
||||||
|
new NotificationCompat.Builder(mContext, mContext.getString(R.string.cia_install_notification_channel_id))
|
||||||
|
.setContentTitle(mContext.getString(R.string.install_cia_title))
|
||||||
|
.setSmallIcon(R.drawable.ic_stat_notification_logo)
|
||||||
|
.setGroup(GROUP_KEY_CIA_INSTALL_STATUS)
|
||||||
|
.setGroupSummary(true)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
private static long mLastNotifiedTime = 0;
|
||||||
|
|
||||||
|
private static final int SUMMARY_NOTIFICATION_ID = 0xC1A0000;
|
||||||
|
private static final int PROGRESS_NOTIFICATION_ID = SUMMARY_NOTIFICATION_ID + 1;
|
||||||
|
private static int mStatusNotificationId = SUMMARY_NOTIFICATION_ID + 2;
|
||||||
|
|
||||||
|
public CiaInstallWorker(
|
||||||
|
@NonNull Context context,
|
||||||
|
@NonNull WorkerParameters params) {
|
||||||
|
super(context, params);
|
||||||
|
}
|
||||||
|
|
||||||
|
enum InstallStatus {
|
||||||
|
Success,
|
||||||
|
ErrorFailedToOpenFile,
|
||||||
|
ErrorFileNotFound,
|
||||||
|
ErrorAborted,
|
||||||
|
ErrorInvalid,
|
||||||
|
ErrorEncrypted,
|
||||||
|
}
|
||||||
|
|
||||||
|
private void notifyInstallStatus(String filename, InstallStatus status) {
|
||||||
|
switch(status){
|
||||||
|
case Success:
|
||||||
|
mInstallStatusBuilder.setContentTitle(
|
||||||
|
mContext.getString(R.string.cia_install_notification_success_title));
|
||||||
|
mInstallStatusBuilder.setContentText(
|
||||||
|
mContext.getString(R.string.cia_install_success, filename));
|
||||||
|
break;
|
||||||
|
case ErrorAborted:
|
||||||
|
mInstallStatusBuilder.setContentTitle(
|
||||||
|
mContext.getString(R.string.cia_install_notification_error_title));
|
||||||
|
mInstallStatusBuilder.setStyle(new NotificationCompat.BigTextStyle()
|
||||||
|
.bigText(mContext.getString(
|
||||||
|
R.string.cia_install_error_aborted, filename)));
|
||||||
|
break;
|
||||||
|
case ErrorInvalid:
|
||||||
|
mInstallStatusBuilder.setContentTitle(
|
||||||
|
mContext.getString(R.string.cia_install_notification_error_title));
|
||||||
|
mInstallStatusBuilder.setContentText(
|
||||||
|
mContext.getString(R.string.cia_install_error_invalid, filename));
|
||||||
|
break;
|
||||||
|
case ErrorEncrypted:
|
||||||
|
mInstallStatusBuilder.setContentTitle(
|
||||||
|
mContext.getString(R.string.cia_install_notification_error_title));
|
||||||
|
mInstallStatusBuilder.setStyle(new NotificationCompat.BigTextStyle()
|
||||||
|
.bigText(mContext.getString(
|
||||||
|
R.string.cia_install_error_encrypted, filename)));
|
||||||
|
break;
|
||||||
|
case ErrorFailedToOpenFile:
|
||||||
|
// TODO:
|
||||||
|
case ErrorFileNotFound:
|
||||||
|
// shouldn't happen
|
||||||
|
default:
|
||||||
|
mInstallStatusBuilder.setContentTitle(
|
||||||
|
mContext.getString(R.string.cia_install_notification_error_title));
|
||||||
|
mInstallStatusBuilder.setStyle(new NotificationCompat.BigTextStyle()
|
||||||
|
.bigText(mContext.getString(R.string.cia_install_error_unknown, filename)));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// Even if newer versions of Android don't show the group summary text that you design,
|
||||||
|
// you always need to manually set a summary to enable grouped notifications.
|
||||||
|
mNotificationManager.notify(SUMMARY_NOTIFICATION_ID, mSummaryNotification);
|
||||||
|
mNotificationManager.notify(mStatusNotificationId++, mInstallStatusBuilder.build());
|
||||||
|
}
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public Result doWork() {
|
||||||
|
String[] selectedFiles = getInputData().getStringArray("CIA_FILES");
|
||||||
|
assert selectedFiles != null;
|
||||||
|
final CharSequence toastText = mContext.getResources().getQuantityString(R.plurals.cia_install_toast,
|
||||||
|
selectedFiles.length, selectedFiles.length);
|
||||||
|
|
||||||
|
getApplicationContext().getMainExecutor().execute(() -> Toast.makeText(mContext, toastText,
|
||||||
|
Toast.LENGTH_LONG).show());
|
||||||
|
|
||||||
|
// Issue the initial notification with zero progress
|
||||||
|
mInstallProgressBuilder.setOngoing(true);
|
||||||
|
setProgressCallback(100, 0);
|
||||||
|
|
||||||
|
int i = 0;
|
||||||
|
for (String file : selectedFiles) {
|
||||||
|
String filename = FileUtil.getFilename(mContext, file);
|
||||||
|
mInstallProgressBuilder.setContentText(mContext.getString(
|
||||||
|
R.string.cia_install_notification_installing, filename, ++i, selectedFiles.length));
|
||||||
|
InstallStatus res = InstallCIA(file);
|
||||||
|
notifyInstallStatus(filename, res);
|
||||||
|
}
|
||||||
|
mNotificationManager.cancel(PROGRESS_NOTIFICATION_ID);
|
||||||
|
|
||||||
|
return Result.success();
|
||||||
|
}
|
||||||
|
public void setProgressCallback(int max, int progress) {
|
||||||
|
long currentTime = System.currentTimeMillis();
|
||||||
|
// Android applies a rate limit when updating a notification.
|
||||||
|
// If you post updates to a single notification too frequently,
|
||||||
|
// such as many in less than one second, the system might drop updates.
|
||||||
|
// TODO: consider moving to C++ side
|
||||||
|
if (currentTime - mLastNotifiedTime < 500 /* ms */){
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
mLastNotifiedTime = currentTime;
|
||||||
|
mInstallProgressBuilder.setProgress(max, progress, false);
|
||||||
|
mNotificationManager.notify(PROGRESS_NOTIFICATION_ID, mInstallProgressBuilder.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public ForegroundInfo getForegroundInfo() {
|
||||||
|
return new ForegroundInfo(PROGRESS_NOTIFICATION_ID, mInstallProgressBuilder.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
private native InstallStatus InstallCIA(String path);
|
||||||
|
}
|
|
@ -2,7 +2,7 @@
|
||||||
// Licensed under GPLv2 or any later version
|
// Licensed under GPLv2 or any later version
|
||||||
// Refer to the license.txt file included.
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
package org.citra.citra_emu.disk_shader_cache;
|
package org.citra.citra_emu.utils;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.app.Dialog;
|
import android.app.Dialog;
|
|
@ -8,6 +8,7 @@
|
||||||
#include "common/logging/filter.h"
|
#include "common/logging/filter.h"
|
||||||
#include "common/logging/log.h"
|
#include "common/logging/log.h"
|
||||||
#include "common/settings.h"
|
#include "common/settings.h"
|
||||||
|
#include "core/hle/service/am/am.h"
|
||||||
#include "jni/applets/mii_selector.h"
|
#include "jni/applets/mii_selector.h"
|
||||||
#include "jni/applets/swkbd.h"
|
#include "jni/applets/swkbd.h"
|
||||||
#include "jni/camera/still_image_camera.h"
|
#include "jni/camera/still_image_camera.h"
|
||||||
|
@ -21,8 +22,6 @@ static JavaVM* s_java_vm;
|
||||||
|
|
||||||
static jclass s_core_error_class;
|
static jclass s_core_error_class;
|
||||||
static jclass s_savestate_info_class;
|
static jclass s_savestate_info_class;
|
||||||
static jclass s_disk_cache_progress_class;
|
|
||||||
static jclass s_load_callback_stage_class;
|
|
||||||
|
|
||||||
static jclass s_native_library_class;
|
static jclass s_native_library_class;
|
||||||
static jmethodID s_on_core_error;
|
static jmethodID s_on_core_error;
|
||||||
|
@ -34,7 +33,6 @@ static jmethodID s_landscape_screen_layout;
|
||||||
static jmethodID s_exit_emulation_activity;
|
static jmethodID s_exit_emulation_activity;
|
||||||
static jmethodID s_request_camera_permission;
|
static jmethodID s_request_camera_permission;
|
||||||
static jmethodID s_request_mic_permission;
|
static jmethodID s_request_mic_permission;
|
||||||
static jmethodID s_disk_cache_load_progress;
|
|
||||||
|
|
||||||
static jclass s_cheat_class;
|
static jclass s_cheat_class;
|
||||||
static jfieldID s_cheat_pointer;
|
static jfieldID s_cheat_pointer;
|
||||||
|
@ -44,8 +42,14 @@ static jfieldID s_cheat_engine_pointer;
|
||||||
|
|
||||||
static jfieldID s_game_info_pointer;
|
static jfieldID s_game_info_pointer;
|
||||||
|
|
||||||
|
static jclass s_disk_cache_progress_class;
|
||||||
|
static jmethodID s_disk_cache_load_progress;
|
||||||
static std::unordered_map<VideoCore::LoadCallbackStage, jobject> s_java_load_callback_stages;
|
static std::unordered_map<VideoCore::LoadCallbackStage, jobject> s_java_load_callback_stages;
|
||||||
|
|
||||||
|
static jclass s_cia_install_helper_class;
|
||||||
|
static jmethodID s_cia_install_helper_set_progress;
|
||||||
|
static std::unordered_map<Service::AM::InstallStatus, jobject> s_java_cia_install_status;
|
||||||
|
|
||||||
namespace IDCache {
|
namespace IDCache {
|
||||||
|
|
||||||
JNIEnv* GetEnvForThread() {
|
JNIEnv* GetEnvForThread() {
|
||||||
|
@ -75,14 +79,6 @@ jclass GetSavestateInfoClass() {
|
||||||
return s_savestate_info_class;
|
return s_savestate_info_class;
|
||||||
}
|
}
|
||||||
|
|
||||||
jclass GetDiskCacheProgressClass() {
|
|
||||||
return s_disk_cache_progress_class;
|
|
||||||
}
|
|
||||||
|
|
||||||
jclass GetDiskCacheLoadCallbackStageClass() {
|
|
||||||
return s_load_callback_stage_class;
|
|
||||||
}
|
|
||||||
|
|
||||||
jclass GetNativeLibraryClass() {
|
jclass GetNativeLibraryClass() {
|
||||||
return s_native_library_class;
|
return s_native_library_class;
|
||||||
}
|
}
|
||||||
|
@ -123,10 +119,6 @@ jmethodID GetRequestMicPermission() {
|
||||||
return s_request_mic_permission;
|
return s_request_mic_permission;
|
||||||
}
|
}
|
||||||
|
|
||||||
jmethodID GetDiskCacheLoadProgress() {
|
|
||||||
return s_disk_cache_load_progress;
|
|
||||||
}
|
|
||||||
|
|
||||||
jclass GetCheatClass() {
|
jclass GetCheatClass() {
|
||||||
return s_cheat_class;
|
return s_cheat_class;
|
||||||
}
|
}
|
||||||
|
@ -147,6 +139,14 @@ jfieldID GetGameInfoPointer() {
|
||||||
return s_game_info_pointer;
|
return s_game_info_pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
jclass GetDiskCacheProgressClass() {
|
||||||
|
return s_disk_cache_progress_class;
|
||||||
|
}
|
||||||
|
|
||||||
|
jmethodID GetDiskCacheLoadProgress() {
|
||||||
|
return s_disk_cache_load_progress;
|
||||||
|
}
|
||||||
|
|
||||||
jobject GetJavaLoadCallbackStage(VideoCore::LoadCallbackStage stage) {
|
jobject GetJavaLoadCallbackStage(VideoCore::LoadCallbackStage stage) {
|
||||||
const auto it = s_java_load_callback_stages.find(stage);
|
const auto it = s_java_load_callback_stages.find(stage);
|
||||||
ASSERT_MSG(it != s_java_load_callback_stages.end(), "Invalid LoadCallbackStage: {}", stage);
|
ASSERT_MSG(it != s_java_load_callback_stages.end(), "Invalid LoadCallbackStage: {}", stage);
|
||||||
|
@ -154,6 +154,19 @@ jobject GetJavaLoadCallbackStage(VideoCore::LoadCallbackStage stage) {
|
||||||
return it->second;
|
return it->second;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
jclass GetCiaInstallHelperClass() {
|
||||||
|
return s_cia_install_helper_class;
|
||||||
|
}
|
||||||
|
|
||||||
|
jmethodID GetCiaInstallHelperSetProgress() {
|
||||||
|
return s_cia_install_helper_set_progress;
|
||||||
|
}
|
||||||
|
jobject GetJavaCiaInstallStatus(Service::AM::InstallStatus status) {
|
||||||
|
const auto it = s_java_cia_install_status.find(status);
|
||||||
|
ASSERT_MSG(it != s_java_cia_install_status.end(), "Invalid InstallStatus: {}", status);
|
||||||
|
|
||||||
|
return it->second;
|
||||||
|
}
|
||||||
} // namespace IDCache
|
} // namespace IDCache
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
|
@ -178,10 +191,6 @@ jint JNI_OnLoad(JavaVM* vm, void* reserved) {
|
||||||
env->NewGlobalRef(env->FindClass("org/citra/citra_emu/NativeLibrary$SavestateInfo")));
|
env->NewGlobalRef(env->FindClass("org/citra/citra_emu/NativeLibrary$SavestateInfo")));
|
||||||
s_core_error_class = reinterpret_cast<jclass>(
|
s_core_error_class = reinterpret_cast<jclass>(
|
||||||
env->NewGlobalRef(env->FindClass("org/citra/citra_emu/NativeLibrary$CoreError")));
|
env->NewGlobalRef(env->FindClass("org/citra/citra_emu/NativeLibrary$CoreError")));
|
||||||
s_disk_cache_progress_class = reinterpret_cast<jclass>(env->NewGlobalRef(
|
|
||||||
env->FindClass("org/citra/citra_emu/disk_shader_cache/DiskShaderCacheProgress")));
|
|
||||||
s_load_callback_stage_class = reinterpret_cast<jclass>(env->NewGlobalRef(env->FindClass(
|
|
||||||
"org/citra/citra_emu/disk_shader_cache/DiskShaderCacheProgress$LoadCallbackStage")));
|
|
||||||
|
|
||||||
// Initialize NativeLibrary
|
// Initialize NativeLibrary
|
||||||
const jclass native_library_class = env->FindClass("org/citra/citra_emu/NativeLibrary");
|
const jclass native_library_class = env->FindClass("org/citra/citra_emu/NativeLibrary");
|
||||||
|
@ -205,9 +214,6 @@ jint JNI_OnLoad(JavaVM* vm, void* reserved) {
|
||||||
env->GetStaticMethodID(s_native_library_class, "RequestCameraPermission", "()Z");
|
env->GetStaticMethodID(s_native_library_class, "RequestCameraPermission", "()Z");
|
||||||
s_request_mic_permission =
|
s_request_mic_permission =
|
||||||
env->GetStaticMethodID(s_native_library_class, "RequestMicPermission", "()Z");
|
env->GetStaticMethodID(s_native_library_class, "RequestMicPermission", "()Z");
|
||||||
s_disk_cache_load_progress = env->GetStaticMethodID(
|
|
||||||
s_disk_cache_progress_class, "loadProgress",
|
|
||||||
"(Lorg/citra/citra_emu/disk_shader_cache/DiskShaderCacheProgress$LoadCallbackStage;II)V");
|
|
||||||
env->DeleteLocalRef(native_library_class);
|
env->DeleteLocalRef(native_library_class);
|
||||||
|
|
||||||
// Initialize Cheat
|
// Initialize Cheat
|
||||||
|
@ -228,16 +234,23 @@ jint JNI_OnLoad(JavaVM* vm, void* reserved) {
|
||||||
s_game_info_pointer = env->GetFieldID(game_info_class, "mPointer", "J");
|
s_game_info_pointer = env->GetFieldID(game_info_class, "mPointer", "J");
|
||||||
env->DeleteLocalRef(game_info_class);
|
env->DeleteLocalRef(game_info_class);
|
||||||
|
|
||||||
|
// Initialize Disk Shader Cache Progress Dialog
|
||||||
|
s_disk_cache_progress_class = reinterpret_cast<jclass>(
|
||||||
|
env->NewGlobalRef(env->FindClass("org/citra/citra_emu/utils/DiskShaderCacheProgress")));
|
||||||
|
jclass load_callback_stage_class =
|
||||||
|
env->FindClass("org/citra/citra_emu/utils/DiskShaderCacheProgress$LoadCallbackStage");
|
||||||
|
s_disk_cache_load_progress = env->GetStaticMethodID(
|
||||||
|
s_disk_cache_progress_class, "loadProgress",
|
||||||
|
"(Lorg/citra/citra_emu/utils/DiskShaderCacheProgress$LoadCallbackStage;II)V");
|
||||||
// Initialize LoadCallbackStage map
|
// Initialize LoadCallbackStage map
|
||||||
const auto to_java_load_callback_stage = [env](const std::string& stage) {
|
const auto to_java_load_callback_stage = [env,
|
||||||
jclass load_callback_stage_class = IDCache::GetDiskCacheLoadCallbackStageClass();
|
load_callback_stage_class](const std::string& stage) {
|
||||||
return env->NewGlobalRef(env->GetStaticObjectField(
|
return env->NewGlobalRef(env->GetStaticObjectField(
|
||||||
load_callback_stage_class,
|
load_callback_stage_class,
|
||||||
env->GetStaticFieldID(load_callback_stage_class, stage.c_str(),
|
env->GetStaticFieldID(load_callback_stage_class, stage.c_str(),
|
||||||
"Lorg/citra/citra_emu/disk_shader_cache/"
|
"Lorg/citra/citra_emu/utils/"
|
||||||
"DiskShaderCacheProgress$LoadCallbackStage;")));
|
"DiskShaderCacheProgress$LoadCallbackStage;")));
|
||||||
};
|
};
|
||||||
|
|
||||||
s_java_load_callback_stages.emplace(VideoCore::LoadCallbackStage::Prepare,
|
s_java_load_callback_stages.emplace(VideoCore::LoadCallbackStage::Prepare,
|
||||||
to_java_load_callback_stage("Prepare"));
|
to_java_load_callback_stage("Prepare"));
|
||||||
s_java_load_callback_stages.emplace(VideoCore::LoadCallbackStage::Decompile,
|
s_java_load_callback_stages.emplace(VideoCore::LoadCallbackStage::Decompile,
|
||||||
|
@ -246,6 +259,36 @@ jint JNI_OnLoad(JavaVM* vm, void* reserved) {
|
||||||
to_java_load_callback_stage("Build"));
|
to_java_load_callback_stage("Build"));
|
||||||
s_java_load_callback_stages.emplace(VideoCore::LoadCallbackStage::Complete,
|
s_java_load_callback_stages.emplace(VideoCore::LoadCallbackStage::Complete,
|
||||||
to_java_load_callback_stage("Complete"));
|
to_java_load_callback_stage("Complete"));
|
||||||
|
env->DeleteLocalRef(load_callback_stage_class);
|
||||||
|
|
||||||
|
// CIA Install
|
||||||
|
s_cia_install_helper_class = reinterpret_cast<jclass>(
|
||||||
|
env->NewGlobalRef(env->FindClass("org/citra/citra_emu/utils/CiaInstallWorker")));
|
||||||
|
s_cia_install_helper_set_progress =
|
||||||
|
env->GetMethodID(s_cia_install_helper_class, "setProgressCallback", "(II)V");
|
||||||
|
// Initialize CIA InstallStatus map
|
||||||
|
jclass cia_install_status_class =
|
||||||
|
env->FindClass("org/citra/citra_emu/utils/CiaInstallWorker$InstallStatus");
|
||||||
|
const auto to_java_cia_install_status = [env,
|
||||||
|
cia_install_status_class](const std::string& stage) {
|
||||||
|
return env->NewGlobalRef(env->GetStaticObjectField(
|
||||||
|
cia_install_status_class, env->GetStaticFieldID(cia_install_status_class, stage.c_str(),
|
||||||
|
"Lorg/citra/citra_emu/utils/"
|
||||||
|
"CiaInstallWorker$InstallStatus;")));
|
||||||
|
};
|
||||||
|
s_java_cia_install_status.emplace(Service::AM::InstallStatus::Success,
|
||||||
|
to_java_cia_install_status("Success"));
|
||||||
|
s_java_cia_install_status.emplace(Service::AM::InstallStatus::ErrorFailedToOpenFile,
|
||||||
|
to_java_cia_install_status("ErrorFailedToOpenFile"));
|
||||||
|
s_java_cia_install_status.emplace(Service::AM::InstallStatus::ErrorFileNotFound,
|
||||||
|
to_java_cia_install_status("ErrorFileNotFound"));
|
||||||
|
s_java_cia_install_status.emplace(Service::AM::InstallStatus::ErrorAborted,
|
||||||
|
to_java_cia_install_status("ErrorAborted"));
|
||||||
|
s_java_cia_install_status.emplace(Service::AM::InstallStatus::ErrorInvalid,
|
||||||
|
to_java_cia_install_status("ErrorInvalid"));
|
||||||
|
s_java_cia_install_status.emplace(Service::AM::InstallStatus::ErrorEncrypted,
|
||||||
|
to_java_cia_install_status("ErrorEncrypted"));
|
||||||
|
env->DeleteLocalRef(cia_install_status_class);
|
||||||
|
|
||||||
MiiSelector::InitJNI(env);
|
MiiSelector::InitJNI(env);
|
||||||
SoftwareKeyboard::InitJNI(env);
|
SoftwareKeyboard::InitJNI(env);
|
||||||
|
@ -264,14 +307,18 @@ void JNI_OnUnload(JavaVM* vm, void* reserved) {
|
||||||
env->DeleteGlobalRef(s_savestate_info_class);
|
env->DeleteGlobalRef(s_savestate_info_class);
|
||||||
env->DeleteGlobalRef(s_core_error_class);
|
env->DeleteGlobalRef(s_core_error_class);
|
||||||
env->DeleteGlobalRef(s_disk_cache_progress_class);
|
env->DeleteGlobalRef(s_disk_cache_progress_class);
|
||||||
env->DeleteGlobalRef(s_load_callback_stage_class);
|
|
||||||
env->DeleteGlobalRef(s_native_library_class);
|
env->DeleteGlobalRef(s_native_library_class);
|
||||||
env->DeleteGlobalRef(s_cheat_class);
|
env->DeleteGlobalRef(s_cheat_class);
|
||||||
|
env->DeleteGlobalRef(s_cia_install_helper_class);
|
||||||
|
|
||||||
for (auto& [key, object] : s_java_load_callback_stages) {
|
for (auto& [key, object] : s_java_load_callback_stages) {
|
||||||
env->DeleteGlobalRef(object);
|
env->DeleteGlobalRef(object);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (auto& [key, object] : s_java_cia_install_status) {
|
||||||
|
env->DeleteGlobalRef(object);
|
||||||
|
}
|
||||||
|
|
||||||
MiiSelector::CleanupJNI(env);
|
MiiSelector::CleanupJNI(env);
|
||||||
SoftwareKeyboard::CleanupJNI(env);
|
SoftwareKeyboard::CleanupJNI(env);
|
||||||
Camera::StillImage::CleanupJNI(env);
|
Camera::StillImage::CleanupJNI(env);
|
||||||
|
|
|
@ -9,14 +9,16 @@
|
||||||
#include <jni.h>
|
#include <jni.h>
|
||||||
#include "video_core/rasterizer_interface.h"
|
#include "video_core/rasterizer_interface.h"
|
||||||
|
|
||||||
|
namespace Service::AM {
|
||||||
|
enum class InstallStatus : u32;
|
||||||
|
} // namespace Service::AM
|
||||||
|
|
||||||
namespace IDCache {
|
namespace IDCache {
|
||||||
|
|
||||||
JNIEnv* GetEnvForThread();
|
JNIEnv* GetEnvForThread();
|
||||||
|
|
||||||
jclass GetCoreErrorClass();
|
jclass GetCoreErrorClass();
|
||||||
jclass GetSavestateInfoClass();
|
jclass GetSavestateInfoClass();
|
||||||
jclass GetDiskCacheProgressClass();
|
|
||||||
jclass GetDiskCacheLoadCallbackStageClass();
|
|
||||||
|
|
||||||
jclass GetNativeLibraryClass();
|
jclass GetNativeLibraryClass();
|
||||||
jmethodID GetOnCoreError();
|
jmethodID GetOnCoreError();
|
||||||
|
@ -28,7 +30,6 @@ jmethodID GetLandscapeScreenLayout();
|
||||||
jmethodID GetExitEmulationActivity();
|
jmethodID GetExitEmulationActivity();
|
||||||
jmethodID GetRequestCameraPermission();
|
jmethodID GetRequestCameraPermission();
|
||||||
jmethodID GetRequestMicPermission();
|
jmethodID GetRequestMicPermission();
|
||||||
jmethodID GetDiskCacheLoadProgress();
|
|
||||||
|
|
||||||
jclass GetCheatClass();
|
jclass GetCheatClass();
|
||||||
jfieldID GetCheatPointer();
|
jfieldID GetCheatPointer();
|
||||||
|
@ -38,8 +39,14 @@ jfieldID GetCheatEnginePointer();
|
||||||
|
|
||||||
jfieldID GetGameInfoPointer();
|
jfieldID GetGameInfoPointer();
|
||||||
|
|
||||||
|
jclass GetDiskCacheProgressClass();
|
||||||
|
jmethodID GetDiskCacheLoadProgress();
|
||||||
jobject GetJavaLoadCallbackStage(VideoCore::LoadCallbackStage stage);
|
jobject GetJavaLoadCallbackStage(VideoCore::LoadCallbackStage stage);
|
||||||
|
|
||||||
|
jclass GetCiaInstallHelperClass();
|
||||||
|
jmethodID GetCiaInstallHelperSetProgress();
|
||||||
|
jobject GetJavaCiaInstallStatus(Service::AM::InstallStatus status);
|
||||||
|
|
||||||
} // namespace IDCache
|
} // namespace IDCache
|
||||||
|
|
||||||
template <typename T = jobject>
|
template <typename T = jobject>
|
||||||
|
|
|
@ -625,29 +625,16 @@ void Java_org_citra_citra_1emu_NativeLibrary_RemoveAmiibo(JNIEnv* env, jclass cl
|
||||||
nfc->RemoveAmiibo();
|
nfc->RemoveAmiibo();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Java_org_citra_citra_1emu_NativeLibrary_InstallCIAS(JNIEnv* env, [[maybe_unused]] jclass clazz,
|
JNIEXPORT jobject JNICALL Java_org_citra_citra_1emu_utils_CiaInstallWorker_InstallCIA(
|
||||||
jobjectArray path) {
|
JNIEnv* env, jobject jobj, jstring jpath) {
|
||||||
const jsize count{env->GetArrayLength(path)};
|
std::string path = GetJString(env, jpath);
|
||||||
std::vector<std::string> paths;
|
Service::AM::InstallStatus res =
|
||||||
paths.reserve(count);
|
Service::AM::InstallCIA(path, [env, jobj](size_t total_bytes_read, size_t file_size) {
|
||||||
for (jsize idx{0}; idx < count; ++idx) {
|
env->CallVoidMethod(jobj, IDCache::GetCiaInstallHelperSetProgress(),
|
||||||
paths.emplace_back(
|
static_cast<jint>(file_size), static_cast<jint>(total_bytes_read));
|
||||||
GetJString(env, static_cast<jstring>(env->GetObjectArrayElement(path, idx))));
|
});
|
||||||
}
|
|
||||||
std::atomic<jsize> idx{count};
|
return IDCache::GetJavaCiaInstallStatus(res);
|
||||||
std::vector<std::thread> threads;
|
|
||||||
std::generate_n(std::back_inserter(threads),
|
|
||||||
std::min<jsize>(std::thread::hardware_concurrency(), count), [&] {
|
|
||||||
return std::thread{[&idx, &paths, env] {
|
|
||||||
jsize work_idx;
|
|
||||||
while ((work_idx = --idx) >= 0) {
|
|
||||||
LOG_INFO(Frontend, "Installing CIA {}", work_idx);
|
|
||||||
Service::AM::InstallCIA(paths[work_idx]);
|
|
||||||
}
|
|
||||||
}};
|
|
||||||
});
|
|
||||||
for (auto& thread : threads)
|
|
||||||
thread.join();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
jobjectArray Java_org_citra_citra_1emu_NativeLibrary_GetSavestateInfo(
|
jobjectArray Java_org_citra_citra_1emu_NativeLibrary_GetSavestateInfo(
|
||||||
|
|
|
@ -146,10 +146,6 @@ JNIEXPORT jboolean Java_org_citra_citra_1emu_NativeLibrary_LoadAmiibo(JNIEnv* en
|
||||||
|
|
||||||
JNIEXPORT void Java_org_citra_citra_1emu_NativeLibrary_RemoveAmiibo(JNIEnv* env, jclass clazz);
|
JNIEXPORT void Java_org_citra_citra_1emu_NativeLibrary_RemoveAmiibo(JNIEnv* env, jclass clazz);
|
||||||
|
|
||||||
JNIEXPORT void JNICALL Java_org_citra_citra_1emu_NativeLibrary_InstallCIAS(JNIEnv* env,
|
|
||||||
jclass clazz,
|
|
||||||
jobjectArray path);
|
|
||||||
|
|
||||||
JNIEXPORT jobjectArray JNICALL
|
JNIEXPORT jobjectArray JNICALL
|
||||||
Java_org_citra_citra_1emu_NativeLibrary_GetSavestateInfo(JNIEnv* env, jclass clazz);
|
Java_org_citra_citra_1emu_NativeLibrary_GetSavestateInfo(JNIEnv* env, jclass clazz);
|
||||||
|
|
||||||
|
@ -162,6 +158,10 @@ JNIEXPORT void JNICALL Java_org_citra_citra_1emu_NativeLibrary_LoadState(JNIEnv*
|
||||||
JNIEXPORT void JNICALL Java_org_citra_citra_1emu_NativeLibrary_LogDeviceInfo(JNIEnv* env,
|
JNIEXPORT void JNICALL Java_org_citra_citra_1emu_NativeLibrary_LogDeviceInfo(JNIEnv* env,
|
||||||
jclass clazz);
|
jclass clazz);
|
||||||
|
|
||||||
|
JNIEXPORT jobject JNICALL Java_org_citra_citra_1emu_NativeLibrary_InstallCIA(JNIEnv* env,
|
||||||
|
jclass clazz,
|
||||||
|
jstring file);
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -262,4 +262,22 @@
|
||||||
<string name="cheats_error_no_name">Name can\'t be empty</string>
|
<string name="cheats_error_no_name">Name can\'t be empty</string>
|
||||||
<string name="cheats_error_no_code_lines">Code can\'t be empty</string>
|
<string name="cheats_error_no_code_lines">Code can\'t be empty</string>
|
||||||
<string name="cheats_error_on_line">Error on line %1$d</string>
|
<string name="cheats_error_on_line">Error on line %1$d</string>
|
||||||
|
|
||||||
|
<!-- CIA Install -->
|
||||||
|
<plurals name="cia_install_toast">
|
||||||
|
<item quantity="one">Installing %d file. See notification for more details.</item>
|
||||||
|
<item quantity="other">Installing %d files. See notification for more details.</item>
|
||||||
|
</plurals>
|
||||||
|
<string name="cia_install_notification_channel_name" translatable="false">Citra CIA Install</string>
|
||||||
|
<string name="cia_install_notification_channel_id" translatable="false">citra-cia</string>
|
||||||
|
<string name="cia_install_notification_channel_description">Citra notifications during CIA Install</string>
|
||||||
|
<string name="cia_install_notification_title">Installing CIA</string>
|
||||||
|
<string name="cia_install_notification_installing">Installing %s (%d/%d)</string>
|
||||||
|
<string name="cia_install_notification_success_title">Successfully installed CIA</string>
|
||||||
|
<string name="cia_install_notification_error_title">Failed to install CIA</string>
|
||||||
|
<string name="cia_install_success">\"%s\" has been installed successfully</string>
|
||||||
|
<string name="cia_install_error_aborted">The installation of \"%s\" was aborted.\n Please see the log for more details</string>
|
||||||
|
<string name="cia_install_error_invalid">\"%s\" is not a valid CIA</string>
|
||||||
|
<string name="cia_install_error_encrypted">\"%s\" must be decrypted before being used with Citra.\n A real 3DS is required</string>
|
||||||
|
<string name="cia_install_error_unknown">An unknown error occurred while installing \"%s\".\n Please see the log for more details</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -8,7 +8,7 @@ buildscript {
|
||||||
jcenter()
|
jcenter()
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:7.4.2'
|
classpath 'com.android.tools.build:gradle:8.0.2'
|
||||||
|
|
||||||
// NOTE: Do not place your application dependencies here; they belong
|
// NOTE: Do not place your application dependencies here; they belong
|
||||||
// in the individual module build.gradle files
|
// in the individual module build.gradle files
|
||||||
|
|
|
@ -14,3 +14,6 @@ org.gradle.jvmargs=-Xmx1536m
|
||||||
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||||
# org.gradle.parallel=true
|
# org.gradle.parallel=true
|
||||||
android.native.buildOutput=verbose
|
android.native.buildOutput=verbose
|
||||||
|
android.defaults.buildfeatures.buildconfig=true
|
||||||
|
android.nonTransitiveRClass=false
|
||||||
|
android.nonFinalResIds=false
|
||||||
|
|
|
@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip
|
||||||
|
|
Reference in New Issue