diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b5e3d7c894..54040b8e93 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -198,7 +198,7 @@ jobs: sudo apt-get update -y sudo apt-get install ccache glslang-dev glslang-tools apksigner -y - name: Build - run: ./.ci/android/build.sh + run: JAVA_HOME=$JAVA_HOME_17_X64 ./.ci/android/build.sh - name: Copy and sign artifacts env: ANDROID_KEYSTORE_B64: ${{ secrets.ANDROID_KEYSTORE_B64 }} diff --git a/src/android/app/build.gradle b/src/android/app/build.gradle index 803f8562c1..1010c40e45 100644 --- a/src/android/app/build.gradle +++ b/src/android/app/build.gradle @@ -10,7 +10,7 @@ def buildType def abiFilter = "arm64-v8a" //, "x86" android { - compileSdkVersion 32 + compileSdkVersion 33 ndkVersion "25.1.8937393" compileOptions { @@ -100,6 +100,7 @@ android { path "../../../CMakeLists.txt" } } + namespace 'org.citra.citra_emu' defaultConfig { externalNativeBuild { @@ -129,6 +130,7 @@ dependencies { implementation "androidx.slidingpanelayout:slidingpanelayout:1.2.0" implementation 'com.google.android.material:material:1.6.1' implementation 'androidx.core:core-splashscreen:1.0.0' + implementation "androidx.work:work-runtime:2.8.1" // For loading huge screenshots from the disk. implementation 'com.squareup.picasso:picasso:2.71828' diff --git a/src/android/app/src/main/java/org/citra/citra_emu/CitraApplication.java b/src/android/app/src/main/java/org/citra/citra_emu/CitraApplication.java index 638e1ebaa2..b572260706 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/CitraApplication.java +++ b/src/android/app/src/main/java/org/citra/citra_emu/CitraApplication.java @@ -23,16 +23,33 @@ public class CitraApplication extends Application { private void createNotificationChannel() { // Create the NotificationChannel, but only on API 26+ because // 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); 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.setSound(null, null); channel.setVibrationPattern(null); - // Register the channel with the system; you can't change the importance - // or other notification behaviors after this - NotificationManager notificationManager = getSystemService(NotificationManager.class); + + notificationManager.createNotificationChannel(channel); + } + { + // 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); } } diff --git a/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.java b/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.java index 4af8b5c8fe..91c9f5be7e 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.java +++ b/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.java @@ -589,8 +589,6 @@ public final class NativeLibrary { public static native void RemoveAmiibo(); - public static native void InstallCIAS(String[] path); - public static final int SAVESTATE_SLOT_COUNT = 10; public static final class SavestateInfo { diff --git a/src/android/app/src/main/java/org/citra/citra_emu/ui/main/MainActivity.java b/src/android/app/src/main/java/org/citra/citra_emu/ui/main/MainActivity.java index c9b28daf9b..68440f9163 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/ui/main/MainActivity.java +++ b/src/android/app/src/main/java/org/citra/citra_emu/ui/main/MainActivity.java @@ -20,10 +20,15 @@ import androidx.core.graphics.Insets; import androidx.core.view.ViewCompat; import androidx.core.view.WindowCompat; 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 org.citra.citra_emu.NativeLibrary; import org.citra.citra_emu.R; import org.citra.citra_emu.activities.EmulationActivity; 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.utils.AddDirectoryHelper; 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.DirectoryInitialization; import org.citra.citra_emu.utils.FileBrowserHelper; @@ -50,7 +56,9 @@ public final class MainActivity extends AppCompatActivity implements MainView { private int mFrameLayoutId; 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 private static BillingManager mBillingManager; @@ -91,7 +99,7 @@ public final class MainActivity extends AppCompatActivity implements MainView { mPresenter.onDirectorySelected(result.toString()); }); - private final ActivityResultLauncher mOpenFileLauncher = + private final ActivityResultLauncher mInstallCiaFileLauncher = registerForActivityResult(new OpenFileResultContract(), result -> { if (result == null) return; @@ -104,8 +112,16 @@ public final class MainActivity extends AppCompatActivity implements MainView { .show(); return; } - NativeLibrary.InstallCIAS(selectedFiles); - mPresenter.refreshGameList(); + WorkManager workManager = WorkManager.getInstance(getApplicationContext()); + 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 @@ -233,7 +249,7 @@ public final class MainActivity extends AppCompatActivity implements MainView { mOpenGameListLauncher.launch(null); break; case MainPresenter.REQUEST_INSTALL_CIA: - mOpenFileLauncher.launch(true); + mInstallCiaFileLauncher.launch(true); break; } } else { diff --git a/src/android/app/src/main/java/org/citra/citra_emu/utils/CiaInstallWorker.java b/src/android/app/src/main/java/org/citra/citra_emu/utils/CiaInstallWorker.java new file mode 100644 index 0000000000..2f7ca66c2e --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/utils/CiaInstallWorker.java @@ -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); +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/disk_shader_cache/DiskShaderCacheProgress.java b/src/android/app/src/main/java/org/citra/citra_emu/utils/DiskShaderCacheProgress.java similarity index 99% rename from src/android/app/src/main/java/org/citra/citra_emu/disk_shader_cache/DiskShaderCacheProgress.java rename to src/android/app/src/main/java/org/citra/citra_emu/utils/DiskShaderCacheProgress.java index ceaacc12a2..5278752498 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/disk_shader_cache/DiskShaderCacheProgress.java +++ b/src/android/app/src/main/java/org/citra/citra_emu/utils/DiskShaderCacheProgress.java @@ -2,7 +2,7 @@ // Licensed under GPLv2 or any later version // 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.Dialog; diff --git a/src/android/app/src/main/jni/id_cache.cpp b/src/android/app/src/main/jni/id_cache.cpp index c34b019172..c2aa048a86 100644 --- a/src/android/app/src/main/jni/id_cache.cpp +++ b/src/android/app/src/main/jni/id_cache.cpp @@ -8,6 +8,7 @@ #include "common/logging/filter.h" #include "common/logging/log.h" #include "common/settings.h" +#include "core/hle/service/am/am.h" #include "jni/applets/mii_selector.h" #include "jni/applets/swkbd.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_savestate_info_class; -static jclass s_disk_cache_progress_class; -static jclass s_load_callback_stage_class; static jclass s_native_library_class; 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_request_camera_permission; static jmethodID s_request_mic_permission; -static jmethodID s_disk_cache_load_progress; static jclass s_cheat_class; static jfieldID s_cheat_pointer; @@ -44,8 +42,14 @@ static jfieldID s_cheat_engine_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 s_java_load_callback_stages; +static jclass s_cia_install_helper_class; +static jmethodID s_cia_install_helper_set_progress; +static std::unordered_map s_java_cia_install_status; + namespace IDCache { JNIEnv* GetEnvForThread() { @@ -75,14 +79,6 @@ jclass GetSavestateInfoClass() { return s_savestate_info_class; } -jclass GetDiskCacheProgressClass() { - return s_disk_cache_progress_class; -} - -jclass GetDiskCacheLoadCallbackStageClass() { - return s_load_callback_stage_class; -} - jclass GetNativeLibraryClass() { return s_native_library_class; } @@ -123,10 +119,6 @@ jmethodID GetRequestMicPermission() { return s_request_mic_permission; } -jmethodID GetDiskCacheLoadProgress() { - return s_disk_cache_load_progress; -} - jclass GetCheatClass() { return s_cheat_class; } @@ -147,6 +139,14 @@ jfieldID GetGameInfoPointer() { 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) { const auto it = s_java_load_callback_stages.find(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; } +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 #ifdef __cplusplus @@ -178,10 +191,6 @@ jint JNI_OnLoad(JavaVM* vm, void* reserved) { env->NewGlobalRef(env->FindClass("org/citra/citra_emu/NativeLibrary$SavestateInfo"))); s_core_error_class = reinterpret_cast( env->NewGlobalRef(env->FindClass("org/citra/citra_emu/NativeLibrary$CoreError"))); - s_disk_cache_progress_class = reinterpret_cast(env->NewGlobalRef( - env->FindClass("org/citra/citra_emu/disk_shader_cache/DiskShaderCacheProgress"))); - s_load_callback_stage_class = reinterpret_cast(env->NewGlobalRef(env->FindClass( - "org/citra/citra_emu/disk_shader_cache/DiskShaderCacheProgress$LoadCallbackStage"))); // Initialize 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"); s_request_mic_permission = 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); // Initialize Cheat @@ -228,16 +234,23 @@ jint JNI_OnLoad(JavaVM* vm, void* reserved) { s_game_info_pointer = env->GetFieldID(game_info_class, "mPointer", "J"); env->DeleteLocalRef(game_info_class); + // Initialize Disk Shader Cache Progress Dialog + s_disk_cache_progress_class = reinterpret_cast( + 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 - const auto to_java_load_callback_stage = [env](const std::string& stage) { - jclass load_callback_stage_class = IDCache::GetDiskCacheLoadCallbackStageClass(); + const auto to_java_load_callback_stage = [env, + load_callback_stage_class](const std::string& stage) { return env->NewGlobalRef(env->GetStaticObjectField( load_callback_stage_class, env->GetStaticFieldID(load_callback_stage_class, stage.c_str(), - "Lorg/citra/citra_emu/disk_shader_cache/" + "Lorg/citra/citra_emu/utils/" "DiskShaderCacheProgress$LoadCallbackStage;"))); }; - s_java_load_callback_stages.emplace(VideoCore::LoadCallbackStage::Prepare, to_java_load_callback_stage("Prepare")); 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")); s_java_load_callback_stages.emplace(VideoCore::LoadCallbackStage::Complete, to_java_load_callback_stage("Complete")); + env->DeleteLocalRef(load_callback_stage_class); + + // CIA Install + s_cia_install_helper_class = reinterpret_cast( + 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); SoftwareKeyboard::InitJNI(env); @@ -264,14 +307,18 @@ void JNI_OnUnload(JavaVM* vm, void* reserved) { env->DeleteGlobalRef(s_savestate_info_class); env->DeleteGlobalRef(s_core_error_class); env->DeleteGlobalRef(s_disk_cache_progress_class); - env->DeleteGlobalRef(s_load_callback_stage_class); env->DeleteGlobalRef(s_native_library_class); env->DeleteGlobalRef(s_cheat_class); + env->DeleteGlobalRef(s_cia_install_helper_class); for (auto& [key, object] : s_java_load_callback_stages) { env->DeleteGlobalRef(object); } + for (auto& [key, object] : s_java_cia_install_status) { + env->DeleteGlobalRef(object); + } + MiiSelector::CleanupJNI(env); SoftwareKeyboard::CleanupJNI(env); Camera::StillImage::CleanupJNI(env); diff --git a/src/android/app/src/main/jni/id_cache.h b/src/android/app/src/main/jni/id_cache.h index e57496faef..3e6d3eb930 100644 --- a/src/android/app/src/main/jni/id_cache.h +++ b/src/android/app/src/main/jni/id_cache.h @@ -9,14 +9,16 @@ #include #include "video_core/rasterizer_interface.h" +namespace Service::AM { +enum class InstallStatus : u32; +} // namespace Service::AM + namespace IDCache { JNIEnv* GetEnvForThread(); jclass GetCoreErrorClass(); jclass GetSavestateInfoClass(); -jclass GetDiskCacheProgressClass(); -jclass GetDiskCacheLoadCallbackStageClass(); jclass GetNativeLibraryClass(); jmethodID GetOnCoreError(); @@ -28,7 +30,6 @@ jmethodID GetLandscapeScreenLayout(); jmethodID GetExitEmulationActivity(); jmethodID GetRequestCameraPermission(); jmethodID GetRequestMicPermission(); -jmethodID GetDiskCacheLoadProgress(); jclass GetCheatClass(); jfieldID GetCheatPointer(); @@ -38,8 +39,14 @@ jfieldID GetCheatEnginePointer(); jfieldID GetGameInfoPointer(); +jclass GetDiskCacheProgressClass(); +jmethodID GetDiskCacheLoadProgress(); jobject GetJavaLoadCallbackStage(VideoCore::LoadCallbackStage stage); +jclass GetCiaInstallHelperClass(); +jmethodID GetCiaInstallHelperSetProgress(); +jobject GetJavaCiaInstallStatus(Service::AM::InstallStatus status); + } // namespace IDCache template diff --git a/src/android/app/src/main/jni/native.cpp b/src/android/app/src/main/jni/native.cpp index 0c1620ec63..91728029af 100644 --- a/src/android/app/src/main/jni/native.cpp +++ b/src/android/app/src/main/jni/native.cpp @@ -625,29 +625,16 @@ void Java_org_citra_citra_1emu_NativeLibrary_RemoveAmiibo(JNIEnv* env, jclass cl nfc->RemoveAmiibo(); } -void Java_org_citra_citra_1emu_NativeLibrary_InstallCIAS(JNIEnv* env, [[maybe_unused]] jclass clazz, - jobjectArray path) { - const jsize count{env->GetArrayLength(path)}; - std::vector paths; - paths.reserve(count); - for (jsize idx{0}; idx < count; ++idx) { - paths.emplace_back( - GetJString(env, static_cast(env->GetObjectArrayElement(path, idx)))); - } - std::atomic idx{count}; - std::vector threads; - std::generate_n(std::back_inserter(threads), - std::min(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(); +JNIEXPORT jobject JNICALL Java_org_citra_citra_1emu_utils_CiaInstallWorker_InstallCIA( + JNIEnv* env, jobject jobj, jstring jpath) { + std::string path = GetJString(env, jpath); + Service::AM::InstallStatus res = + Service::AM::InstallCIA(path, [env, jobj](size_t total_bytes_read, size_t file_size) { + env->CallVoidMethod(jobj, IDCache::GetCiaInstallHelperSetProgress(), + static_cast(file_size), static_cast(total_bytes_read)); + }); + + return IDCache::GetJavaCiaInstallStatus(res); } jobjectArray Java_org_citra_citra_1emu_NativeLibrary_GetSavestateInfo( diff --git a/src/android/app/src/main/jni/native.h b/src/android/app/src/main/jni/native.h index a2156a4e4a..30f0126533 100644 --- a/src/android/app/src/main/jni/native.h +++ b/src/android/app/src/main/jni/native.h @@ -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 JNICALL Java_org_citra_citra_1emu_NativeLibrary_InstallCIAS(JNIEnv* env, - jclass clazz, - jobjectArray path); - JNIEXPORT jobjectArray JNICALL 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, jclass clazz); +JNIEXPORT jobject JNICALL Java_org_citra_citra_1emu_NativeLibrary_InstallCIA(JNIEnv* env, + jclass clazz, + jstring file); + #ifdef __cplusplus } #endif diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml index 03c403ece5..b8c6776646 100644 --- a/src/android/app/src/main/res/values/strings.xml +++ b/src/android/app/src/main/res/values/strings.xml @@ -262,4 +262,22 @@ Name can\'t be empty Code can\'t be empty Error on line %1$d + + + + Installing %d file. See notification for more details. + Installing %d files. See notification for more details. + + Citra CIA Install + citra-cia + Citra notifications during CIA Install + Installing CIA + Installing %s (%d/%d) + Successfully installed CIA + Failed to install CIA + \"%s\" has been installed successfully + The installation of \"%s\" was aborted.\n Please see the log for more details + \"%s\" is not a valid CIA + \"%s\" must be decrypted before being used with Citra.\n A real 3DS is required + An unknown error occurred while installing \"%s\".\n Please see the log for more details diff --git a/src/android/build.gradle b/src/android/build.gradle index 0290f416d9..cfece10624 100644 --- a/src/android/build.gradle +++ b/src/android/build.gradle @@ -8,7 +8,7 @@ buildscript { jcenter() } 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 // in the individual module build.gradle files diff --git a/src/android/gradle.properties b/src/android/gradle.properties index cbb503141d..01f4d78b62 100644 --- a/src/android/gradle.properties +++ b/src/android/gradle.properties @@ -14,3 +14,6 @@ org.gradle.jvmargs=-Xmx1536m # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # org.gradle.parallel=true android.native.buildOutput=verbose +android.defaults.buildfeatures.buildconfig=true +android.nonTransitiveRClass=false +android.nonFinalResIds=false diff --git a/src/android/gradle/wrapper/gradle-wrapper.properties b/src/android/gradle/wrapper/gradle-wrapper.properties index bd08889e59..fbd7c4f4fe 100644 --- a/src/android/gradle/wrapper/gradle-wrapper.properties +++ b/src/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME 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