WIP: [core][android] Display profile picture in profile page #8701
10 changed files with 151 additions and 56 deletions
|
@ -9,6 +9,7 @@
|
|||
|
||||
#include "editor/osm_auth.hpp"
|
||||
#include "editor/server_api.hpp"
|
||||
#include "editor/profile_picture.hpp"
|
||||
|
||||
namespace
|
||||
{
|
||||
|
@ -86,7 +87,7 @@ Java_app_organicmaps_editor_OsmOAuth_nativeGetOsmUsername(JNIEnv * env, jclass,
|
|||
UserPreferences prefs;
|
||||
if (LoadOsmUserPreferences(jni::ToNativeString(env, oauthToken), prefs))
|
||||
return jni::ToJavaString(env, prefs.m_displayName);
|
||||
return nullptr;
|
||||
return jstring();
|
||||
}
|
||||
|
||||
JNIEXPORT jint JNICALL
|
||||
|
@ -99,12 +100,12 @@ Java_app_organicmaps_editor_OsmOAuth_nativeGetOsmChangesetsCount(JNIEnv * env, j
|
|||
}
|
||||
|
||||
JNIEXPORT jstring JNICALL
|
||||
Java_app_organicmaps_editor_OsmOAuth_nativeGetOsmProfilePictureUrl(JNIEnv * env, jclass, jstring oauthToken)
|
||||
Java_app_organicmaps_editor_OsmOAuth_nativeGetOsmProfilePicturePath(JNIEnv * env, jclass, jstring oauthToken)
|
||||
{
|
||||
UserPreferences prefs;
|
||||
if (LoadOsmUserPreferences(jni::ToNativeString(env, oauthToken), prefs))
|
||||
return jni::ToJavaString(env, prefs.m_imageUrl);
|
||||
return nullptr;
|
||||
LoadOsmUserPreferences(jni::ToNativeString(env, oauthToken), prefs);
|
||||
std::string img_path = editor::ProfilePicture::download(prefs.m_imageUrl);
|
||||
return jni::ToJavaString(env, img_path);
|
||||
}
|
||||
|
||||
JNIEXPORT jstring JNICALL
|
||||
|
|
|
@ -2,18 +2,15 @@ package app.organicmaps.editor;
|
|||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.graphics.Bitmap;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.Size;
|
||||
import androidx.annotation.WorkerThread;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import app.organicmaps.MwmApplication;
|
||||
import app.organicmaps.util.NetworkPolicy;
|
||||
|
||||
public final class OsmOAuth
|
||||
{
|
||||
|
@ -66,15 +63,29 @@ public final class OsmOAuth
|
|||
return MwmApplication.prefs(context).getString(PREF_OSM_OAUTH2_TOKEN, "");
|
||||
}
|
||||
|
||||
public static String getUsername(@NonNull Context context)
|
||||
public static String getUsername(@NonNull Context context, boolean internet_allowed)
|
||||
{
|
||||
return MwmApplication.prefs(context).getString(PREF_OSM_USERNAME, "");
|
||||
final SharedPreferences prefs = MwmApplication.prefs(context);
|
||||
|
||||
if (internet_allowed)
|
||||
{
|
||||
final String token = getAuthToken(context);
|
||||
final String username = OsmOAuth.nativeGetOsmUsername(token);
|
||||
|
||||
if(username != null && !username.isEmpty())
|
||||
prefs.edit().putString(PREF_OSM_USERNAME, username).apply();
|
||||
}
|
||||
return prefs.getString(PREF_OSM_USERNAME, "Error: should be unreachable");
|
||||
}
|
||||
|
||||
public static Bitmap getProfilePicture(@NonNull Context context)
|
||||
public static String getProfilePicturePath(@NonNull Context context, boolean internet_allowed)
|
||||
{
|
||||
//TODO(HB): load and store image in cache here
|
||||
return null;
|
||||
String token = "";
|
||||
if (internet_allowed)
|
||||
token = getAuthToken(context);
|
||||
|
||||
![]()
biodranik
commented
Review
1. We don't use python_variable_naming, but camelCase.
2. Introducing a separate getCachedUsername() method would be clearer.
|
||||
// If token is empty, will only return cached image
|
||||
return nativeGetOsmProfilePicturePath(token);
|
||||
}
|
||||
|
||||
public static void setAuthorization(@NonNull Context context, String oauthToken, String username)
|
||||
|
@ -97,7 +108,21 @@ public final class OsmOAuth
|
|||
|
||||
public static String getHistoryUrl(@NonNull Context context)
|
||||
{
|
||||
return nativeGetHistoryUrl(getUsername(context));
|
||||
return nativeGetHistoryUrl(getUsername(context, false));
|
||||
}
|
||||
|
||||
|
||||
public static int getOsmChangesetsCount(@NonNull Context context, boolean internet_allowed) {
|
||||
final SharedPreferences prefs = MwmApplication.prefs(context);
|
||||
|
||||
if (internet_allowed) {
|
||||
final String token = getAuthToken(context);
|
||||
final int editsCount = OsmOAuth.nativeGetOsmChangesetsCount(token);
|
||||
if(editsCount > 0)
|
||||
prefs.edit().putInt(PREF_OSM_CHANGESETS_COUNT, editsCount).apply();
|
||||
}
|
||||
|
||||
return prefs.getInt(PREF_OSM_CHANGESETS_COUNT, 0);
|
||||
}
|
||||
|
||||
/*
|
||||
|
@ -127,7 +152,7 @@ public final class OsmOAuth
|
|||
|
||||
@WorkerThread
|
||||
@Nullable
|
||||
public static native String nativeGetOsmProfilePictureUrl(String oauthToken);
|
||||
public static native String nativeGetOsmProfilePicturePath(String oauthToken);
|
||||
|
||||
@WorkerThread
|
||||
@NonNull
|
||||
|
@ -138,22 +163,4 @@ public final class OsmOAuth
|
|||
*/
|
||||
@WorkerThread
|
||||
private static native int nativeGetOsmChangesetsCount(String oauthToken);
|
||||
|
||||
@WorkerThread
|
||||
public static int getOsmChangesetsCount(@NonNull Context context, @NonNull FragmentManager fm) {
|
||||
final int[] editsCount = {-1};
|
||||
NetworkPolicy.checkNetworkPolicy(fm, policy -> {
|
||||
if (!policy.canUseNetwork())
|
||||
return;
|
||||
|
||||
final String token = getAuthToken(context);
|
||||
editsCount[0] = OsmOAuth.nativeGetOsmChangesetsCount(token);
|
||||
});
|
||||
final SharedPreferences prefs = MwmApplication.prefs(context);
|
||||
if (editsCount[0] < 0)
|
||||
return prefs.getInt(PREF_OSM_CHANGESETS_COUNT, 0);
|
||||
|
||||
prefs.edit().putInt(PREF_OSM_CHANGESETS_COUNT, editsCount[0]).apply();
|
||||
return editsCount[0];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
package app.organicmaps.editor;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
|
@ -15,16 +15,17 @@ import androidx.annotation.Nullable;
|
|||
|
||||
import app.organicmaps.R;
|
||||
import app.organicmaps.base.BaseMwmToolbarFragment;
|
||||
import app.organicmaps.util.NetworkPolicy;
|
||||
import app.organicmaps.util.UiUtils;
|
||||
import app.organicmaps.util.Utils;
|
||||
import app.organicmaps.util.concurrency.ThreadPool;
|
||||
import app.organicmaps.util.concurrency.UiThread;
|
||||
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import java.text.NumberFormat;
|
||||
|
||||
public class ProfileFragment extends BaseMwmToolbarFragment
|
||||
{
|
||||
private View mUserInfoBlock;
|
||||
private TextView mEditsSent;
|
||||
private TextView mProfileName;
|
||||
private ImageView mProfileImage;
|
||||
|
@ -50,43 +51,56 @@ public class ProfileFragment extends BaseMwmToolbarFragment
|
|||
{
|
||||
View logoutButton = getToolbarController().getToolbar().findViewById(R.id.logout);
|
||||
logoutButton.setOnClickListener((v) -> logout());
|
||||
mUserInfoBlock = view.findViewById(R.id.block_user_info);
|
||||
View mUserInfoBlock = view.findViewById(R.id.block_user_info);
|
||||
mProfileInfoLoading = view.findViewById(R.id.user_profile_loading);
|
||||
mEditsSent = mUserInfoBlock.findViewById(R.id.user_sent_edits);
|
||||
mProfileName = mUserInfoBlock.findViewById(R.id.user_profile_name);
|
||||
mProfileImage = mUserInfoBlock.findViewById(R.id.user_profile_image);
|
||||
view.findViewById(R.id.about_osm).setOnClickListener((v) -> Utils.openUrl(requireActivity(), getString(R.string.osm_wiki_about_url)));
|
||||
view.findViewById(R.id.osm_history).setOnClickListener((v) -> Utils.openUrl(requireActivity(), OsmOAuth.getHistoryUrl(requireContext())));
|
||||
|
||||
}
|
||||
|
||||
private void refreshViews()
|
||||
{
|
||||
// If logged in
|
||||
if (OsmOAuth.isAuthorized(requireContext()))
|
||||
{
|
||||
// Update the number of uploaded changesets from OSM.
|
||||
ThreadPool.getWorker().execute(() -> {
|
||||
if (mEditsSent.getText().equals(""))
|
||||
{
|
||||
UiUtils.show(mProfileInfoLoading);
|
||||
UiUtils.hide(mUserInfoBlock);
|
||||
}
|
||||
final int profileEditCount = OsmOAuth.getOsmChangesetsCount(requireContext(), getParentFragmentManager());
|
||||
final String profileUsername = OsmOAuth.getUsername(requireContext());
|
||||
final Bitmap profilePicture = OsmOAuth.getProfilePicture(requireContext());
|
||||
// Get/Display cached values first
|
||||
final int cachedProfileEditCount = OsmOAuth.getOsmChangesetsCount(requireContext(), false);
|
||||
final String cachedProfilePicture = OsmOAuth.getProfilePicturePath(requireContext(), false);
|
||||
final String cachedProfileUsername = OsmOAuth.getUsername(requireContext(), false);
|
||||
|
||||
UiThread.run(() -> {
|
||||
mEditsSent.setText(NumberFormat.getInstance().format(profileEditCount));
|
||||
mProfileName.setText(profileUsername);
|
||||
|
||||
mEditsSent.setText(NumberFormat.getInstance().format(cachedProfileEditCount));
|
||||
mProfileName.setText(cachedProfileUsername);
|
||||
// Use generic image if user has no profile picture or it failed to load.
|
||||
if (profilePicture != null)
|
||||
mProfileImage.setImageBitmap(profilePicture);
|
||||
if (!cachedProfilePicture.isEmpty())
|
||||
mProfileImage.setImageBitmap(BitmapFactory.decodeFile(cachedProfilePicture));
|
||||
else
|
||||
mProfileImage.setImageResource(R.drawable.profile_generic);
|
||||
});
|
||||
|
||||
![]()
```suggestion
final View userInfoBlock = view.findViewById(R.id.block_user_info);
```
|
||||
UiUtils.show(mUserInfoBlock);
|
||||
UiUtils.hide(mProfileInfoLoading);
|
||||
|
||||
// Then try to cache/display online values
|
||||
NetworkPolicy.checkNetworkPolicy(getParentFragmentManager(), policy -> {
|
||||
if (policy.canUseNetwork())
|
||||
{
|
||||
UiUtils.show(mProfileInfoLoading);
|
||||
final int newProfileEditCount = OsmOAuth.getOsmChangesetsCount(requireContext(), true);
|
||||
final String newProfileUsername = OsmOAuth.getUsername(requireContext(), true);
|
||||
final String newProfilePicture = OsmOAuth.getProfilePicturePath(requireContext(), true);
|
||||
|
||||
UiThread.run(() -> {
|
||||
mEditsSent.setText(NumberFormat.getInstance().format(newProfileEditCount));
|
||||
mProfileName.setText(newProfileUsername);
|
||||
// Needed in case user removed picture online, to
|
||||
if (!newProfilePicture.isEmpty())
|
||||
mProfileImage.setImageBitmap(BitmapFactory.decodeFile(newProfilePicture));
|
||||
|
||||
UiUtils.hide(mProfileInfoLoading);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -83,7 +83,8 @@ public class SettingsPrefsFragment extends BaseXmlSettingsFragment
|
|||
final Preference pref = getPreference(getString(R.string.pref_osm_profile));
|
||||
if (OsmOAuth.isAuthorized(requireContext()))
|
||||
{
|
||||
final String username = OsmOAuth.getUsername(requireContext());
|
||||
// Only update profile when visiting profile page
|
||||
final String username = OsmOAuth.getUsername(requireContext(), false);
|
||||
pref.setSummary(username);
|
||||
}
|
||||
else
|
||||
|
|
|
@ -49,7 +49,7 @@
|
|||
android:layout_width="100dp"
|
||||
android:layout_height="match_parent"
|
||||
android:importantForAccessibility="no"
|
||||
app:srcCompat="@drawable/profile_generic" />
|
||||
tools:src="@drawable/profile_generic" />
|
||||
<LinearLayout
|
||||
![]() Why is this change needed? Why is this change needed?
|
||||
android:id="@+id/text"
|
||||
android:layout_marginStart="@dimen/margin_base"
|
||||
|
|
|
@ -78,6 +78,7 @@
|
|||
#define WORLD_COASTS_FILE_NAME "WorldCoasts"
|
||||
|
||||
#define SETTINGS_FILE_NAME "settings.ini"
|
||||
#define PROFILE_PICTURE_FILENAME "profile_picture"
|
||||
![]() Why it should be in defines.hpp? Why it should be in defines.hpp?
|
||||
#define MARKETING_SETTINGS_FILE_NAME "marketing_settings.ini"
|
||||
|
||||
#define SEARCH_CATEGORIES_FILE_NAME "categories.txt"
|
||||
|
|
|
@ -26,6 +26,8 @@ set(SRC
|
|||
osm_auth.hpp
|
||||
osm_editor.cpp
|
||||
osm_editor.hpp
|
||||
profile_picture.cpp
|
||||
profile_picture.hpp
|
||||
server_api.cpp
|
||||
server_api.hpp
|
||||
ui2oh.cpp
|
||||
|
|
54
editor/profile_picture.cpp
Normal file
54
editor/profile_picture.cpp
Normal file
|
@ -0,0 +1,54 @@
|
|||
#include "editor/profile_picture.hpp"
|
||||
|
||||
#include "platform/remote_file.hpp"
|
||||
#include "platform/platform.hpp"
|
||||
#include "platform/settings.hpp"
|
||||
|
||||
#include "filesystem"
|
||||
|
||||
// Example pic_url: https://www.openstreetmap.org/rails/active_storage/representations/redirect/eyJfcmFpbHMiOnsiZGF0YSI6MzM1MjcxMDUsInB1ciI6ImJsb2JfaWQifX0=--957a2a99111c97d4b9f4181003c3e69713a221c6/eyJfcmFpbHMiOnsiZGF0YSI6eyJmb3JtYXQiOiJwbmciLCJyZXNpemVfdG9fbGltaXQiOlsxMDAsMTAwXX0sInB1ciI6InZhcmlhdGlvbiJ9fQ==--473f0190a9e741bb16565db85fe650d7b0a9ee69/cat.png
|
||||
// Example hash: 473f0190a9e741bb16565db85fe650d7b0a9ee69
|
||||
namespace editor
|
||||
{
|
||||
std::string ProfilePicture::download(const std::string& pic_url)
|
||||
![]() Is there any generic "download file" api already somewhere? If no, it would be better to add a generic "download a file" API, with an optional hint of expected MIME file type if necessary, so it can be reused to download anything. Is there any generic "download file" api already somewhere? If no, it would be better to add a generic "download a file" API, with an optional hint of expected MIME file type if necessary, so it can be reused to download anything.
|
||||
{
|
||||
std::string image_path = GetPlatform().WritablePathForFile(PROFILE_PICTURE_FILENAME);
|
||||
bool image_exists = std::filesystem::exists(image_path);
|
||||
|
||||
if(!pic_url.empty()) // If internet available
|
||||
{
|
||||
if(pic_url != "none")
|
||||
![]() Using a lot of logic here looks cumbersome, it may be cleaner to split it. Maybe:
Using a lot of logic here looks cumbersome, it may be cleaner to split it. Maybe:
- Upper level getter retrieves an image
-- if there's a cached image, then it is returned
-- if no cached image, it is downloaded
--- if download fails, it will be repeated next time, a stub image is used.
--- if user doesn't have a picture, this state is cached and generic image is used.
--- image is cached and returned
|
||||
{
|
||||
// Get new hash (hash start always includes '=--' but is usually '==--')
|
||||
const int hash_start = static_cast<int>(pic_url.rfind("=--") + 3);
|
||||
const int length = static_cast<int>(pic_url.rfind('/') - hash_start);
|
||||
std::string new_hash = pic_url.substr(hash_start, length);
|
||||
|
||||
// Get cached hash
|
||||
std::string current_hash;
|
||||
settings::StringStorage::Instance().GetValue("ProfilePictureHash", current_hash);
|
||||
|
||||
// Download new image
|
||||
if (new_hash != current_hash || !image_exists)
|
||||
{
|
||||
settings::StringStorage::Instance().SetValue("ProfilePictureHash", std::move(new_hash));
|
||||
platform::RemoteFile remoteFile(pic_url);
|
||||
remoteFile.Download(image_path);
|
||||
image_exists = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// User has removed profile picture online
|
||||
settings::StringStorage::Instance().DeleteKeyAndValue("ProfilePictureHash");
|
||||
std::filesystem::remove(image_path);
|
||||
![]() Is std::filesystem available in the iOS? Is std::filesystem available in the iOS?
|
||||
image_exists = false;
|
||||
}
|
||||
}
|
||||
|
||||
if(image_exists)
|
||||
return image_path;
|
||||
else
|
||||
return {};
|
||||
}
|
||||
}
|
14
editor/profile_picture.hpp
Normal file
14
editor/profile_picture.hpp
Normal file
|
@ -0,0 +1,14 @@
|
|||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace editor
|
||||
{
|
||||
class ProfilePicture
|
||||
![]() Can UserPreferences class be used instead of introducing this wrapper? Can UserPreferences class be used instead of introducing this wrapper?
|
||||
{
|
||||
public:
|
||||
// Returns downloaded image or cached image if one exists.
|
||||
// if not, an empty string is returned.
|
||||
static std::string download(const std::string& pic_url);
|
||||
![]() There should be a cpp code style doc.
There should be a cpp code style doc.
```suggestion
static std::string download(std::string const & pic_url);
```
|
||||
};
|
||||
}
|
|
@ -167,8 +167,9 @@ UserPreferences ServerApi06::GetUserPreferences() const
|
|||
pref.m_id = user.attribute("id").as_ullong();
|
||||
pref.m_displayName = user.attribute("display_name").as_string();
|
||||
pref.m_accountCreated = base::StringToTimestamp(user.attribute("account_created").as_string());
|
||||
pref.m_imageUrl = user.child("img").attribute("href").as_string();
|
||||
pref.m_changesets = user.child("changesets").attribute("count").as_uint();
|
||||
// Return "none if profile has no 'img', to differentiate from no network
|
||||
pref.m_imageUrl = user.child("img")? user.child("img").attribute("href").as_string() : "none";
|
||||
![]() This function should throw if there's a network error. Is "none" here really needed? Can an empty string be used instead? This function should throw if there's a network error. Is "none" here really needed? Can an empty string be used instead?
|
||||
return pref;
|
||||
}
|
||||
|
||||
|
|
Reference in a new issue