diff --git a/android/app/src/main/cpp/app/organicmaps/Framework.cpp b/android/app/src/main/cpp/app/organicmaps/Framework.cpp index fe3b5435cf..0be0e09d42 100644 --- a/android/app/src/main/cpp/app/organicmaps/Framework.cpp +++ b/android/app/src/main/cpp/app/organicmaps/Framework.cpp @@ -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) { diff --git a/android/app/src/main/cpp/app/organicmaps/editor/OsmOAuth.cpp b/android/app/src/main/cpp/app/organicmaps/editor/OsmOAuth.cpp index 90d320ef65..fc18cf19f8 100644 --- a/android/app/src/main/cpp/app/organicmaps/editor/OsmOAuth.cpp +++ b/android/app/src/main/cpp/app/organicmaps/editor/OsmOAuth.cpp @@ -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) { diff --git a/android/app/src/main/java/app/organicmaps/Framework.java b/android/app/src/main/java/app/organicmaps/Framework.java index 2a076225c3..a5d65bbb96 100644 --- a/android/app/src/main/java/app/organicmaps/Framework.java +++ b/android/app/src/main/java/app/organicmaps/Framework.java @@ -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(); diff --git a/android/app/src/main/java/app/organicmaps/api/RequestType.java b/android/app/src/main/java/app/organicmaps/api/RequestType.java index e90743a5bb..0634d305cf 100644 --- a/android/app/src/main/java/app/organicmaps/api/RequestType.java +++ b/android/app/src/main/java/app/organicmaps/api/RequestType.java @@ -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; } diff --git a/android/app/src/main/java/app/organicmaps/editor/OsmLoginActivity.java b/android/app/src/main/java/app/organicmaps/editor/OsmLoginActivity.java index 35894517bf..0e15133d16 100644 --- a/android/app/src/main/java/app/organicmaps/editor/OsmLoginActivity.java +++ b/android/app/src/main/java/app/organicmaps/editor/OsmLoginActivity.java @@ -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 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); + } } diff --git a/android/app/src/main/java/app/organicmaps/editor/OsmLoginBottomFragment.java b/android/app/src/main/java/app/organicmaps/editor/OsmLoginBottomFragment.java new file mode 100644 index 0000000000..f91e9bbfb3 --- /dev/null +++ b/android/app/src/main/java/app/organicmaps/editor/OsmLoginBottomFragment.java @@ -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); + } +} diff --git a/android/app/src/main/java/app/organicmaps/editor/OsmLoginFragment.java b/android/app/src/main/java/app/organicmaps/editor/OsmLoginFragment.java index 4e33db9e22..a82cba792a 100644 --- a/android/app/src/main/java/app/organicmaps/editor/OsmLoginFragment.java +++ b/android/app/src/main/java/app/organicmaps/editor/OsmLoginFragment.java @@ -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(); } diff --git a/android/app/src/main/java/app/organicmaps/editor/OsmOAuth.java b/android/app/src/main/java/app/organicmaps/editor/OsmOAuth.java index d5d7dc8450..a8afa2f3b6 100644 --- a/android/app/src/main/java/app/organicmaps/editor/OsmOAuth.java +++ b/android/app/src/main/java/app/organicmaps/editor/OsmOAuth.java @@ -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); diff --git a/android/app/src/main/java/app/organicmaps/editor/ProfileActivity.java b/android/app/src/main/java/app/organicmaps/editor/ProfileActivity.java index eb5a6076c3..625b54ef3a 100644 --- a/android/app/src/main/java/app/organicmaps/editor/ProfileActivity.java +++ b/android/app/src/main/java/app/organicmaps/editor/ProfileActivity.java @@ -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 getFragmentClass() { diff --git a/android/app/src/main/java/app/organicmaps/editor/ProfileFragment.java b/android/app/src/main/java/app/organicmaps/editor/ProfileFragment.java index 95ff55c045..4f48fc11b0 100644 --- a/android/app/src/main/java/app/organicmaps/editor/ProfileFragment.java +++ b/android/app/src/main/java/app/organicmaps/editor/ProfileFragment.java @@ -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(); } diff --git a/android/app/src/main/java/app/organicmaps/intent/Factory.java b/android/app/src/main/java/app/organicmaps/intent/Factory.java index 2ddf4a28c6..c58a84e09e 100644 --- a/android/app/src/main/java/app/organicmaps/intent/Factory.java +++ b/android/app/src/main/java/app/organicmaps/intent/Factory.java @@ -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; } } diff --git a/android/app/src/main/res/layout-land/fragment_osm_login.xml b/android/app/src/main/res/layout-land/fragment_osm_login.xml index e5b177f708..a811956ee2 100644 --- a/android/app/src/main/res/layout-land/fragment_osm_login.xml +++ b/android/app/src/main/res/layout-land/fragment_osm_login.xml @@ -63,119 +63,32 @@ - - - - - - - - - -