diff --git a/app/build.gradle b/app/build.gradle index f364e302..fb114408 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -153,6 +153,7 @@ dependencies { implementation 'androidx.appcompat:appcompat:1.5.0' implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation 'androidx.preference:preference-ktx:1.2.0' + implementation 'androidx.activity:activity-ktx:1.6.1' implementation 'com.google.android.material:material:1.6.1' implementation 'androidx.documentfile:documentfile:1.0.1' implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' diff --git a/app/src/main/assets/profile_picture.jpeg b/app/src/main/assets/profile_picture.jpeg new file mode 100644 index 00000000..b1c38ce5 Binary files /dev/null and b/app/src/main/assets/profile_picture.jpeg differ diff --git a/app/src/main/cpp/skyline/common/android_settings.h b/app/src/main/cpp/skyline/common/android_settings.h index eedbd7c4..77f4ad0d 100644 --- a/app/src/main/cpp/skyline/common/android_settings.h +++ b/app/src/main/cpp/skyline/common/android_settings.h @@ -33,6 +33,7 @@ namespace skyline { void Update() override { isDocked = ktSettings.GetBool("isDocked"); usernameValue = std::move(ktSettings.GetString("usernameValue")); + profilePictureValue = ktSettings.GetString("profilePictureValue"); systemLanguage = ktSettings.GetInt("systemLanguage"); systemRegion = ktSettings.GetInt("systemRegion"); forceTripleBuffering = ktSettings.GetBool("forceTripleBuffering"); diff --git a/app/src/main/cpp/skyline/common/settings.h b/app/src/main/cpp/skyline/common/settings.h index 47cc2ca4..ef181d1d 100644 --- a/app/src/main/cpp/skyline/common/settings.h +++ b/app/src/main/cpp/skyline/common/settings.h @@ -61,6 +61,7 @@ namespace skyline { // System Setting isDocked; //!< If the emulated Switch should be handheld or docked Setting usernameValue; //!< The user name to be supplied to the guest + Setting profilePictureValue; //!< The profile picture path to be supplied to the guest Setting systemLanguage; //!< The system language Setting systemRegion; //!< The system region diff --git a/app/src/main/cpp/skyline/services/account/IProfile.cpp b/app/src/main/cpp/skyline/services/account/IProfile.cpp index 13ae1358..a966d36d 100644 --- a/app/src/main/cpp/skyline/services/account/IProfile.cpp +++ b/app/src/main/cpp/skyline/services/account/IProfile.cpp @@ -1,23 +1,13 @@ // SPDX-License-Identifier: MPL-2.0 // Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/) +#include +#include +#include #include #include "IProfile.h" namespace skyline::service::account { - // Smallest JPEG file https://github.com/mathiasbynens/small/blob/master/jpeg.jpg - constexpr std::array profileImageIcon{ - 0xFF, 0xD8, 0xFF, 0xDB, 0x00, 0x43, 0x00, 0x03, 0x02, 0x02, 0x02, 0x02, - 0x02, 0x03, 0x02, 0x02, 0x02, 0x03, 0x03, 0x03, 0x03, 0x04, 0x06, 0x04, - 0x04, 0x04, 0x04, 0x04, 0x08, 0x06, 0x06, 0x05, 0x06, 0x09, 0x08, 0x0A, - 0x0A, 0x09, 0x08, 0x09, 0x09, 0x0A, 0x0C, 0x0F, 0x0C, 0x0A, 0x0B, 0x0E, - 0x0B, 0x09, 0x09, 0x0D, 0x11, 0x0D, 0x0E, 0x0F, 0x10, 0x10, 0x11, 0x10, - 0x0A, 0x0C, 0x12, 0x13, 0x12, 0x10, 0x13, 0x0F, 0x10, 0x10, 0x10, 0xFF, - 0xC9, 0x00, 0x0B, 0x08, 0x00, 0x01, 0x00, 0x01, 0x01, 0x01, 0x11, 0x00, - 0xFF, 0xCC, 0x00, 0x06, 0x00, 0x10, 0x10, 0x05, 0xFF, 0xDA, 0x00, 0x08, - 0x01, 0x01, 0x00, 0x00, 0x3F, 0x00, 0xD2, 0xCF, 0x20, 0xFF, 0xD9 - }; - IProfile::IProfile(const DeviceState &state, ServiceManager &manager, const UserId &userId) : userId(userId), BaseService(state, manager) {} Result IProfile::Get(type::KSession &session, ipc::IpcRequest &request, ipc::IpcResponse &response) { @@ -53,14 +43,28 @@ namespace skyline::service::account { } Result IProfile::GetImageSize(type::KSession &session, ipc::IpcRequest &request, ipc::IpcResponse &response) { - response.Push(profileImageIcon.size()); + std::shared_ptr profileImageIcon{GetProfilePicture()}; + response.Push(static_cast(profileImageIcon->size)); + return {}; } Result IProfile::LoadImage(type::KSession &session, ipc::IpcRequest &request, ipc::IpcResponse &response) { - // TODO: load actual profile image - request.outputBuf.at(0).copy_from(profileImageIcon); - response.Push(profileImageIcon.size()); + std::shared_ptr profileImageIcon{GetProfilePicture()}; + + profileImageIcon->Read(span(request.outputBuf.at(0)).first(profileImageIcon->size), 0); + response.Push(static_cast(profileImageIcon->size)); + return {}; } + + std::shared_ptr IProfile::GetProfilePicture() { + std::string profilePicturePath{*state.settings->profilePictureValue}; + int fd{open((profilePicturePath).c_str(), O_RDONLY)}; + if (fd < 0) + // If we can't find the profile picture then just return the placeholder profile picture + return state.os->assetFileSystem->OpenFile("profile_picture.jpeg"); + else + return std::make_shared(fd, true); + } } diff --git a/app/src/main/cpp/skyline/services/account/IProfile.h b/app/src/main/cpp/skyline/services/account/IProfile.h index 006e775b..ee8a6205 100644 --- a/app/src/main/cpp/skyline/services/account/IProfile.h +++ b/app/src/main/cpp/skyline/services/account/IProfile.h @@ -38,6 +38,12 @@ namespace skyline::service::account { */ Result LoadImage(type::KSession &session, ipc::IpcRequest &request, ipc::IpcResponse &response); + /** + * @brief Tries to get the user's profile picture. If not found, returns the default one + * @return A shared pointer to a Backing object of the profile picture + */ + std::shared_ptr GetProfilePicture(); + SERVICE_DECL( SFUNC(0x0, IProfile, Get), SFUNC(0x1, IProfile, GetBase), diff --git a/app/src/main/java/emu/skyline/preference/ProfilePicturePreference.kt b/app/src/main/java/emu/skyline/preference/ProfilePicturePreference.kt new file mode 100644 index 00000000..bb86f6c3 --- /dev/null +++ b/app/src/main/java/emu/skyline/preference/ProfilePicturePreference.kt @@ -0,0 +1,83 @@ +/* + * SPDX-License-Identifier: MPL-2.0 + * Copyright © 2022 Skyline Team and Contributors (https://github.com/skyline-emu/) + */ + +package emu.skyline.preference + +import android.content.ContentResolver +import android.content.Context +import android.content.Intent +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.net.Uri +import android.util.AttributeSet +import androidx.activity.ComponentActivity +import androidx.activity.result.PickVisualMediaRequest +import androidx.activity.result.contract.ActivityResultContracts +import androidx.preference.Preference +import androidx.preference.Preference.SummaryProvider +import androidx.preference.PreferenceManager +import androidx.preference.R +import emu.skyline.SkylineApplication +import emu.skyline.getPublicFilesDir +import java.io.File +import java.io.FileOutputStream +import java.io.InputStream + +class ProfilePicturePreference @JvmOverloads constructor(context : Context, attrs : AttributeSet? = null, defStyleAttr : Int = R.attr.preferenceStyle) : Preference(context, attrs, defStyleAttr) { + private val pickMedia = (context as ComponentActivity).registerForActivityResult(ActivityResultContracts.PickVisualMedia()) { uri -> + val profilePictureDir = SkylineApplication.instance.getPublicFilesDir().canonicalPath + "/switch/nand/system/save/8000000000000010/su/avators" + val profilePictureName = "profile_picture.jpeg" + try { + if (uri != null) { // The user selected a picture + PreferenceManager.getDefaultSharedPreferences(context).edit().putString(key, "$profilePictureDir/$profilePictureName").apply() + File(profilePictureDir).mkdirs() + context.applicationContext.contentResolver.let { contentResolver : ContentResolver -> + val readUriPermission : Int = Intent.FLAG_GRANT_READ_URI_PERMISSION + contentResolver.takePersistableUriPermission(uri, readUriPermission) + contentResolver.openInputStream(uri)?.use { inputStream : InputStream -> + var bitmap = BitmapFactory.decodeStream(inputStream) + // Compress the picture + bitmap = Bitmap.createScaledBitmap(bitmap, 256, 256, false) + StoreBitmap(bitmap, "$profilePictureDir/$profilePictureName") + } + } + } else { // No picture was selected, clear the profile picture if one was already set + if (File("$profilePictureDir/$profilePictureName").exists()) { + File("$profilePictureDir/$profilePictureName").delete() + } + PreferenceManager.getDefaultSharedPreferences(context).edit().putString(key, "No picture selected").apply() + } + notifyChanged() + } catch (e : Exception) { + e.printStackTrace() + } + } + + init { + summaryProvider = SummaryProvider { preference -> + Uri.decode(preference.getPersistedString("No picture selected")) + } + } + + override fun onClick() = pickMedia.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)) + + /** + * Given a bitmap, saves it in the specified location + */ + private fun StoreBitmap(bitmap : Bitmap, filePath : String) { + try { + // Create the file where the bitmap will be stored + val file = File(filePath) + file.createNewFile() + // Store bitmap as JPEG + val outputFile = FileOutputStream(file) + bitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputFile) + outputFile.flush() + outputFile.close() + } catch (e : Exception) { + e.printStackTrace() + } + } +} diff --git a/app/src/main/java/emu/skyline/utils/NativeSettings.kt b/app/src/main/java/emu/skyline/utils/NativeSettings.kt index 39ce55da..5d7777f7 100644 --- a/app/src/main/java/emu/skyline/utils/NativeSettings.kt +++ b/app/src/main/java/emu/skyline/utils/NativeSettings.kt @@ -15,6 +15,7 @@ class NativeSettings(context : Context, pref : PreferenceSettings) { // System var isDocked : Boolean = pref.isDocked var usernameValue : String = pref.usernameValue + var profilePictureValue : String = pref.profilePictureValue var systemLanguage : Int = pref.systemLanguage var systemRegion : Int = pref.systemRegion diff --git a/app/src/main/java/emu/skyline/utils/PreferenceSettings.kt b/app/src/main/java/emu/skyline/utils/PreferenceSettings.kt index 611a635f..812aa2ba 100644 --- a/app/src/main/java/emu/skyline/utils/PreferenceSettings.kt +++ b/app/src/main/java/emu/skyline/utils/PreferenceSettings.kt @@ -25,6 +25,7 @@ class PreferenceSettings @Inject constructor(@ApplicationContext private val con // System var isDocked by sharedPreferences(context, true) var usernameValue by sharedPreferences(context, context.getString(R.string.username_default)) + var profilePictureValue by sharedPreferences(context, "") var systemLanguage by sharedPreferences(context, 1) var systemRegion by sharedPreferences(context, -1) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3c3c2fed..176f539f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -47,6 +47,7 @@ The system will emulate being in docked mode Username @string/app_name + Profile picture System language System region diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index 07506875..ee57b9e6 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -70,6 +70,9 @@ app:key="username_value" app:limit="31" app:title="@string/username" /> +