android: frontend: Integrate key installation for SAF.

This commit is contained in:
bunnei 2023-02-04 00:55:02 -08:00
parent 63a98e3e1c
commit 93cf8c3090
20 changed files with 102 additions and 21 deletions

View File

@ -6,18 +6,22 @@ import android.os.Bundle;
import android.view.Menu; import android.view.Menu;
import android.view.MenuInflater; import android.view.MenuInflater;
import android.view.MenuItem; import android.view.MenuItem;
import android.widget.Toast;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar; import androidx.appcompat.widget.Toolbar;
import org.yuzu.yuzu_emu.NativeLibrary;
import org.yuzu.yuzu_emu.R; import org.yuzu.yuzu_emu.R;
import org.yuzu.yuzu_emu.activities.EmulationActivity; import org.yuzu.yuzu_emu.activities.EmulationActivity;
import org.yuzu.yuzu_emu.features.settings.ui.SettingsActivity; import org.yuzu.yuzu_emu.features.settings.ui.SettingsActivity;
import org.yuzu.yuzu_emu.model.GameProvider; import org.yuzu.yuzu_emu.model.GameProvider;
import org.yuzu.yuzu_emu.ui.platform.PlatformGamesFragment; import org.yuzu.yuzu_emu.ui.platform.PlatformGamesFragment;
import org.yuzu.yuzu_emu.utils.AddDirectoryHelper; import org.yuzu.yuzu_emu.utils.AddDirectoryHelper;
import org.yuzu.yuzu_emu.utils.DirectoryInitialization;
import org.yuzu.yuzu_emu.utils.FileBrowserHelper; import org.yuzu.yuzu_emu.utils.FileBrowserHelper;
import org.yuzu.yuzu_emu.utils.FileUtil;
import org.yuzu.yuzu_emu.utils.PicassoUtils; import org.yuzu.yuzu_emu.utils.PicassoUtils;
import org.yuzu.yuzu_emu.utils.StartupHandler; import org.yuzu.yuzu_emu.utils.StartupHandler;
import org.yuzu.yuzu_emu.utils.ThemeUtil; import org.yuzu.yuzu_emu.utils.ThemeUtil;
@ -116,8 +120,13 @@ public final class MainActivity extends AppCompatActivity implements MainView {
switch (request) { switch (request) {
case MainPresenter.REQUEST_ADD_DIRECTORY: case MainPresenter.REQUEST_ADD_DIRECTORY:
FileBrowserHelper.openDirectoryPicker(this, FileBrowserHelper.openDirectoryPicker(this,
MainPresenter.REQUEST_ADD_DIRECTORY, MainPresenter.REQUEST_ADD_DIRECTORY,
R.string.select_game_folder); R.string.select_game_folder);
break;
case MainPresenter.REQUEST_INSTALL_KEYS:
FileBrowserHelper.openFilePicker(this,
MainPresenter.REQUEST_INSTALL_KEYS,
R.string.install_keys);
break; break;
} }
} }
@ -132,7 +141,6 @@ public final class MainActivity extends AppCompatActivity implements MainView {
super.onActivityResult(requestCode, resultCode, result); super.onActivityResult(requestCode, resultCode, result);
switch (requestCode) { switch (requestCode) {
case MainPresenter.REQUEST_ADD_DIRECTORY: case MainPresenter.REQUEST_ADD_DIRECTORY:
// If the user picked a file, as opposed to just backing out.
if (resultCode == MainActivity.RESULT_OK) { if (resultCode == MainActivity.RESULT_OK) {
int takeFlags = (Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION); int takeFlags = (Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION);
getContentResolver().takePersistableUriPermission(Uri.parse(result.getDataString()), takeFlags); getContentResolver().takePersistableUriPermission(Uri.parse(result.getDataString()), takeFlags);
@ -144,6 +152,22 @@ public final class MainActivity extends AppCompatActivity implements MainView {
mPresenter.onDirectorySelected(FileBrowserHelper.getSelectedDirectory(result)); mPresenter.onDirectorySelected(FileBrowserHelper.getSelectedDirectory(result));
} }
break; break;
case MainPresenter.REQUEST_INSTALL_KEYS:
if (resultCode == MainActivity.RESULT_OK) {
int takeFlags = (Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION);
getContentResolver().takePersistableUriPermission(Uri.parse(result.getDataString()), takeFlags);
String dstPath = DirectoryInitialization.getUserDirectory() + "/keys/";
if (FileUtil.copyUriToInternalStorage(this, result.getData(), dstPath, "prod.keys")) {
if (NativeLibrary.ReloadKeys()) {
Toast.makeText(this, R.string.install_keys_success, Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(this, R.string.install_keys_failure, Toast.LENGTH_SHORT).show();
launchFileListActivity(MainPresenter.REQUEST_INSTALL_KEYS);
}
}
}
break;
} }
} }

View File

@ -11,6 +11,7 @@ import org.yuzu.yuzu_emu.utils.AddDirectoryHelper;
public final class MainPresenter { public final class MainPresenter {
public static final int REQUEST_ADD_DIRECTORY = 1; public static final int REQUEST_ADD_DIRECTORY = 1;
public static final int REQUEST_INSTALL_KEYS = 2;
private final MainView mView; private final MainView mView;
private String mDirToAdd; private String mDirToAdd;
private long mLastClickTime = 0; private long mLastClickTime = 0;
@ -46,6 +47,10 @@ public final class MainPresenter {
case R.id.button_add_directory: case R.id.button_add_directory:
launchFileListActivity(REQUEST_ADD_DIRECTORY); launchFileListActivity(REQUEST_ADD_DIRECTORY);
return true; return true;
case R.id.button_install_keys:
launchFileListActivity(REQUEST_INSTALL_KEYS);
return true;
} }
return false; return false;

View File

@ -10,6 +10,15 @@ public final class FileBrowserHelper {
activity.startActivityForResult(i, requestCode); activity.startActivityForResult(i, requestCode);
} }
public static void openFilePicker(FragmentActivity activity, int requestCode, int title) {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION);
intent.putExtra(Intent.EXTRA_TITLE, title);
intent.setType("*/*");
activity.startActivityForResult(intent, requestCode);
}
public static String getSelectedDirectory(Intent result) { public static String getSelectedDirectory(Intent result) {
return result.getDataString(); return result.getDataString();
} }

