android: frontend: Integrate key installation for SAF.
|
@ -6,18 +6,22 @@ import android.os.Bundle;
|
|||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
|
||||
import org.yuzu.yuzu_emu.NativeLibrary;
|
||||
import org.yuzu.yuzu_emu.R;
|
||||
import org.yuzu.yuzu_emu.activities.EmulationActivity;
|
||||
import org.yuzu.yuzu_emu.features.settings.ui.SettingsActivity;
|
||||
import org.yuzu.yuzu_emu.model.GameProvider;
|
||||
import org.yuzu.yuzu_emu.ui.platform.PlatformGamesFragment;
|
||||
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.FileUtil;
|
||||
import org.yuzu.yuzu_emu.utils.PicassoUtils;
|
||||
import org.yuzu.yuzu_emu.utils.StartupHandler;
|
||||
import org.yuzu.yuzu_emu.utils.ThemeUtil;
|
||||
|
@ -116,8 +120,13 @@ public final class MainActivity extends AppCompatActivity implements MainView {
|
|||
switch (request) {
|
||||
case MainPresenter.REQUEST_ADD_DIRECTORY:
|
||||
FileBrowserHelper.openDirectoryPicker(this,
|
||||
MainPresenter.REQUEST_ADD_DIRECTORY,
|
||||
R.string.select_game_folder);
|
||||
MainPresenter.REQUEST_ADD_DIRECTORY,
|
||||
R.string.select_game_folder);
|
||||
break;
|
||||
case MainPresenter.REQUEST_INSTALL_KEYS:
|
||||
FileBrowserHelper.openFilePicker(this,
|
||||
MainPresenter.REQUEST_INSTALL_KEYS,
|
||||
R.string.install_keys);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -132,7 +141,6 @@ public final class MainActivity extends AppCompatActivity implements MainView {
|
|||
super.onActivityResult(requestCode, resultCode, result);
|
||||
switch (requestCode) {
|
||||
case MainPresenter.REQUEST_ADD_DIRECTORY:
|
||||
// If the user picked a file, as opposed to just backing out.
|
||||
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);
|
||||
|
@ -144,6 +152,22 @@ public final class MainActivity extends AppCompatActivity implements MainView {
|
|||
mPresenter.onDirectorySelected(FileBrowserHelper.getSelectedDirectory(result));
|
||||
}
|
||||
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 static final int REQUEST_ADD_DIRECTORY = 1;
|
||||
public static final int REQUEST_INSTALL_KEYS = 2;
|
||||
private final MainView mView;
|
||||
private String mDirToAdd;
|
||||
private long mLastClickTime = 0;
|
||||
|
@ -46,6 +47,10 @@ public final class MainPresenter {
|
|||
case R.id.button_add_directory:
|
||||
launchFileListActivity(REQUEST_ADD_DIRECTORY);
|
||||
return true;
|
||||
|
||||
case R.id.button_install_keys:
|
||||
launchFileListActivity(REQUEST_INSTALL_KEYS);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
|
|
@ -10,6 +10,15 @@ public final class FileBrowserHelper {
|
|||
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) {
|
||||
return result.getDataString();
|
||||
}
|
||||
|
|
|
@ -12,8 +12,9 @@ import androidx.documentfile.provider.DocumentFile;
|
|||
|
||||
import org.yuzu.yuzu_emu.model.MinimalDocumentFile;
|
||||
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.URLDecoder;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
@ -243,6 +244,40 @@ public class FileUtil {
|
|||
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) {
|
||||
final List<String> paths = uri.getPathSegments();
|
||||
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.preference.PreferenceManager;
|
||||
import android.text.Html;
|
||||
import android.text.method.LinkMovementMethod;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
import org.yuzu.yuzu_emu.R;
|
||||
|
@ -13,7 +17,7 @@ public final class StartupHandler {
|
|||
private static SharedPreferences mPreferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.getAppContext());
|
||||
|
||||
private static void handleStartupPromptDismiss(MainActivity parent) {
|
||||
parent.launchFileListActivity(MainPresenter.REQUEST_ADD_DIRECTORY);
|
||||
parent.launchFileListActivity(MainPresenter.REQUEST_INSTALL_KEYS);
|
||||
}
|
||||
|
||||
private static void markFirstBoot() {
|
||||
|
@ -26,14 +30,16 @@ public final class StartupHandler {
|
|||
if (mPreferences.getBoolean("FirstApplicationLaunch", true)) {
|
||||
markFirstBoot();
|
||||
|
||||
// Prompt user with standard first boot disclaimer
|
||||
new AlertDialog.Builder(parent)
|
||||
.setTitle(R.string.app_name)
|
||||
.setIcon(R.mipmap.ic_launcher)
|
||||
.setMessage(parent.getResources().getString(R.string.app_disclaimer))
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.setOnDismissListener(dialogInterface -> handleStartupPromptDismiss(parent))
|
||||
.show();
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(parent);
|
||||
builder.setMessage(Html.fromHtml(parent.getResources().getString(R.string.app_disclaimer)));
|
||||
builder.setTitle(R.string.app_name);
|
||||
builder.setIcon(R.mipmap.ic_launcher);
|
||||
builder.setPositiveButton(android.R.string.ok, null);
|
||||
builder.setOnDismissListener(dialogInterface -> handleStartupPromptDismiss(parent));
|
||||
|
||||
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,
|
||||
[[maybe_unused]] jclass clazz) {
|
||||
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,
|
||||
|
|
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"
|
||||
app:showAsAction="ifRoom" />
|
||||
<item
|
||||
android:id="@+id/button_install_cia"
|
||||
android:icon="@drawable/ic_cia_install"
|
||||
android:title="@string/install_cia_title"
|
||||
android:id="@+id/button_install_keys"
|
||||
android:icon="@drawable/ic_install"
|
||||
android:title="@string/install_keys"
|
||||
app:showAsAction="ifRoom" />
|
||||
</menu>
|
||||
</item>
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
<!-- General application strings -->
|
||||
<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_id" translatable="false">yuzu</string>
|
||||
<string name="app_notification_channel_description">yuzu Switch emulator notifications</string>
|
||||
|
@ -49,7 +49,9 @@
|
|||
|
||||
<!-- Add Directory Screen-->
|
||||
<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 -->
|
||||
<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();
|
||||
}
|
||||
|
||||
|
|
|
@ -268,7 +268,7 @@ public:
|
|||
bool AddTicketPersonalized(Ticket raw);
|
||||
|
||||
void ReloadKeys();
|
||||
bool IsKeysLoaded() const;
|
||||
bool AreKeysLoaded() const;
|
||||
|
||||
private:
|
||||
KeyManager();
|
||||
|
|