Compare commits

...
Sign in to create a new pull request.

13 commits

Author SHA1 Message Date
44aff764c6 [Android] Using Utils.openUri(...)
Signed-off-by: Sergiy Kozyr <s.trump@gmail.com>
2024-08-20 10:22:16 +03:00
f692cb4caf [Android] Added round corners for OSM Login bottom sheet
Signed-off-by: Sergiy Kozyr <s.trump@gmail.com>
2024-08-20 10:15:31 +03:00
6fb9dbc440 Fix compilation error
Signed-off-by: Sergiy Kozyr <s.trump@gmail.com>
2024-08-20 09:53:02 +03:00
7b2fedb93a Code formatting
Signed-off-by: Sergiy Kozyr <s.trump@gmail.com>
2024-08-20 09:45:33 +03:00
6371fe09ef
Apply suggestions from code review
Co-authored-by: Alexander Borsuk <170263+biodranik@users.noreply.github.com>
Signed-off-by: Sergiy Kozyr <s.trump@gmail.com>
2024-08-20 09:39:35 +03:00
db1552c1bf Fixed landscape layout for OSM Login UI.
Set username+password bottom panel behaviour to expand by default.

Signed-off-by: Sergiy Kozyr <s.trump@gmail.com>
2024-08-19 20:00:48 +03:00
7cc56dd318 [Android] Build OAuth2 URL using native code instead of Java
Small changes according to PR comments

Signed-off-by: Sergiy Kozyr <s.trump@gmail.com>
2024-08-19 17:26:51 +03:00
c6a0ad53c3 Changed OSM login UI with 2 options: login+password or via browser.
Signed-off-by: Sergiy Kozyr <s.trump@gmail.com>
2024-08-19 17:03:21 +03:00
558b65398e [ios] fixed compilation error
Signed-off-by: Sergiy Kozyr <s.trump@gmail.com>
2024-08-19 17:03:21 +03:00
8edf155970 Improved tests for OSM OAuth2 URI parsing
Signed-off-by: Sergiy Kozyr <s.trump@gmail.com>
2024-08-19 17:03:21 +03:00
9081d45d9a Completed OAuth2 flow in OsmLoginFragment
Signed-off-by: Sergiy Kozyr <s.trump@gmail.com>
2024-08-19 17:03:21 +03:00
c1cf2b5c94 Implemented parsing of om://oauth2/osm/callback URLs
Signed-off-by: Sergiy Kozyr <s.trump@gmail.com>
2024-08-19 17:03:20 +03:00
cc32c18732 Added draft of "Login with OSM website" button
Signed-off-by: Sergiy Kozyr <s.trump@gmail.com>
2024-08-19 17:03:20 +03:00
25 changed files with 596 additions and 233 deletions

View file

