Implement full profile picture support

Extends the profile picture stub into a full-fledged implementation with the ability for users to set their profile picture in settings while having the Skyline icon as the default profile picture.
This commit is contained in:
PabloG02 2022-12-27 18:23:41 +01:00 committed by GitHub
parent 7a3d2e4a26
commit 80c0f8f04d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 119 additions and 17 deletions

View File

@ -153,6 +153,7 @@ dependencies {
implementation 'androidx.appcompat:appcompat:1.5.0' implementation 'androidx.appcompat:appcompat:1.5.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.preference:preference-ktx:1.2.0' 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 'com.google.android.material:material:1.6.1'
implementation 'androidx.documentfile:documentfile:1.0.1' implementation 'androidx.documentfile:documentfile:1.0.1'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View File

@ -33,6 +33,7 @@ namespace skyline {
void Update() override { void Update() override {
isDocked = ktSettings.GetBool("isDocked"); isDocked = ktSettings.GetBool("isDocked");
usernameValue = std::move(ktSettings.GetString("usernameValue")); usernameValue = std::move(ktSettings.GetString("usernameValue"));
profilePictureValue = ktSettings.GetString("profilePictureValue");
systemLanguage = ktSettings.GetInt<skyline::language::SystemLanguage>("systemLanguage"); systemLanguage = ktSettings.GetInt<skyline::language::SystemLanguage>("systemLanguage");
systemRegion = ktSettings.GetInt<skyline::region::RegionCode>("systemRegion"); systemRegion = ktSettings.GetInt<skyline::region::RegionCode>("systemRegion");
forceTripleBuffering = ktSettings.GetBool("forceTripleBuffering"); forceTripleBuffering = ktSettings.GetBool("forceTripleBuffering");

View File

@ -61,6 +61,7 @@ namespace skyline {
// System // System
Setting<bool> isDocked; //!< If the emulated Switch should be handheld or docked Setting<bool> isDocked; //!< If the emulated Switch should be handheld or docked
Setting<std::string> usernameValue; //!< The user name to be supplied to the guest Setting<std::string> usernameValue; //!< The user name to be supplied to the guest
Setting<std::string> profilePictureValue; //!< The profile picture path to be supplied to the guest
Setting<language::SystemLanguage> systemLanguage; //!< The system language Setting<language::SystemLanguage> systemLanguage; //!< The system language
Setting<region::RegionCode> systemRegion; //!< The system region Setting<region::RegionCode> systemRegion; //!< The system region

View File

@ -1,23 +1,13 @@
// SPDX-License-Identifier: MPL-2.0 // SPDX-License-Identifier: MPL-2.0
// Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/) // Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
#include <os.h>
#include <vfs/os_backing.h>
#include <fcntl.h>
#include <common/settings.h> #include <common/settings.h>
#include "IProfile.h" #include "IProfile.h"
namespace skyline::service::account { namespace skyline::service::account {
// Smallest JPEG file https://github.com/mathiasbynens/small/blob/master/jpeg.jpg
constexpr std::array<u8, 107> 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) {} 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) { 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) { Result IProfile::GetImageSize(type::KSession &session, ipc::IpcRequest &request, ipc::IpcResponse &response) {
response.Push<u32>(profileImageIcon.size()); std::shared_ptr<vfs::Backing> profileImageIcon{GetProfilePicture()};
response.Push(static_cast<u32>(profileImageIcon->size));
return {}; return {};
} }
Result IProfile::LoadImage(type::KSession &session, ipc::IpcRequest &request, ipc::IpcResponse &response) { Result IProfile::LoadImage(type::KSession &session, ipc::IpcRequest &request, ipc::IpcResponse &response) {
// TODO: load actual profile image std::shared_ptr<vfs::Backing> profileImageIcon{GetProfilePicture()};
request.outputBuf.at(0).copy_from(profileImageIcon);
response.Push<u32>(profileImageIcon.size()); profileImageIcon->Read(span(request.outputBuf.at(0)).first(profileImageIcon->size), 0);
response.Push(static_cast<u32>(profileImageIcon->size));
return {}; return {};
} }
std::shared_ptr<vfs::Backing> 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<vfs::OsBacking>(fd, true);
}
} }

View File

@ -38,6 +38,12 @@ namespace skyline::service::account {
*/ */
Result LoadImage(type::KSession &session, ipc::IpcRequest &request, ipc::IpcResponse &response); 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<vfs::Backing> GetProfilePicture();
SERVICE_DECL( SERVICE_DECL(
SFUNC(0x0, IProfile, Get), SFUNC(0x0, IProfile, Get),
SFUNC(0x1, IProfile, GetBase), SFUNC(0x1, IProfile, GetBase),

View File

@ -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<ProfilePicturePreference> { 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()
}
}
}

View File

@ -15,6 +15,7 @@ class NativeSettings(context : Context, pref : PreferenceSettings) {
// System // System
var isDocked : Boolean = pref.isDocked var isDocked : Boolean = pref.isDocked
var usernameValue : String = pref.usernameValue var usernameValue : String = pref.usernameValue
var profilePictureValue : String = pref.profilePictureValue
var systemLanguage : Int = pref.systemLanguage var systemLanguage : Int = pref.systemLanguage
var systemRegion : Int = pref.systemRegion var systemRegion : Int = pref.systemRegion

View File

@ -25,6 +25,7 @@ class PreferenceSettings @Inject constructor(@ApplicationContext private val con
// System // System
var isDocked by sharedPreferences(context, true) var isDocked by sharedPreferences(context, true)
var usernameValue by sharedPreferences(context, context.getString(R.string.username_default)) var usernameValue by sharedPreferences(context, context.getString(R.string.username_default))
var profilePictureValue by sharedPreferences(context, "")
var systemLanguage by sharedPreferences(context, 1) var systemLanguage by sharedPreferences(context, 1)
var systemRegion by sharedPreferences(context, -1) var systemRegion by sharedPreferences(context, -1)

View File

@ -47,6 +47,7 @@
<string name="docked_enabled">The system will emulate being in docked mode</string> <string name="docked_enabled">The system will emulate being in docked mode</string>
<string name="username">Username</string> <string name="username">Username</string>
<string name="username_default" translatable="false">@string/app_name</string> <string name="username_default" translatable="false">@string/app_name</string>
<string name="profile_picture">Profile picture</string>
<string name="system_language">System language</string> <string name="system_language">System language</string>
<string name="system_region">System region</string> <string name="system_region">System region</string>
<!-- Settings - Keys --> <!-- Settings - Keys -->

View File

@ -70,6 +70,9 @@
app:key="username_value" app:key="username_value"
app:limit="31" app:limit="31"
app:title="@string/username" /> app:title="@string/username" />
<emu.skyline.preference.ProfilePicturePreference
app:key="profile_picture_value"
app:title="@string/profile_picture" />
<emu.skyline.preference.IntegerListPreference <emu.skyline.preference.IntegerListPreference
android:defaultValue="1" android:defaultValue="1"
android:entries="@array/system_languages" android:entries="@array/system_languages"