View File

@ -12,8 +12,9 @@ import androidx.documentfile.provider.DocumentFile;
import org.yuzu.yuzu_emu.model.MinimalDocumentFile; import org.yuzu.yuzu_emu.model.MinimalDocumentFile;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream;
import java.net.URLDecoder; import java.net.URLDecoder;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@ -243,6 +244,40 @@ public class FileUtil {
return size; return size;
} }
public static boolean copyUriToInternalStorage(Context context, Uri sourceUri, String destinationParentPath, String destinationFilename) {
InputStream input = null;
FileOutputStream output = null;
try {
input = context.getContentResolver().openInputStream(sourceUri);
output = new FileOutputStream(destinationParentPath + "/" + destinationFilename);
byte[] buffer = new byte[1024];
int len;
while ((len = input.read(buffer)) != -1) {
output.write(buffer, 0, len);
}
output.flush();
return true;
} catch (Exception e) {
Log.error("[FileUtil]: Cannot copy file, error: " + e.getMessage());
} finally {
if (input != null) {
try {
input.close();
} catch (IOException e) {
Log.error("[FileUtil]: Cannot close input file, error: " + e.getMessage());
}
}
if (output != null) {
try {
output.close();
} catch (IOException e) {
Log.error("[FileUtil]: Cannot close output file, error: " + e.getMessage());
}
}
}
return false;
}
public static boolean isRootTreeUri(Uri uri) { public static boolean isRootTreeUri(Uri uri) {
final List<String> paths = uri.getPathSegments(); final List<String> paths = uri.getPathSegments();
return paths.size() == 2 && PATH_TREE.equals(paths.get(0)); return paths.size() == 2 && PATH_TREE.equals(paths.get(0));

View File

@ -2,6 +2,10 @@ package org.yuzu.yuzu_emu.utils;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
import android.text.Html;
import android.text.method.LinkMovementMethod;
import android.widget.TextView;
import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AlertDialog;
import org.yuzu.yuzu_emu.R; import org.yuzu.yuzu_emu.R;
@ -13,7 +17,7 @@ public final class StartupHandler {
private static SharedPreferences mPreferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.getAppContext()); private static SharedPreferences mPreferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.getAppContext());
private static void handleStartupPromptDismiss(MainActivity parent) { private static void handleStartupPromptDismiss(MainActivity parent) {
parent.launchFileListActivity(MainPresenter.REQUEST_ADD_DIRECTORY); parent.launchFileListActivity(MainPresenter.REQUEST_INSTALL_KEYS);
} }
private static void markFirstBoot() { private static void markFirstBoot() {
@ -26,14 +30,16 @@ public final class StartupHandler {
if (mPreferences.getBoolean("FirstApplicationLaunch", true)) { if (mPreferences.getBoolean("FirstApplicationLaunch", true)) {
markFirstBoot(); markFirstBoot();
// Prompt user with standard first boot disclaimer AlertDialog.Builder builder = new AlertDialog.Builder(parent);
new AlertDialog.Builder(parent) builder.setMessage(Html.fromHtml(parent.getResources().getString(R.string.app_disclaimer)));
.setTitle(R.string.app_name) builder.setTitle(R.string.app_name);
.setIcon(R.mipmap.ic_launcher) builder.setIcon(R.mipmap.ic_launcher);
.setMessage(parent.getResources().getString(R.string.app_disclaimer)) builder.setPositiveButton(android.R.string.ok, null);
.setPositiveButton(android.R.string.ok, null) builder.setOnDismissListener(dialogInterface -> handleStartupPromptDismiss(parent));
.setOnDismissListener(dialogInterface -> handleStartupPromptDismiss(parent))
.show(); AlertDialog alert = builder.create();
alert.show();
((TextView) alert.findViewById(android.R.id.message)).setMovementMethod(LinkMovementMethod.getInstance());
} }
} }
} }