@ -862,6 +862,13 @@ Java_app_organicmaps_Framework_nativeGetParsedAppName(JNIEnv * env, jclass)
return (appName.empty()) ? nullptr : jni::ToJavaString(env, appName);
}
JNIEXPORT jstring JNICALL
Java_app_organicmaps_Framework_nativeGetParsedOAuth2Code(JNIEnv * env, jclass)
{
std::string const & code = frm()->GetParsedOAuth2Code();
return jni::ToJavaString(env, code);
}
JNIEXPORT jstring JNICALL
Java_app_organicmaps_Framework_nativeGetParsedBackUrl(JNIEnv * env, jclass)
{

View file

@ -34,6 +34,13 @@ bool LoadOsmUserPreferences(std::string const & oauthToken, UserPreferences & ou
extern "C"
{
JNIEXPORT jstring JNICALL
Java_app_organicmaps_editor_OsmOAuth_nativeGetOAuth2Url(JNIEnv * env, jclass)
{
auto const auth = OsmOAuth::ServerAuth();
return ToJavaString(env, auth.BuildOAuth2Url());
}
JNIEXPORT jstring JNICALL
Java_app_organicmaps_editor_OsmOAuth_nativeAuthWithPassword(JNIEnv * env, jclass clazz,
jstring login, jstring password)
@ -52,6 +59,27 @@ Java_app_organicmaps_editor_OsmOAuth_nativeAuthWithPassword(JNIEnv * env, jclass
return nullptr;
}
JNIEXPORT jstring JNICALL
Java_app_organicmaps_editor_OsmOAuth_nativeAuthWithOAuth2Code(JNIEnv * env, jclass, jstring oauth2code)
{
OsmOAuth auth = OsmOAuth::ServerAuth();
try
{
auto token = auth.FinishAuthorization(ToNativeString(env, oauth2code));
if (!token.empty())
{
auth.SetAuthToken(token);
return ToJavaString(env, token);
}
LOG(LWARNING, ("nativeAuthWithOAuth2Code: invalid OAuth2 code", oauth2code));
}
catch (std::exception const & ex)
{
LOG(LWARNING, ("nativeAuthWithOAuth2Code error ", ex.what()));
}
return nullptr;
}
JNIEXPORT jstring JNICALL
Java_app_organicmaps_editor_OsmOAuth_nativeGetOsmUsername(JNIEnv * env, jclass, jstring oauthToken)
{

View file

@ -239,6 +239,7 @@ public class Framework
public static native ParsedRoutingData nativeGetParsedRoutingData();
public static native ParsedSearchRequest nativeGetParsedSearchRequest();
public static native @Nullable String nativeGetParsedAppName();
public static native @Nullable String nativeGetParsedOAuth2Code();
@Nullable @Size(2)
public static native double[] nativeGetParsedCenterLatLon();
public static native @Nullable String nativeGetParsedBackUrl();

View file

@ -6,7 +6,7 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@Retention(RetentionPolicy.SOURCE)
@IntDef({RequestType.INCORRECT, RequestType.MAP, RequestType.ROUTE, RequestType.SEARCH, RequestType.CROSSHAIR})
@IntDef({RequestType.INCORRECT, RequestType.MAP, RequestType.ROUTE, RequestType.SEARCH, RequestType.CROSSHAIR, RequestType.OAUTH2})
public @interface RequestType
{
// Represents url_scheme::ParsedMapApi::UrlType from c++ part.
@ -15,4 +15,5 @@ public @interface RequestType
public static final int ROUTE = 2;
public static final int SEARCH = 3;
public static final int CROSSHAIR = 4;
public static final int OAUTH2 = 5;
}

View file

@ -1,14 +1,31 @@
package app.organicmaps.editor;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import app.organicmaps.base.BaseMwmFragmentActivity;
public class OsmLoginActivity extends BaseMwmFragmentActivity
{
public static final String EXTRA_OAUTH2CODE = "oauth2code";
@Override
protected Class<? extends Fragment> getFragmentClass()
{
return OsmLoginFragment.class;
}
public static void OAuth2Callback(@NonNull Activity activity, String oauth2code)
{
final Intent i = new Intent(activity, OsmLoginActivity.class);
Bundle args = new Bundle();
args.putString(EXTRA_OAUTH2CODE, oauth2code);
args.putBoolean(ProfileActivity.EXTRA_REDIRECT_TO_PROFILE, true);
i.putExtras(args);
activity.startActivity(i);
}
}

View file

@ -0,0 +1,124 @@
package app.organicmaps.editor;
import android.app.Dialog;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.ProgressBar;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.android.material.bottomsheet.BottomSheetBehavior;
import com.google.android.material.bottomsheet.BottomSheetDialog;
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
import com.google.android.material.textfield.TextInputEditText;
import app.organicmaps.R;
import app.organicmaps.util.Constants;
import app.organicmaps.util.InputUtils;
import app.organicmaps.util.UiUtils;
import app.organicmaps.util.Utils;
import app.organicmaps.util.concurrency.ThreadPool;
import app.organicmaps.util.concurrency.UiThread;
public class OsmLoginBottomFragment extends BottomSheetDialogFragment
{
final private OsmLoginFragment parentFragment;
private TextInputEditText mLoginInput;
private TextInputEditText mPasswordInput;
private Button mLoginButton;
private Button mLostPasswordButton;
private Button mRegisterButton;
private ProgressBar mProgress;
public OsmLoginBottomFragment(OsmLoginFragment parentFragment)
{
super();
this.parentFragment = parentFragment;
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState)
{
return inflater.inflate(R.layout.fragment_osm_login_bottom, container, false);
}
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState)
{
BottomSheetDialog dialog = (BottomSheetDialog) super.onCreateDialog(savedInstanceState);
dialog.getBehavior().setState(BottomSheetBehavior.STATE_EXPANDED);
return dialog;
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState)
{
mLoginInput = view.findViewById(R.id.osm_username);
mPasswordInput = view.findViewById(R.id.osm_password);
mLoginButton = view.findViewById(R.id.login);
mLoginButton.setOnClickListener((v) -> doLogin());
mLostPasswordButton = view.findViewById(R.id.lost_password);
mLostPasswordButton.setOnClickListener((v) -> Utils.openUrl(requireActivity(), Constants.Url.OSM_RECOVER_PASSWORD));
mRegisterButton = view.findViewById(R.id.register);
mRegisterButton.setOnClickListener((v) -> Utils.openUrl(requireActivity(), Constants.Url.OSM_REGISTER));
mProgress = view.findViewById(R.id.osm_login_progress);
}
private void doLogin()
{
InputUtils.hideKeyboard(mLoginInput);
final String username = mLoginInput.getText().toString().trim();
final String password = mPasswordInput.getText().toString();
enableInput(false);
UiUtils.show(mProgress);
mLoginButton.setText("");
ThreadPool.getWorker().execute(() ->
{
final String oauthToken = OsmOAuth.nativeAuthWithPassword(username, password);
final String username1 = (oauthToken == null) ? null : OsmOAuth.nativeGetOsmUsername(oauthToken);
UiThread.run(() -> processAuth(oauthToken, username1));
});
}
private void processAuth(String oauthToken, String username)
{
if (!isAdded())
return;
enableInput(true);
UiUtils.hide(mProgress);
mLoginButton.setText(R.string.login_osm);
if (oauthToken == null)
parentFragment.onAuthFail();
else
{
this.dismiss();
parentFragment.onAuthSuccess(oauthToken, username);
}
}
private void enableInput(boolean enable)
{
mPasswordInput.setEnabled(enable);
mLoginInput.setEnabled(enable);
mLoginButton.setEnabled(enable);
mLostPasswordButton.setEnabled(enable);
mRegisterButton.setEnabled(enable);
}
}

View file

@ -1,37 +1,34 @@
package app.organicmaps.editor;
import android.content.ActivityNotFoundException;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.ProgressBar;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import app.organicmaps.Framework;
import app.organicmaps.R;
import app.organicmaps.base.BaseMwmToolbarFragment;
import app.organicmaps.util.Constants;
import app.organicmaps.util.DateUtils;
import app.organicmaps.util.InputUtils;
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 com.google.android.material.textfield.TextInputEditText;
public class OsmLoginFragment extends BaseMwmToolbarFragment
{
private ProgressBar mProgress;
private Button mLoginButton;
private Button mLostPasswordButton;
private TextInputEditText mLoginInput;
private TextInputEditText mPasswordInput;
private Button mLoginUsernameButton;
private Button mLoginWebsiteButton;
private String mArgOAuth2Code;
@Nullable
@Override
@ -45,71 +42,85 @@ public class OsmLoginFragment extends BaseMwmToolbarFragment
{
super.onViewCreated(view, savedInstanceState);
getToolbarController().setTitle(R.string.login);
mLoginInput = view.findViewById(R.id.osm_username);
mPasswordInput = view.findViewById(R.id.osm_password);
mLoginButton = view.findViewById(R.id.login);
mLoginButton.setOnClickListener((v) -> login());
mLostPasswordButton = view.findViewById(R.id.lost_password);
mLostPasswordButton.setOnClickListener((v) -> Utils.openUrl(requireActivity(), Constants.Url.OSM_RECOVER_PASSWORD));
Button registerButton = view.findViewById(R.id.register);
registerButton.setOnClickListener((v) -> Utils.openUrl(requireActivity(), Constants.Url.OSM_REGISTER));
mProgress = view.findViewById(R.id.osm_login_progress);
mLoginWebsiteButton = view.findViewById(R.id.login_website);
mLoginWebsiteButton.setOnClickListener((v) -> loginWithBrowser());
mLoginUsernameButton = view.findViewById(R.id.login_username);
mLoginUsernameButton.setOnClickListener((v) -> loginUsername());
final String dataVersion = DateUtils.getShortDateFormatter().format(Framework.getDataVersion());
((TextView) view.findViewById(R.id.osm_presentation))
.setText(getString(R.string.osm_presentation, dataVersion));
readArguments();
if (mArgOAuth2Code != null && !mArgOAuth2Code.isEmpty())
continueOAuth2Flow(mArgOAuth2Code);
}
private void login()
private void readArguments()
{
InputUtils.hideKeyboard(mLoginInput);
final String username = mLoginInput.getText().toString().trim();
final String password = mPasswordInput.getText().toString();
enableInput(false);
UiUtils.show(mProgress);
mLoginButton.setText("");
final Bundle arguments = getArguments();
if (arguments == null)
return;
ThreadPool.getWorker().execute(() -> {
final String oauthToken = OsmOAuth.nativeAuthWithPassword(username, password);
final String username1 = (oauthToken == null) ? null : OsmOAuth.nativeGetOsmUsername(oauthToken);
UiThread.run(() -> processAuth(oauthToken, username1));
});
mArgOAuth2Code = arguments.getString(OsmLoginActivity.EXTRA_OAUTH2CODE);
}
private void enableInput(boolean enable)
private void loginUsername()
{
mPasswordInput.setEnabled(enable);
mLoginInput.setEnabled(enable);
mLoginButton.setEnabled(enable);
mLostPasswordButton.setEnabled(enable);
OsmLoginBottomFragment bottomSheetFragment = new OsmLoginBottomFragment(this);
bottomSheetFragment.show(requireActivity().getSupportFragmentManager(), bottomSheetFragment.getTag());
}
private void processAuth(String oauthToken, String username)
private void loginWithBrowser()
{
Utils.openUri(requireContext(), Uri.parse(OsmOAuth.nativeGetOAuth2Url()));
}
// This method is called by MwmActivity & UrlProcessor when "om://oauth2/osm/callback?code=XXX" is handled
private void continueOAuth2Flow(String oauth2code)
{
if (!isAdded())
return;
if (oauth2code == null || oauth2code.isEmpty())
onAuthFail();
else
{
ThreadPool.getWorker().execute(() -> {
// Finish OAuth2 auth flow and get username for UI.
final String oauthToken = OsmOAuth.nativeAuthWithOAuth2Code(oauth2code);
final String username = (oauthToken == null) ? null : OsmOAuth.nativeGetOsmUsername(oauthToken);
UiThread.run(() -> {
processAuth(oauthToken, username);
});
});
}
}
public void processAuth(String oauthToken, String username)
{
if (!isAdded())
return;
enableInput(true);
UiUtils.hide(mProgress);
mLoginButton.setText(R.string.login_osm);
if (oauthToken == null)
onAuthFail();
else
onAuthSuccess(oauthToken, username);
}
private void onAuthFail()
public void onAuthFail()
{
new MaterialAlertDialogBuilder(requireActivity(), R.style.MwmTheme_AlertDialog)
.setTitle(R.string.editor_login_error_dialog)
.setPositiveButton(R.string.ok, null)
.show();
.setTitle(R.string.editor_login_error_dialog)
.setPositiveButton(R.string.ok, null)
.show();
}
private void onAuthSuccess(String oauthToken, String username)
public void onAuthSuccess(String oauthToken, String username)
{
OsmOAuth.setAuthorization(requireContext(), oauthToken, username);
final Bundle extras = requireActivity().getIntent().getExtras();
if (extras != null && extras.getBoolean("redirectToProfile", false))
if (extras != null && extras.getBoolean(ProfileActivity.EXTRA_REDIRECT_TO_PROFILE, false))
startActivity(new Intent(requireContext(), ProfileActivity.class));
requireActivity().finish();
}

View file

@ -10,6 +10,8 @@ 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;
@ -98,6 +100,12 @@ public final class OsmOAuth
return nativeGetHistoryUrl(getUsername(context));
}
/*
Returns 5 strings: ServerURL, ClientId, ClientSecret, Scope, RedirectUri
*/
@NonNull
public static native String nativeGetOAuth2Url();
/**
* @return string with OAuth2 token
*/
@ -106,6 +114,13 @@ public final class OsmOAuth
@Nullable
public static native String nativeAuthWithPassword(String login, String password);
/**
* @return string with OAuth2 token
*/
@WorkerThread
@Nullable
public static native String nativeAuthWithOAuth2Code(String oauth2code);
@WorkerThread
@Nullable
public static native String nativeGetOsmUsername(String oauthToken);

View file

@ -6,6 +6,8 @@ import app.organicmaps.base.BaseMwmFragmentActivity;
public class ProfileActivity extends BaseMwmFragmentActivity
{
public static final String EXTRA_REDIRECT_TO_PROFILE = "redirectToProfile";
@Override
protected Class<? extends Fragment> getFragmentClass()
{

View file

@ -93,7 +93,7 @@ public class ProfileFragment extends BaseMwmToolbarFragment
else
{
Intent intent = new Intent(requireContext(), OsmLoginActivity.class);
intent.putExtra("redirectToProfile", true);
intent.putExtra(ProfileActivity.EXTRA_REDIRECT_TO_PROFILE, true);
startActivity(intent);
requireActivity().finish();
}

View file

@ -3,6 +3,7 @@ package app.organicmaps.intent;
import android.content.ContentResolver;
import android.content.Intent;
import android.net.Uri;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.core.content.IntentCompat;
@ -18,6 +19,7 @@ import app.organicmaps.api.RoutePoint;
import app.organicmaps.bookmarks.data.BookmarkManager;
import app.organicmaps.bookmarks.data.FeatureId;
import app.organicmaps.bookmarks.data.MapObject;
import app.organicmaps.editor.OsmLoginActivity;
import app.organicmaps.routing.RoutingController;
import app.organicmaps.search.SearchActivity;
import app.organicmaps.search.SearchEngine;
@ -128,6 +130,15 @@ public class Factory
Framework.nativeSetViewportCenter(latlon[0], latlon[1], SEARCH_IN_VIEWPORT_ZOOM);
}
return true;
}
case RequestType.OAUTH2:
{
SearchEngine.INSTANCE.cancelInteractiveSearch();
final String oauth2code = Framework.nativeGetParsedOAuth2Code();
OsmLoginActivity.OAuth2Callback(target, oauth2code);
return true;
}
}

View file

@ -63,119 +63,32 @@
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_half">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/osm_username_container"
style="@style/MwmWidget.Editor.CustomTextInput"
android:layout_width="0dp"
android:layout_marginEnd="@dimen/margin_half"
android:textColorHint="?android:textColorSecondary"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/osm_password_container"
app:layout_constraintTop_toTopOf="parent"
app:endIconMode="custom">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/osm_username"
style="@style/MwmWidget.Editor.FieldLayout.EditText"
android:autofillHints="emailAddress"
android:hint="@string/email_or_username" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/osm_password_container"
style="@style/MwmWidget.Editor.CustomTextInput"
android:layout_width="0dp"
android:textColorHint="?android:textColorSecondary"
app:layout_constraintStart_toEndOf="@id/osm_username_container"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:endIconMode="password_toggle"
app:endIconTint="?android:textColorSecondary">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/osm_password"
style="@style/MwmWidget.Editor.FieldLayout.EditText"
android:autofillHints="password"
android:hint="@string/password"
android:inputType="textPassword" />
</com.google.android.material.textfield.TextInputLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/margin_base">
<FrameLayout
android:id="@+id/login_container"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/lost_password"
app:layout_constraintTop_toTopOf="parent">
<Button
android:id="@+id/login"
style="@style/MwmWidget.Button.Accent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/login_osm"
android:textAppearance="@style/MwmTextAppearance.Body2.Light" />
<ProgressBar
android:id="@+id/osm_login_progress"
android:layout_width="@dimen/editor_auth_btn_height"
android:layout_height="@dimen/editor_auth_btn_height"
android:layout_gravity="center"
android:elevation="@dimen/design_fab_elevation"
android:visibility="gone" />
</FrameLayout>
<Button
android:id="@+id/lost_password"
style="@style/MwmWidget.Button"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:background="?clickableBackground"
android:gravity="start|center_vertical"
android:padding="@dimen/margin_half"
android:text="@string/forgot_password"
android:textAppearance="@style/MwmTextAppearance.Body3"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/login_container"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<com.google.android.material.divider.MaterialDivider
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_base"
android:layout_marginBottom="@dimen/margin_base" />
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/register_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:fontFamily="@string/robotoRegular"
android:gravity="start|center_vertical"
android:text="@string/no_osm_account"
android:textAppearance="@style/MwmTextAppearance.Body2"
android:textColor="?android:textColorPrimary"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/register"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
android:layout_marginBottom="@dimen/margin_base">
<Button
android:id="@+id/register"
android:id="@+id/login_website"
style="@style/MwmWidget.Button.Accent"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:background="@drawable/button_editor_light"
android:fontFamily="@string/robotoMedium"
android:padding="@dimen/margin_quarter"
android:text="@string/register_at_openstreetmap"
android:textAppearance="@style/MwmTextAppearance.Body2"
android:textColor="@color/text_dark"
android:padding="@dimen/margin_half"
android:layout_marginEnd="@dimen/margin_base"
android:text="Login with OSM website"
android:textAppearance="@style/MwmTextAppearance.Body2.Light"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/register_text"
app:layout_constraintEnd_toStartOf="@id/login_username"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/login_username"
style="@style/MwmWidget.Button.Accent"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="Login with username"
android:textAppearance="@style/MwmTextAppearance.Body2.Light"
app:layout_constraintStart_toEndOf="@id/login_website"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>
</ScrollView>

View file

@ -0,0 +1,117 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<include
layout="@layout/bottom_sheet_handle" />
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?cardBackground"
android:fadeScrollbars="false"
android:fillViewport="true"
tools:ignore="DuplicateIds">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipChildren="false"
android:clipToPadding="false"
android:orientation="vertical"
android:padding="@dimen/margin_base"
tools:ignore="ScrollViewSize">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_half">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/osm_username_container"
style="@style/MwmWidget.Editor.CustomTextInput"
android:layout_width="0dp"
android:layout_marginEnd="@dimen/margin_half"
android:textColorHint="?android:textColorSecondary"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/osm_password_container"
app:layout_constraintTop_toTopOf="parent"
app:endIconMode="custom">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/osm_username"
style="@style/MwmWidget.Editor.FieldLayout.EditText"
android:autofillHints="emailAddress"
android:hint="@string/email_or_username" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/osm_password_container"
style="@style/MwmWidget.Editor.CustomTextInput"
android:layout_width="0dp"
android:textColorHint="?android:textColorSecondary"
app:layout_constraintStart_toEndOf="@id/osm_username_container"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:endIconMode="password_toggle"
app:endIconTint="?android:textColorSecondary">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/osm_password"
style="@style/MwmWidget.Editor.FieldLayout.EditText"
android:autofillHints="password"
android:hint="@string/password"
android:inputType="textPassword" />
</com.google.android.material.textfield.TextInputLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_base">
<Button
android:id="@+id/login"
style="@style/MwmWidget.Button.Accent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Login with username"
android:layout_marginTop="@dimen/margin_half"
android:textAppearance="@style/MwmTextAppearance.Body2.Light" />
<ProgressBar
android:id="@+id/osm_login_progress"
android:layout_width="@dimen/editor_auth_btn_height"
android:layout_height="@dimen/editor_auth_btn_height"
android:layout_gravity="center"
android:elevation="@dimen/design_fab_elevation"
android:visibility="gone" />
</FrameLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginTop="@dimen/margin_base">
<Button
android:id="@+id/lost_password"
style="@style/MwmWidget.Button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start"
android:layout_weight="1"
android:background="?clickableBackground"
android:padding="@dimen/margin_quarter"
android:text="@string/forgot_password"
android:textAppearance="@style/MwmTextAppearance.Body3" />
<Button
android:id="@+id/register"
style="@style/MwmWidget.Button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_gravity="end"
android:background="?clickableBackground"
android:padding="@dimen/margin_quarter"
android:text="@string/register_at_openstreetmap"
android:textAppearance="@style/MwmTextAppearance.Body3" />
</LinearLayout>
</LinearLayout>
</ScrollView>
</LinearLayout>

View file

@ -60,83 +60,22 @@
android:text="@string/login_to_make_edits_visible"
android:textAppearance="@style/MwmTextAppearance.Body2"
android:textColor="?android:textColorPrimary" />
<com.google.android.material.textfield.TextInputLayout
style="@style/MwmWidget.Editor.CustomTextInput"
android:layout_marginTop="@dimen/margin_half"
android:textColorHint="?android:textColorSecondary"
app:endIconMode="custom">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/osm_username"
style="@style/MwmWidget.Editor.FieldLayout.EditText"
android:autofillHints="emailAddress"
android:hint="@string/email_or_username" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
style="@style/MwmWidget.Editor.CustomTextInput"
<Button
android:id="@+id/login_website"
style="@style/MwmWidget.Button.Accent"
android:layout_marginTop="@dimen/margin_base"
android:textColorHint="?android:textColorSecondary"
app:endIconMode="password_toggle"
app:endIconTint="?android:textColorSecondary">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/osm_password"
style="@style/MwmWidget.Editor.FieldLayout.EditText"
android:autofillHints="password"
android:hint="@string/password"
android:inputType="textPassword" />
</com.google.android.material.textfield.TextInputLayout>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/margin_base">
<Button
android:id="@+id/login"
style="@style/MwmWidget.Button.Accent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/login_osm"
android:textAppearance="@style/MwmTextAppearance.Body2.Light" />
<ProgressBar
android:id="@+id/osm_login_progress"
android:layout_width="@dimen/editor_auth_btn_height"
android:layout_height="@dimen/editor_auth_btn_height"
android:layout_gravity="center"
android:elevation="@dimen/design_fab_elevation"
android:visibility="gone" />
</FrameLayout>
android:text="Login with OSM website"
android:textAppearance="@style/MwmTextAppearance.Body2.Light" />
<Button
android:id="@+id/lost_password"
style="@style/MwmWidget.Button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:background="?clickableBackground"
android:padding="@dimen/margin_half"
android:text="@string/forgot_password"
android:textAppearance="@style/MwmTextAppearance.Body3" />
<com.google.android.material.divider.MaterialDivider
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_base"
android:layout_marginBottom="@dimen/margin_base" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fontFamily="@string/robotoRegular"
android:text="@string/no_osm_account"
android:textAppearance="@style/MwmTextAppearance.Body2"
android:textColor="?android:textColorPrimary" />
<Button
android:id="@+id/register"
android:id="@+id/login_username"
style="@style/MwmWidget.Button.Accent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Login with username"
android:layout_marginTop="@dimen/margin_base"
android:background="@drawable/button_editor_light"
android:fontFamily="@string/robotoMedium"
android:padding="@dimen/margin_quarter"
android:text="@string/register_at_openstreetmap"
android:textAppearance="@style/MwmTextAppearance.Body2"
android:textColor="@color/text_dark" />
android:textAppearance="@style/MwmTextAppearance.Body2.Light" />
</LinearLayout>
</ScrollView>
</LinearLayout>

View file

@ -0,0 +1,110 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<include
layout="@layout/bottom_sheet_handle" />
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?cardBackground"
android:fadeScrollbars="false"
android:fillViewport="true"
tools:ignore="DuplicateIds">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipChildren="false"
android:clipToPadding="false"
android:orientation="vertical"
android:padding="@dimen/margin_base"
tools:ignore="ScrollViewSize">
<LinearLayout
android:id="@+id/login_bottom_sheet"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<com.google.android.material.textfield.TextInputLayout
style="@style/MwmWidget.Editor.CustomTextInput"
android:layout_marginTop="@dimen/margin_half"
android:textColorHint="?android:textColorSecondary"
app:endIconMode="custom">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/osm_username"
style="@style/MwmWidget.Editor.FieldLayout.EditText"
android:padding="@dimen/margin_half_double_plus"
android:autofillHints="emailAddress"
android:hint="@string/email_or_username" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
style="@style/MwmWidget.Editor.CustomTextInput"
android:textColorHint="?android:textColorSecondary"
app:endIconMode="password_toggle"
app:endIconTint="?android:textColorSecondary">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/osm_password"
style="@style/MwmWidget.Editor.FieldLayout.EditText"
android:padding="@dimen/margin_half_double_plus"
android:autofillHints="password"
android:hint="@string/password"
android:inputType="textPassword" />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_base">
<Button
android:id="@+id/login"
style="@style/MwmWidget.Button.Accent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Login with username"
android:layout_marginTop="@dimen/margin_half"
android:textAppearance="@style/MwmTextAppearance.Body2.Light" />
<ProgressBar
android:id="@+id/osm_login_progress"
android:layout_width="@dimen/editor_auth_btn_height"
android:layout_height="@dimen/editor_auth_btn_height"
android:layout_gravity="center"
android:elevation="@dimen/design_fab_elevation"
android:visibility="gone" />
</FrameLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginTop="@dimen/margin_base">
<Button
android:id="@+id/lost_password"
style="@style/MwmWidget.Button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start"
android:layout_weight="1"
android:background="?clickableBackground"
android:padding="@dimen/margin_quarter"
android:text="@string/forgot_password"
android:textAppearance="@style/MwmTextAppearance.Body3" />
<Button
android:id="@+id/register"
style="@style/MwmWidget.Button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_gravity="end"
android:background="?clickableBackground"
android:padding="@dimen/margin_quarter"
android:text="@string/register_at_openstreetmap"
android:textAppearance="@style/MwmTextAppearance.Body3" />
</LinearLayout>
</LinearLayout>
</ScrollView>
</LinearLayout>

View file

@ -241,14 +241,7 @@ string OsmOAuth::SendAuthRequest(string const & requestTokenKey, SessionID const
string OsmOAuth::FetchRequestToken(SessionID const & sid) const
{
string const requestTokenUrl = m_baseUrl + "/oauth2/authorize";
string const requestTokenQuery = BuildPostRequest({
{"client_id", m_oauth2params.m_clientId},
{"redirect_uri", m_oauth2params.m_redirectUri},
{"scope", m_oauth2params.m_scope},
{"response_type", "code"}
});
HttpClient request(requestTokenUrl + "?" + requestTokenQuery);
HttpClient request(BuildOAuth2Url());
request.SetCookies(sid.m_cookies)
.SetFollowRedirects(false);
@ -280,6 +273,18 @@ string OsmOAuth::FetchRequestToken(SessionID const & sid) const
}
}
string OsmOAuth::BuildOAuth2Url() const
{
string requestTokenUrl = m_baseUrl + "/oauth2/authorize";
string const requestTokenQuery = BuildPostRequest({
{"client_id", m_oauth2params.m_clientId},
{"redirect_uri", m_oauth2params.m_redirectUri},
{"scope", m_oauth2params.m_scope},
{"response_type", "code"}
});
return requestTokenUrl.append("?").append(requestTokenQuery);
}
string OsmOAuth::FinishAuthorization(string const & oauth2code) const
{
auto params = BuildPostRequest({

View file

@ -93,6 +93,13 @@ public:
/// @param[api] If false, request is made to m_baseUrl.
Response DirectRequest(std::string const & method, bool api = true) const;
// Getters
std::string GetBaseUrl() const { return m_baseUrl; }
std::string GetClientId() const { return m_oauth2params.m_clientId; }
std::string GetClientSecret() const { return m_oauth2params.m_clientSecret; }
std::string GetScope() const { return m_oauth2params.m_scope; }
std::string GetRedirectUri() const { return m_oauth2params.m_redirectUri; }
/// @name Methods for WebView-based authentication.
//@{
std::string FinishAuthorization(std::string const & oauth2code) const;
@ -102,6 +109,7 @@ public:
{
return m_baseUrl + "/user/" + user + "/history";
}
std::string BuildOAuth2Url() const;
//@}
private:
@ -136,7 +144,6 @@ private:
/// @returns valid key and secret or throws otherwise.
std::string FetchRequestToken(SessionID const & sid) const;
std::string FetchAccessToken(SessionID const & sid) const;
//AuthResult FetchAccessToken(SessionID const & sid);
};
std::string DebugPrint(OsmOAuth::Response const & code);

View file

@ -8,6 +8,7 @@ typedef NS_ENUM(NSUInteger, DeeplinkUrlType) {
DeeplinkUrlTypeRoute,
DeeplinkUrlTypeSearch,
DeeplinkUrlTypeCrosshair,
DeeplinkUrlTypeOAuth2
};
@interface DeepLinkParser : NSObject

View file

@ -13,6 +13,7 @@ static inline DeeplinkUrlType deeplinkUrlType(url_scheme::ParsedMapApi::UrlType
case url_scheme::ParsedMapApi::UrlType::Route: return DeeplinkUrlTypeRoute;
case url_scheme::ParsedMapApi::UrlType::Search: return DeeplinkUrlTypeSearch;
case url_scheme::ParsedMapApi::UrlType::Crosshair: return DeeplinkUrlTypeCrosshair;
case url_scheme::ParsedMapApi::UrlType::OAuth2: return DeeplinkUrlTypeOAuth2;
}
}

View file

@ -115,6 +115,10 @@
case .crosshair:
// Not supported on iOS.
return false;
case .oAuth2:
// TODO: support OAuth2
return false;
case .incorrect:
// Invalid URL or API parameters.

View file

@ -1738,6 +1738,11 @@ url_scheme::SearchRequest Framework::GetParsedSearchRequest() const
return m_parsedMapApi.GetSearchRequest();
}
std::string Framework::GetParsedOAuth2Code() const
{
return m_parsedMapApi.GetOAuth2Code();
}
std::string const & Framework::GetParsedAppName() const
{
return m_parsedMapApi.GetAppName();

View file

@ -578,6 +578,7 @@ public:
ParsedRoutingData GetParsedRoutingData() const;
url_scheme::SearchRequest GetParsedSearchRequest() const;
std::string GetParsedOAuth2Code() const;
std::string const & GetParsedAppName() const;
std::string const & GetParsedBackUrl() const;
ms::LatLon GetParsedCenterLatLon() const;

View file

@ -474,6 +474,23 @@ UNIT_TEST(AppNameTest)
}
}
UNIT_TEST(OAuth2Test)
{
{
ParsedMapApi api("om://oauth2/osm/callback?code=THE_MEGA_CODE");
TEST_EQUAL(api.GetRequestType(), UrlType::OAuth2, ());
TEST_EQUAL(api.GetOAuth2Code(), "THE_MEGA_CODE", ());
}
{
ParsedMapApi api("om://oauth2/google/callback?code=THE_MEGA_CODE");
TEST_EQUAL(api.GetRequestType(), UrlType::Incorrect, ());
}
{
ParsedMapApi api("om://oauth2/osm/callback?code=");
TEST_EQUAL(api.GetRequestType(), UrlType::Incorrect, ());
}
}
namespace
{
string generatePartOfUrl(url_scheme::MapPoint const & point)

View file

@ -175,6 +175,22 @@ ParsedMapApi::UrlType ParsedMapApi::SetUrlAndParse(std::string const & raw)
return m_requestType = UrlType::Crosshair;
}
else if (type == "oauth2")
{
if (url.GetPath() != "osm/callback")
return m_requestType = UrlType::Incorrect;
url.ForEachParam([this](auto const & key, auto const & value)
{
if (key == "code")
m_oauth2code = value;
});
if (m_oauth2code.empty())
return m_requestType = UrlType::Incorrect;
else
return m_requestType = UrlType::OAuth2;
}
else if (checkForGe0Link)
{
// The URL is prefixed by one of the kGe0Prefixes AND doesn't match any supported API call:
@ -386,6 +402,7 @@ void ParsedMapApi::Reset()
m_mapPoints.clear();
m_routePoints.clear();
m_searchRequest = {};
m_oauth2code = {};
m_globalBackUrl ={};
m_appName = {};
m_centerLatLon = ms::LatLon::Invalid();
@ -467,6 +484,7 @@ std::string DebugPrint(ParsedMapApi::UrlType type)
case ParsedMapApi::UrlType::Route: return "Route";
case ParsedMapApi::UrlType::Search: return "Search";
case ParsedMapApi::UrlType::Crosshair: return "Crosshair";
case ParsedMapApi::UrlType::OAuth2: return "OAuth2";
}
UNREACHABLE();
}

View file

@ -45,6 +45,7 @@ public:
Route = 2,
Search = 3,
Crosshair = 4,
OAuth2 = 5,
};
ParsedMapApi() = default;
@ -93,6 +94,12 @@ public:
return m_searchRequest;
}
std::string const & GetOAuth2Code() const
{
ASSERT_EQUAL(m_requestType, UrlType::OAuth2, ("Expected OAuth2 API"));
return m_oauth2code;
}
private:
void ParseMapParam(std::string const & key, std::string const & value,
bool & correctOrder);
@ -107,6 +114,7 @@ private:
SearchRequest m_searchRequest;
std::string m_globalBackUrl;
std::string m_appName;
std::string m_oauth2code;
ms::LatLon m_centerLatLon = ms::LatLon::Invalid();
std::string m_routingType;
int m_version = 0;