mirror of
				https://github.com/PabloMK7/citra.git
				synced 2025-10-31 13:50:03 +00:00 
			
		
		
		
	Android: Offload CIA installation to background thread
This commit is contained in:
		
							parent
							
								
									ccb2a7cbea
								
							
						
					
					
						commit
						f5bb17c82e
					
				
					 10 changed files with 313 additions and 47 deletions
				
			
		|  | @ -129,6 +129,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' | ||||
|  |  | |||
|  | @ -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); | ||||
|         } | ||||
|     } | ||||
|  |  | |||
|  | @ -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 { | ||||
|  |  | |||
|  | @ -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<Boolean> mOpenFileLauncher = | ||||
|     private final ActivityResultLauncher<Boolean> 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 { | ||||
|  |  | |||
|  | @ -0,0 +1,167 @@ | |||
| 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.os.Handler; | ||||
| import android.os.Looper; | ||||
| 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 com.google.common.util.concurrent.ListenableFuture; | ||||
| 
 | ||||
| import org.citra.citra_emu.CitraApplication; | ||||
| import org.citra.citra_emu.R; | ||||
| import org.citra.citra_emu.ui.main.MainActivity; | ||||
| 
 | ||||
| import java.util.concurrent.ExecutorService; | ||||
| import java.util.concurrent.Executors; | ||||
| 
 | ||||
| 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 int mStatusNotificationId = 0xC1A0001; | ||||
| 
 | ||||
|     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(0xC1A0000, 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(0xC1A); | ||||
| 
 | ||||
|         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(0xC1A, mInstallProgressBuilder.build()); | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     @Override | ||||
|     public ForegroundInfo getForegroundInfo() { | ||||
|         return new ForegroundInfo(0xC1A, mInstallProgressBuilder.build()); | ||||
|     } | ||||
| 
 | ||||
|     private native InstallStatus InstallCIA(String path); | ||||
| } | ||||
|  | @ -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" | ||||
|  | @ -45,6 +46,10 @@ 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 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 { | ||||
| 
 | ||||
| JNIEnv* GetEnvForThread() { | ||||
|  | @ -149,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 | ||||
|  | @ -217,15 +235,16 @@ jint JNI_OnLoad(JavaVM* vm, void* reserved) { | |||
|     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_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"); | ||||
|         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, load_callback_stage_class](const std::string& stage) { | ||||
|     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(), | ||||
|  | @ -241,6 +260,36 @@ jint JNI_OnLoad(JavaVM* vm, void* reserved) { | |||
|     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<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); | ||||
|     SoftwareKeyboard::InitJNI(env); | ||||
|     Camera::StillImage::InitJNI(env); | ||||
|  | @ -260,11 +309,16 @@ void JNI_OnUnload(JavaVM* vm, void* reserved) { | |||
|     env->DeleteGlobalRef(s_disk_cache_progress_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); | ||||
|  |  | |||
|  | @ -9,6 +9,10 @@ | |||
| #include <jni.h> | ||||
| #include "video_core/rasterizer_interface.h" | ||||
| 
 | ||||
| namespace Service::AM { | ||||
| enum class InstallStatus : u32; | ||||
| } // namespace Service::AM
 | ||||
| 
 | ||||
| namespace IDCache { | ||||
| 
 | ||||
| JNIEnv* GetEnvForThread(); | ||||
|  | @ -39,6 +43,10 @@ jclass GetDiskCacheProgressClass(); | |||
| jmethodID GetDiskCacheLoadProgress(); | ||||
| jobject GetJavaLoadCallbackStage(VideoCore::LoadCallbackStage stage); | ||||
| 
 | ||||
| jclass GetCiaInstallHelperClass(); | ||||
| jmethodID GetCiaInstallHelperSetProgress(); | ||||
| jobject GetJavaCiaInstallStatus(Service::AM::InstallStatus status); | ||||
| 
 | ||||
| } // namespace IDCache
 | ||||
| 
 | ||||
| template <typename T = jobject> | ||||
|  |  | |||
|  | @ -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<std::string> paths; | ||||
|     paths.reserve(count); | ||||
|     for (jsize idx{0}; idx < count; ++idx) { | ||||
|         paths.emplace_back( | ||||
|             GetJString(env, static_cast<jstring>(env->GetObjectArrayElement(path, idx)))); | ||||
|     } | ||||
|     std::atomic<jsize> idx{count}; | ||||
|     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(); | ||||
| 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<jint>(file_size), static_cast<jint>(total_bytes_read)); | ||||
|         }); | ||||
| 
 | ||||
|     return IDCache::GetJavaCiaInstallStatus(res); | ||||
| } | ||||
| 
 | ||||
| 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 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 | ||||
|  |  | |||
|  | @ -262,4 +262,22 @@ | |||
|     <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_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> | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue