Просмотр исходного кода

android: Add user directory shortcut

Charles Lombardo 3 лет назад
Родитель
Сommit
912bf6a0c6

+ 1 - 0
src/android/app/src/main/AndroidManifest.xml

@@ -15,6 +15,7 @@
     <uses-permission android:name="android.permission.INTERNET" />
     <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
     <uses-permission android:name="android.permission.NFC" />
+    <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
 
     <application
         android:name="org.yuzu.yuzu_emu.YuzuApplication"

+ 19 - 14
src/android/app/src/main/java/org/yuzu/yuzu_emu/YuzuApplication.kt

@@ -15,23 +15,29 @@ import java.io.File
 fun Context.getPublicFilesDir() : File = getExternalFilesDir(null) ?: filesDir
 
 class YuzuApplication : Application() {
-    private fun createNotificationChannel() {
-        // Create the NotificationChannel, but only on API 26+ because
-        // the NotificationChannel class is new and not in the support library
-        val name: CharSequence = getString(R.string.app_notification_channel_name)
-        val description = getString(R.string.app_notification_channel_description)
-        val channel = NotificationChannel(
-            getString(R.string.app_notification_channel_id),
-            name,
+    private fun createNotificationChannels() {
+        val emulationChannel = NotificationChannel(
+            getString(R.string.emulation_notification_channel_id),
+            getString(R.string.emulation_notification_channel_name),
             NotificationManager.IMPORTANCE_LOW
         )
-        channel.description = description
-        channel.setSound(null, null)
-        channel.vibrationPattern = null
+        emulationChannel.description = getString(R.string.emulation_notification_channel_description)
+        emulationChannel.setSound(null, null)
+        emulationChannel.vibrationPattern = null
+
+        val noticeChannel = NotificationChannel(
+            getString(R.string.notice_notification_channel_id),
+            getString(R.string.notice_notification_channel_name),
+            NotificationManager.IMPORTANCE_HIGH
+        )
+        noticeChannel.description = getString(R.string.notice_notification_channel_description)
+        noticeChannel.setSound(null, null)
+
         // Register the channel with the system; you can't change the importance
         // or other notification behaviors after this
         val notificationManager = getSystemService(NotificationManager::class.java)
-        notificationManager.createNotificationChannel(channel)
+        notificationManager.createNotificationChannel(emulationChannel)
+        notificationManager.createNotificationChannel(noticeChannel)
     }
 
     override fun onCreate() {
@@ -42,8 +48,7 @@ class YuzuApplication : Application() {
         GpuDriverHelper.initializeDriverParameters(applicationContext)
         NativeLibrary.logDeviceInfo()
 
-        // TODO(bunnei): Disable notifications until we support app suspension.
-        //createNotificationChannel();
+        createNotificationChannels();
     }
 
     companion object {

+ 89 - 0
src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt

@@ -3,14 +3,21 @@
 
 package org.yuzu.yuzu_emu.fragments
 
+import android.Manifest
+import android.content.ActivityNotFoundException
 import android.content.DialogInterface
 import android.content.Intent
+import android.content.pm.PackageManager
 import android.os.Bundle
+import android.provider.DocumentsContract
 import android.view.LayoutInflater
 import android.view.View
 import android.view.ViewGroup
 import android.widget.Toast
 import androidx.appcompat.app.AppCompatActivity
+import androidx.core.app.ActivityCompat
+import androidx.core.app.NotificationCompat
+import androidx.core.app.NotificationManagerCompat
 import androidx.core.view.ViewCompat
 import androidx.core.view.WindowInsetsCompat
 import androidx.fragment.app.Fragment
@@ -19,6 +26,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
 import org.yuzu.yuzu_emu.R
 import org.yuzu.yuzu_emu.adapters.HomeSettingAdapter
 import org.yuzu.yuzu_emu.databinding.FragmentHomeSettingsBinding
+import org.yuzu.yuzu_emu.features.DocumentProvider
 import org.yuzu.yuzu_emu.features.settings.ui.SettingsActivity
 import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile
 import org.yuzu.yuzu_emu.model.HomeSetting
@@ -49,6 +57,11 @@ class HomeSettingsFragment : Fragment() {
                 R.string.settings_description,
                 R.drawable.ic_settings
             ) { SettingsActivity.launch(requireContext(), SettingsFile.FILE_NAME_CONFIG, "") },
+            HomeSetting(
+                R.string.open_user_folder,
+                R.string.open_user_folder_description,
+                R.drawable.ic_folder
+            ) { openFileManager() },
             HomeSetting(
                 R.string.install_gpu_driver,
                 R.string.install_gpu_driver_description,
@@ -84,6 +97,82 @@ class HomeSettingsFragment : Fragment() {
         _binding = null
     }
 
+    private fun openFileManager() {
+        // First, try to open the user data folder directly
+        try {
+            startActivity(getFileManagerIntentOnDocumentProvider(Intent.ACTION_VIEW))
+            return
+        } catch (_: ActivityNotFoundException) {}
+
+        try {
+            startActivity(getFileManagerIntentOnDocumentProvider("android.provider.action.BROWSE"))
+            return
+        } catch (_: ActivityNotFoundException) {}
+
+        // Just try to open the file manager, try the package name used on "normal" phones
+        try {
+            startActivity(getFileManagerIntent("com.google.android.documentsui"))
+            showNoLinkNotification()
+            return
+        } catch (_: ActivityNotFoundException) {}
+
+        try {
+            // Next, try the AOSP package name
+            startActivity(getFileManagerIntent("com.android.documentsui"))
+            showNoLinkNotification()
+            return
+        } catch (_: ActivityNotFoundException) {}
+
+        Toast.makeText(
+            requireContext(),
+            resources.getString(R.string.no_file_manager),
+            Toast.LENGTH_LONG
+        ).show()
+    }
+
+    private fun getFileManagerIntent(packageName: String): Intent {
+        // Fragile, but some phones don't expose the system file manager in any better way
+        val intent = Intent(Intent.ACTION_MAIN)
+        intent.setClassName(packageName, "com.android.documentsui.files.FilesActivity")
+        intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
+        return intent
+    }
+
+    private fun getFileManagerIntentOnDocumentProvider(action: String): Intent {
+        val authority = "${requireContext().packageName}.user"
+        val intent = Intent(action)
+        intent.addCategory(Intent.CATEGORY_DEFAULT)
+        intent.data = DocumentsContract.buildRootUri(authority, DocumentProvider.ROOT_ID)
+        intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION or Intent.FLAG_GRANT_PREFIX_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
+        return intent
+    }
+
+    private fun showNoLinkNotification() {
+        val builder = NotificationCompat.Builder(requireContext(), getString(R.string.notice_notification_channel_id))
+            .setSmallIcon(R.drawable.ic_stat_notification_logo)
+            .setContentTitle(getString(R.string.notification_no_directory_link))
+            .setContentText(getString(R.string.notification_no_directory_link_description))
+            .setPriority(NotificationCompat.PRIORITY_HIGH)
+            .setAutoCancel(true)
+        // TODO: Make the click action for this notification lead to a help article
+
+        with(NotificationManagerCompat.from(requireContext())) {
+            if (ActivityCompat.checkSelfPermission(
+                    requireContext(),
+                    Manifest.permission.POST_NOTIFICATIONS
+                ) != PackageManager.PERMISSION_GRANTED
+            ) {
+                Toast.makeText(
+                    requireContext(),
+                    resources.getString(R.string.notification_permission_not_granted),
+                    Toast.LENGTH_LONG
+                ).show()
+                return
+            }
+            notify(0, builder.build())
+        }
+    }
+
     private fun driverInstaller() {
         // Get the driver name for the dialog message.
         var driverName = GpuDriverHelper.customDriverName

+ 11 - 0
src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SetupFragment.kt

@@ -4,11 +4,13 @@
 package org.yuzu.yuzu_emu.fragments
 
 import android.content.Intent
+import android.os.Build
 import android.os.Bundle
 import android.view.LayoutInflater
 import android.view.View
 import android.view.ViewGroup
 import androidx.activity.OnBackPressedCallback
+import androidx.activity.result.contract.ActivityResultContracts
 import androidx.appcompat.app.AppCompatActivity
 import androidx.core.content.ContextCompat
 import androidx.core.view.ViewCompat
@@ -63,6 +65,10 @@ class SetupFragment : Fragment() {
     }
 
     override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+            pushNotificationPermissionLauncher.launch(android.Manifest.permission.POST_NOTIFICATIONS)
+        }
+
         mainActivity = requireActivity() as MainActivity
 
         homeViewModel.setNavigationVisibility(false)
@@ -219,6 +225,11 @@ class SetupFragment : Fragment() {
         _binding = null
     }
 
+    private val pushNotificationPermissionLauncher =
+        registerForActivityResult(ActivityResultContracts.RequestPermission()) {
+            // TODO: Show proper notification request reason and confirmation
+        }
+
     private fun finishSetup() {
         PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext).edit()
             .putBoolean(Settings.PREF_FIRST_APP_LAUNCH, false)

+ 2 - 2
src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/ForegroundService.kt

@@ -29,10 +29,10 @@ class ForegroundService : Service() {
             PendingIntent.FLAG_IMMUTABLE
         )
         val builder =
-            NotificationCompat.Builder(this, getString(R.string.app_notification_channel_id))
+            NotificationCompat.Builder(this, getString(R.string.emulation_notification_channel_id))
                 .setSmallIcon(R.drawable.ic_stat_notification_logo)
                 .setContentTitle(getString(R.string.app_name))
-                .setContentText(getString(R.string.app_notification_running))
+                .setContentText(getString(R.string.emulation_notification_running))
                 .setPriority(NotificationCompat.PRIORITY_LOW)
                 .setOngoing(true)
                 .setVibrate(null)

+ 18 - 9
src/android/app/src/main/res/values/strings.xml

@@ -4,10 +4,14 @@
     <!-- 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 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/help/quickstart">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>
-    <string name="app_notification_running">yuzu is running</string>
+    <string name="emulation_notification_channel_name">Emulation is Active</string>
+    <string name="emulation_notification_channel_id" translatable="false">emulationIsActive</string>
+    <string name="emulation_notification_channel_description">Shows a persistent notification when emulation is running.</string>
+    <string name="emulation_notification_running">yuzu is running</string>
+    <string name="notice_notification_channel_name">Notices and errors</string>
+    <string name="notice_notification_channel_id" translatable="false">noticesAndErrors</string>
+    <string name="notice_notification_channel_description">Shows notifications when something goes wrong.</string>
+    <string name="notification_permission_not_granted">Notification permission not granted!</string>
 
     <!-- Setup strings -->
     <string name="welcome">Welcome!</string>
@@ -29,14 +33,14 @@
     <!-- Home strings -->
     <string name="home_games">Games</string>
     <string name="home_settings">Settings</string>
-    <string name="select_games_folder">Select Games Folder</string>
+    <string name="select_games_folder">Select games folder</string>
     <string name="select_games_folder_description">Allows yuzu to populate the games list</string>
     <string name="add_games_warning">Skip selecting games folder?</string>
     <string name="add_games_warning_description">Games won\'t be displayed in the Games list if a folder isn\'t selected.</string>
     <string name="add_games_warning_help">https://yuzu-emu.org/help/quickstart/#dumping-games</string>
     <string name="home_search_games">Search Games</string>
     <string name="games_dir_selected">Games directory selected</string>
-    <string name="install_prod_keys">Install Prod.keys</string>
+    <string name="install_prod_keys">Install prod.keys</string>
     <string name="install_prod_keys_description">Required to decrypt retail games</string>
     <string name="install_prod_keys_warning">Skip adding keys?</string>
     <string name="install_prod_keys_warning_description">Valid keys are required to emulate retail games. Only homebrew apps will function if you continue.</string>
@@ -44,16 +48,21 @@
     <string name="warning_help">Help</string>
     <string name="warning_skip">Skip</string>
     <string name="warning_cancel">Cancel</string>
-    <string name="install_amiibo_keys">Install Amiibo Keys</string>
+    <string name="install_amiibo_keys">Install Amiibo keys</string>
     <string name="install_amiibo_keys_description">Required to use Amiibo in game</string>
     <string name="invalid_keys_file">Invalid keys file selected</string>
     <string name="install_keys_success">Keys successfully installed</string>
     <string name="install_keys_failure">Keys file (prod.keys) is invalid</string>
     <string name="install_amiibo_keys_failure">Keys file (key_retail.bin) is invalid</string>
-    <string name="install_gpu_driver">Install GPU Driver</string>
+    <string name="install_gpu_driver">Install GPU driver</string>
     <string name="install_gpu_driver_description">Install alternative drivers for potentially better performance or accuracy</string>
-    <string name="advanced_settings">Advanced Settings</string>
+    <string name="advanced_settings">Advanced settings</string>
     <string name="settings_description">Configure emulator settings</string>
+    <string name="open_user_folder">Open yuzu folder</string>
+    <string name="open_user_folder_description">Manage yuzu\'s internal files</string>
+    <string name="no_file_manager">No file manager found</string>
+    <string name="notification_no_directory_link">Couldn\'t open yuzu directory</string>
+    <string name="notification_no_directory_link_description">Please locate the user folder with the file manager\'s side panel manually.</string>
 
     <!-- General settings strings -->
     <string name="frame_limit_enable">Enable limit speed</string>