Android: Offload CIA installation to background thread

This commit is contained in:
SachinVin 2023-05-01 17:51:01 +05:30
parent ccb2a7cbea
commit f5bb17c82e
10 changed files with 313 additions and 47 deletions

View file

@ -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'

View file

@ -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);
}
}

View file

@ -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 {

View file

@ -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 {

View file

@ -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);
}

View file

@ -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);

View file

@ -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>

View file

@ -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(

View file

@ -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

View file

@ -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>