android: frontend: Integrate key installation for SAF.
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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));
|
||||||
|
|
|
@ -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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
Before Width: | Height: | Size: 514 B After Width: | Height: | Size: 514 B |
Before Width: | Height: | Size: 364 B After Width: | Height: | Size: 364 B |
Before Width: | Height: | Size: 556 B After Width: | Height: | Size: 556 B |
Before Width: | Height: | Size: 405 B After Width: | Height: | Size: 405 B |
Before Width: | Height: | Size: 729 B After Width: | Height: | Size: 729 B |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 656 B After Width: | Height: | Size: 656 B |
Before Width: | Height: | Size: 967 B After Width: | Height: | Size: 967 B |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
@ -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>
|
||||||
|
|
|
@ -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.<br /><br />Before you begin, please locate your <![CDATA[<b> prod.keys </b>]]> file on your device storage.<br /><br /><![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>
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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();
|
||||||
|
|