View File

@ -271,7 +271,7 @@ void Java_org_yuzu_yuzu_1emu_NativeLibrary_SetAppDirectory(JNIEnv* env,
jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_ReloadKeys(JNIEnv* env, jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_ReloadKeys(JNIEnv* env,
[[maybe_unused]] jclass clazz) { [[maybe_unused]] jclass clazz) {
Core::Crypto::KeyManager::Instance().ReloadKeys(); Core::Crypto::KeyManager::Instance().ReloadKeys();
return static_cast<jboolean>(Core::Crypto::KeyManager::Instance().IsKeysLoaded()); return static_cast<jboolean>(Core::Crypto::KeyManager::Instance().AreKeysLoaded());
} }
void Java_org_yuzu_yuzu_1emu_NativeLibrary_UnPauseEmulation([[maybe_unused]] JNIEnv* env, void Java_org_yuzu_yuzu_1emu_NativeLibrary_UnPauseEmulation([[maybe_unused]] JNIEnv* env,

View File

Before

Width:  |  Height:  |  Size: 514 B

After

Width:  |  Height:  |  Size: 514 B

View File

Before

Width:  |  Height:  |  Size: 364 B

After

Width:  |  Height:  |  Size: 364 B

View File

Before

Width:  |  Height:  |  Size: 556 B

After

Width:  |  Height:  |  Size: 556 B

View File

Before

Width:  |  Height:  |  Size: 405 B

After

Width:  |  Height:  |  Size: 405 B

View File

Before

Width:  |  Height:  |  Size: 729 B

After

Width:  |  Height:  |  Size: 729 B

View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

Before

Width:  |  Height:  |  Size: 656 B

After

Width:  |  Height:  |  Size: 656 B

View File

Before

Width:  |  Height:  |  Size: 967 B

After

Width:  |  Height:  |  Size: 967 B

View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -14,9 +14,9 @@
android:title="@string/select_game_folder" android:title="@string/select_game_folder"
app:showAsAction="ifRoom" /> app:showAsAction="ifRoom" />
<item <item
android:id="@+id/button_install_cia" android:id="@+id/button_install_keys"
android:icon="@drawable/ic_cia_install" android:icon="@drawable/ic_install"
android:title="@string/install_cia_title" android:title="@string/install_keys"
app:showAsAction="ifRoom" /> app:showAsAction="ifRoom" />
</menu> </menu>
</item> </item>

View File

@ -3,7 +3,7 @@
<!-- General application strings --> <!-- General application strings -->
<string name="app_name" translatable="false">yuzu</string> <string name="app_name" translatable="false">yuzu</string>
<string name="app_disclaimer">This software will run games for the Nintendo Switch game console. No game titles are included.\n\nBefore you run, please place your rightfully owned Switch game files onto your device storage.</string> <string name="app_disclaimer">This software will run games for the Nintendo Switch game console. No game titles or keys are included.&lt;br /&gt;&lt;br /&gt;Before you begin, please locate your <![CDATA[<b> prod.keys </b>]]> file on your device storage.&lt;br /&gt;&lt;br /&gt;<![CDATA[<a href="https://yuzu-emu.org/wiki/dumping-decryption-keys-from-a-switch-console/">Learn more</a>]]></string>
<string name="app_notification_channel_name" translatable="false">yuzu</string> <string name="app_notification_channel_name" translatable="false">yuzu</string>
<string name="app_notification_channel_id" translatable="false">yuzu</string> <string name="app_notification_channel_id" translatable="false">yuzu</string>
<string name="app_notification_channel_description">yuzu Switch emulator notifications</string> <string name="app_notification_channel_description">yuzu Switch emulator notifications</string>
@ -49,7 +49,9 @@
<!-- Add Directory Screen--> <!-- Add Directory Screen-->
<string name="select_game_folder">Select game folder</string> <string name="select_game_folder">Select game folder</string>
<string name="install_cia_title">Install CIA</string> <string name="install_keys">Install keys</string>
<string name="install_keys_success">Keys successfully installed</string>
<string name="install_keys_failure">Keys file (prod.keys) is invalid</string>
<!-- Preferences Screen --> <!-- Preferences Screen -->
<string name="preferences_settings">Settings</string> <string name="preferences_settings">Settings</string>

View File

@ -706,7 +706,7 @@ void KeyManager::LoadFromFile(const std::filesystem::path& file_path, bool is_ti
} }
} }
bool KeyManager::IsKeysLoaded() const { bool KeyManager::AreKeysLoaded() const {
return !s128_keys.empty() && !s256_keys.empty(); return !s128_keys.empty() && !s256_keys.empty();
} }

View File

@ -268,7 +268,7 @@ public:
bool AddTicketPersonalized(Ticket raw); bool AddTicketPersonalized(Ticket raw);
void ReloadKeys(); void ReloadKeys();
bool IsKeysLoaded() const; bool AreKeysLoaded() const;
private: private:
KeyManager(); KeyManager();