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