SIGNATURE_SET =
150 | Collections.singleton(SIGNATURE_HASH);
151 |
152 | /**
153 | * The version in which Custom Tabs were introduced in Samsung Internet.
154 | */
155 | public static final DelimitedVersion MINIMUM_VERSION_FOR_CUSTOM_TAB =
156 | DelimitedVersion.parse("4.0");
157 |
158 | /**
159 | * Creates a browser descriptor for the specified version of SBrowser, when
160 | * used as a standalone browser.
161 | */
162 | public static BrowserDescriptor standaloneBrowser(@NonNull String version) {
163 | return new BrowserDescriptor(PACKAGE_NAME, SIGNATURE_SET, version, false);
164 | }
165 |
166 | /**
167 | * Creates a browser descriptor for the specified version of SBrowser, when
168 | * used as a custom tab.
169 | */
170 | public static BrowserDescriptor customTab(@NonNull String version) {
171 | return new BrowserDescriptor(PACKAGE_NAME, SIGNATURE_SET, version, true);
172 | }
173 |
174 | private SBrowser() {
175 | // no need to construct this class
176 | }
177 | }
178 |
179 | private Browsers() {
180 | // no need to construct this class
181 | }
182 | }
--------------------------------------------------------------------------------
/sdk/src/main/java/ru/mail/auth/sdk/MailRuSdkServiceActivity.java:
--------------------------------------------------------------------------------
1 | package ru.mail.auth.sdk;
2 |
3 | import android.app.Activity;
4 | import android.content.ActivityNotFoundException;
5 | import android.content.Context;
6 | import android.content.Intent;
7 | import android.net.Uri;
8 | import android.os.Build;
9 | import android.os.Bundle;
10 | import androidx.annotation.NonNull;
11 | import androidx.annotation.Nullable;
12 | import android.text.TextUtils;
13 |
14 | import androidx.fragment.app.Fragment;
15 | import ru.mail.auth.sdk.browser.BrowserRequestInitiator;
16 | import ru.mail.auth.sdk.ui.OAuthWebviewDialog;
17 |
18 | /**
19 | * Middleman UI-less activity to handle interactions and set results
20 | * between SDK clients and OAuth flow providers (webview, mail app)
21 | *
22 | */
23 |
24 | public class MailRuSdkServiceActivity extends Activity implements OAuthWebviewDialog.WebViewAuthFlowListener {
25 | static final String ACTION_LOGIN = "ru.mail.auth.sdk.login";
26 | public static final String AUTH_RESULT_EXTRA = "ru.mail.auth.sdk.EXTRA_RESULT";
27 | public static final String AUTH_RESULT_EXTRA_CODE_VERIFIER = "ru.mail.auth.sdk.EXTRA_RESULT_CODE_VERIFIER";
28 | public static final String EXTRA_LOGIN = "ru.mail.auth.sdk.EXTRA_LOGIN";
29 | public static final String AUTH_STARTED = "auth_started";
30 | public static final String EXTRA_AUTH_TYPE = "ru.mail.auth.sdk.EXTRA_AUTH_TYPE";
31 |
32 | private BrowserRequestInitiator mBrowserRequestInitiator = new BrowserRequestInitiator();
33 | private OAuthRequest mBrowserOAuthRequest;
34 | private boolean mBrowserAuthStarted;
35 | private Analytics.Type mAuthType = Analytics.Type.WEB;
36 |
37 | @Override
38 | protected void onCreate(Bundle savedInstanceState) {
39 | super.onCreate(savedInstanceState);
40 |
41 | if (savedInstanceState != null) {
42 | mBrowserAuthStarted = savedInstanceState.getBoolean(AUTH_STARTED);
43 | mAuthType = (Analytics.Type) savedInstanceState.getSerializable(EXTRA_AUTH_TYPE);
44 | } else {
45 | if (TextUtils.equals(getIntent().getAction(), ACTION_LOGIN)) {
46 | if (Utils.hasMailApp(getApplicationContext())) {
47 | MailRuAuthSdk.getInstance().getAnalytics().onLoginStarted(mAuthType = Analytics.Type.APP);
48 | startActivityForResult(Utils.getMailAppLoginFlowIntent(getIntent().getStringExtra(EXTRA_LOGIN)),
49 | RequestCodeOffset.LOGIN.toRequestCode());
50 | } else {
51 | MailRuAuthSdk.getInstance().getAnalytics().onLoginStarted(mAuthType = Analytics.Type.WEB);
52 | if (shouldUseExternalBrowser()) {
53 | createBrowserRequest();
54 | } else {
55 | showOAuthDialog();
56 | }
57 | }
58 | }
59 | }
60 | }
61 |
62 | private boolean shouldUseExternalBrowser() {
63 | MailRuAuthSdk sdk = MailRuAuthSdk.getInstance();
64 | String redirectUrl = sdk.getOAuthParams().getRedirectUrl();
65 | return Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT // only from KitKat browser flow is stable.
66 | && !redirectUrl.startsWith("http"); // only custom scheme redirects i.e. "ru.mail://redirect" are supported.
67 | }
68 |
69 | private void showOAuthDialog() {
70 | new OAuthWebviewDialog(this).show();
71 | }
72 |
73 | private void createBrowserRequest() {
74 | mBrowserOAuthRequest = OAuthRequest.from(MailRuAuthSdk.getInstance().getOAuthParams());
75 | }
76 |
77 | static void login(Activity activity, RequestCodeOffset code, String login) {
78 | activity.startActivityForResult(getLoginIntent(activity, login), code.toRequestCode());
79 | }
80 |
81 | static void login(Activity activity, RequestCodeOffset code) {
82 | login(activity, code, null);
83 | }
84 |
85 | @Override
86 | protected void onResume() {
87 | super.onResume();
88 | if (mBrowserOAuthRequest != null) {
89 | if (!mBrowserAuthStarted) {
90 | mBrowserAuthStarted = true;
91 | try {
92 | mBrowserRequestInitiator.startOAuthFlow(mBrowserOAuthRequest, this);
93 | } catch (ActivityNotFoundException e) {
94 | /* no suitable browser found... fallback to Webview */
95 | mBrowserAuthStarted = false;
96 | mBrowserOAuthRequest = null;
97 | showOAuthDialog();
98 | }
99 | } else {
100 | Intent intent = getIntent();
101 | if (intent != null) {
102 | Uri uri = intent.getParcelableExtra(RedirectReceiverActivity.EXTRA_URI);
103 | OAuthResponse response = OAuthResponse.from(mBrowserOAuthRequest, uri);
104 | onAuthResult(response.getResultCode(), packResult(response.getResult(), mBrowserOAuthRequest.getCodeVerifier()));
105 | }
106 | }
107 | }
108 | }
109 |
110 |
111 | static void login(Fragment fragment, RequestCodeOffset code) {
112 | login(fragment, code, null);
113 | }
114 |
115 | static void login(Fragment fragment, RequestCodeOffset code, String email) {
116 | fragment.startActivityForResult(getLoginIntent(fragment.getContext(), email), code.toRequestCode());
117 | }
118 |
119 | @NonNull
120 | private static Intent getLoginIntent(Context context, @Nullable String email) {
121 | Intent intent = new Intent(context, MailRuSdkServiceActivity.class);
122 | intent.putExtra(EXTRA_LOGIN, email);
123 | intent.setAction(ACTION_LOGIN);
124 | return intent;
125 | }
126 |
127 | @Override
128 | protected void onNewIntent(Intent intent) {
129 | super.onNewIntent(intent);
130 | setIntent(intent);
131 | }
132 |
133 | @Nullable
134 | public static Intent packResult(@Nullable String code, @Nullable String codeVerifier) {
135 | Intent intent = null;
136 | if (code != null) {
137 | intent = new Intent();
138 | intent.putExtra(AUTH_RESULT_EXTRA, code);
139 | intent.putExtra(AUTH_RESULT_EXTRA_CODE_VERIFIER, codeVerifier);
140 | }
141 | return intent;
142 | }
143 |
144 | @Override
145 | protected void onSaveInstanceState(Bundle outState) {
146 | outState.putBoolean(AUTH_STARTED, mBrowserAuthStarted);
147 | outState.putSerializable(EXTRA_AUTH_TYPE, mAuthType);
148 | super.onSaveInstanceState(outState);
149 | }
150 |
151 | @Override
152 | protected void onActivityResult(int requestCode, int resultCode, Intent data) {
153 | if (data == null) {
154 | data = new Intent();
155 | }
156 | data.putExtra(EXTRA_AUTH_TYPE, mAuthType);
157 | setResult(resultCode, data);
158 | finish();
159 | }
160 |
161 | @Override
162 | public void onAuthResult(@MailRuAuthSdk.Status int statusCode, @Nullable Intent result) {
163 | onActivityResult(RequestCodeOffset.LOGIN.toRequestCode(), statusCode, result);
164 | }
165 | }
166 |
--------------------------------------------------------------------------------
/sdk/src/main/java/ru/mail/auth/sdk/ui/OAuthWebviewDialog.java:
--------------------------------------------------------------------------------
1 | package ru.mail.auth.sdk.ui;
2 |
3 | import android.app.Dialog;
4 | import android.content.Context;
5 | import android.content.DialogInterface;
6 | import android.content.Intent;
7 | import android.net.Uri;
8 | import android.net.http.SslError;
9 | import androidx.annotation.Nullable;
10 |
11 | import android.os.Build;
12 | import android.text.TextUtils;
13 | import android.util.Log;
14 | import android.view.View;
15 | import android.webkit.SslErrorHandler;
16 | import android.webkit.WebResourceResponse;
17 | import android.webkit.WebView;
18 | import android.webkit.WebViewClient;
19 | import android.webkit.WebViewDatabase;
20 | import android.widget.ProgressBar;
21 |
22 | import ru.mail.auth.sdk.MailRuSdkServiceActivity;
23 | import ru.mail.auth.sdk.OAuthRequest;
24 | import ru.mail.auth.sdk.MailRuAuthSdk;
25 | import ru.mail.auth.sdk.OAuthParams;
26 | import ru.mail.auth.sdk.OAuthResponse;
27 | import ru.mail.auth.sdk.pub.BuildConfig;
28 | import ru.mail.auth.sdk.pub.R;
29 |
30 |
31 | public class OAuthWebviewDialog {
32 | private Context mContext;
33 | private ProgressBar mProgressBar;
34 | private WebView mWebView;
35 | private Dialog mDialog;
36 | private OAuthParams mOAuthParams;
37 |
38 | private int mResult = MailRuAuthSdk.STATUS_CANCELLED;
39 |
40 | @Nullable
41 | private String mResultString;
42 |
43 | @Nullable
44 | private String mUserAgent;
45 |
46 | private OAuthRequest mOAuthRequest;
47 |
48 | public OAuthWebviewDialog(Context context) {
49 | this(context, MailRuAuthSdk.getInstance().getOAuthParams());
50 | }
51 |
52 | public OAuthWebviewDialog(Context context, OAuthParams params) {
53 | mContext = context;
54 | mOAuthParams = params;
55 | mOAuthRequest = OAuthRequest.from(mOAuthParams);
56 | }
57 |
58 | public OAuthParams getOAuthParams() {
59 | return mOAuthParams;
60 | }
61 |
62 | public void setPreferredLogin(String login) {
63 | mOAuthRequest.withLogin(login);
64 | }
65 |
66 | public void setUserAgent(String userAgent) {
67 | mUserAgent = userAgent;
68 | }
69 |
70 | public void show() {
71 | mDialog = new Dialog(mContext, R.style.OauthDialog);
72 | View inflate = View.inflate(mContext, R.layout.webview_dialog, null);
73 | mWebView = (WebView) inflate.findViewById(R.id.webview);
74 | mWebView.setWebViewClient(new OauthWebviewClient());
75 | mProgressBar = (ProgressBar) inflate.findViewById(R.id.progress);
76 | mDialog.setContentView(inflate);
77 | mDialog.setOnDismissListener(new DialogInterface.OnDismissListener() {
78 | @Override
79 | public void onDismiss(DialogInterface dialog) {
80 | sendResultBackToActivity();
81 | }
82 | });
83 | mDialog.setOnCancelListener(new DialogInterface.OnCancelListener() {
84 | @Override
85 | public void onCancel(DialogInterface dialog) {
86 | setResultAndState(MailRuAuthSdk.STATUS_CANCELLED, null);
87 | }
88 | });
89 | initWebView();
90 | mDialog.show();
91 | }
92 |
93 | private void initWebView() {
94 | WebViewDatabase.getInstance(mContext).clearUsernamePassword();
95 | WebViewDatabase.getInstance(mContext).clearHttpAuthUsernamePassword();
96 | WebViewDatabase.getInstance(mContext).clearFormData();
97 |
98 | if (!TextUtils.isEmpty(mUserAgent)) {
99 | mWebView.getSettings().setUserAgentString(mUserAgent);
100 | }
101 |
102 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
103 | WebView.setWebContentsDebuggingEnabled(MailRuAuthSdk.getInstance().isDebugEnabled());
104 | }
105 |
106 | mWebView.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
107 | mWebView.getSettings().setJavaScriptEnabled(true);
108 | mWebView.setOverScrollMode(View.OVER_SCROLL_NEVER);
109 | String url = mOAuthRequest.toUri().toString();
110 | if (MailRuAuthSdk.getInstance().isDebugEnabled()) {
111 | Log.d(MailRuAuthSdk.AUTHSDK_TAG, "OAuth url: " + url);
112 | }
113 | mWebView.loadUrl(url);
114 | }
115 |
116 | private void sendResultBackToActivity() {
117 | if (mWebView.getContext() instanceof WebViewAuthFlowListener) {
118 | ((WebViewAuthFlowListener) mWebView.getContext()).onAuthResult(mResult,
119 | MailRuSdkServiceActivity.packResult(mResultString, mOAuthRequest.getCodeVerifier()));
120 | }
121 | }
122 |
123 | private class OauthWebviewClient extends WebViewClient {
124 |
125 | @Override
126 | public void onPageFinished(WebView view, String url) {
127 | super.onPageFinished(view, url);
128 | mProgressBar.setVisibility(View.INVISIBLE);
129 | view.setVisibility(View.VISIBLE);
130 | }
131 |
132 | @Override
133 | public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {
134 | setResultAndState(MailRuAuthSdk.STATUS_ERROR, description);
135 | safeDismiss();
136 | }
137 |
138 | @Override
139 | public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
140 | if (MailRuAuthSdk.getInstance().isDebugEnabled()) {
141 | handler.proceed();
142 | } else {
143 | super.onReceivedSslError(view, handler, error);
144 | }
145 | }
146 |
147 | @Override
148 | public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
149 | return super.shouldInterceptRequest(view, url);
150 | }
151 |
152 | @Override
153 | public boolean shouldOverrideUrlLoading(WebView view, String url) {
154 | Uri uri = Uri.parse(url);
155 | if (needHandleRedirectUrl(uri)) {
156 | if (MailRuAuthSdk.getInstance().isDebugEnabled()) {
157 | Log.d(MailRuAuthSdk.AUTHSDK_TAG, "Handle redirect " + uri);
158 | }
159 | processUrl(uri);
160 | return true;
161 | }
162 | return false;
163 | }
164 |
165 | private void processUrl(Uri uri) {
166 | OAuthResponse response = OAuthResponse.from(mOAuthRequest, uri);
167 | setResultAndState(response.getResultCode(), response.getResult());
168 | safeDismiss();
169 | }
170 |
171 | private boolean needHandleRedirectUrl(Uri url) {
172 | Uri redirectURI = Uri.parse(mOAuthParams.getRedirectUrl());
173 | boolean schemeMatch = TextUtils.equals(redirectURI.getScheme(), url.getScheme());
174 | boolean authorityMatch = TextUtils.equals(redirectURI.getAuthority(), url.getAuthority());
175 | boolean pathMatch = redirectURI.getPathSegments().containsAll(url.getPathSegments());
176 | return schemeMatch && authorityMatch && pathMatch;
177 | }
178 | }
179 |
180 | private void safeDismiss() {
181 | if (mDialog != null) {
182 | try {
183 | mDialog.dismiss();
184 | } catch (Exception e) {
185 | /* safe dismiss */
186 | }
187 | }
188 | }
189 |
190 | private void setResultAndState(int resultCode, String result) {
191 | mResultString = result;
192 | mResult = resultCode;
193 | }
194 |
195 | public interface WebViewAuthFlowListener {
196 | void onAuthResult(int statusCode, @Nullable Intent data);
197 | }
198 | }
199 |
--------------------------------------------------------------------------------
/sdk/src/main/java/ru/mail/auth/sdk/MailRuAuthSdk.java:
--------------------------------------------------------------------------------
1 | package ru.mail.auth.sdk;
2 |
3 | import android.app.Activity;
4 | import android.content.Context;
5 | import android.content.Intent;
6 | import android.os.Looper;
7 |
8 | import androidx.annotation.IntDef;
9 | import androidx.annotation.NonNull;
10 | import androidx.annotation.Nullable;
11 | import androidx.annotation.UiThread;
12 | import androidx.fragment.app.Fragment;
13 | import ru.mail.auth.sdk.api.ApiManager;
14 | import ru.mail.auth.sdk.api.CommonErrorCodes;
15 | import ru.mail.auth.sdk.api.OAuthRequestErrorCodes;
16 | import ru.mail.auth.sdk.api.token.InMemoryTokensStorage;
17 | import ru.mail.auth.sdk.api.token.OAuthTokensResult;
18 | import ru.mail.auth.sdk.api.user.UserInfoResult;
19 |
20 | import java.lang.annotation.Retention;
21 | import java.lang.annotation.RetentionPolicy;
22 | import java.util.Map;
23 |
24 | public class MailRuAuthSdk {
25 | public static final int STATUS_OK = Activity.RESULT_OK;
26 | public static final int STATUS_ERROR = 1; // some network error
27 | public static final int STATUS_CANCELLED = Activity.RESULT_CANCELED; // user press go back
28 | public static final int STATUS_ACCESS_DENIED = 2; // user revoke access
29 | public static final String AUTHSDK_TAG = "MailRuAuthSDK";
30 |
31 | private static volatile MailRuAuthSdk sInstance;
32 | private OAuthParams mOAuthParams;
33 | private int mRequestCodeOffset = 4000;
34 | private final Context mContext;
35 | private volatile boolean mDebugEnabled;
36 | private Analytics mAnalytics = new StubAnalytics();
37 |
38 | private MailRuAuthSdk(Context context) {
39 | mContext = context;
40 | }
41 |
42 | @UiThread
43 | public static void initialize(Context context) {
44 | if (Looper.myLooper() != Looper.getMainLooper()) {
45 | throw new IllegalStateException("This method should be called from main thread");
46 | }
47 | if (sInstance == null) {
48 | sInstance = new MailRuAuthSdk(context.getApplicationContext());
49 | }
50 | }
51 |
52 | public void setRequestCodeOffset(int offset) {
53 | mRequestCodeOffset = offset;
54 | }
55 |
56 | int getRequestCodeOffset() {
57 | return mRequestCodeOffset;
58 | }
59 |
60 | void setAnalytics(Analytics analytics) {
61 | mAnalytics = analytics;
62 | }
63 |
64 | public Analytics getAnalytics() {
65 | return mAnalytics;
66 | }
67 |
68 | public int getLoginRequestCode() {
69 | return RequestCodeOffset.LOGIN.toRequestCode();
70 | }
71 |
72 | public static MailRuAuthSdk getInstance() {
73 | if (sInstance == null) {
74 | throw new IllegalStateException("You must call initialize() first");
75 | }
76 | return sInstance;
77 | }
78 |
79 | public synchronized OAuthParams getOAuthParams() {
80 | if (mOAuthParams == null) {
81 | mOAuthParams = new OAuthParams(mContext);
82 | }
83 | return mOAuthParams;
84 | }
85 |
86 | public void setDebugEnabled(boolean enabled) {
87 | mDebugEnabled = enabled;
88 | }
89 |
90 | public boolean isDebugEnabled() {
91 | return mDebugEnabled;
92 | }
93 |
94 | public synchronized void setOAuthParams(OAuthParams params) {
95 | mOAuthParams = params;
96 | }
97 |
98 | public Context getContext() {
99 | return mContext;
100 | }
101 |
102 | public void startLogin(Activity activity) {
103 | mAnalytics.onLoginStarted(null);
104 | MailRuSdkServiceActivity.login(activity, RequestCodeOffset.LOGIN);
105 | }
106 |
107 | public void startLogin(Activity activity, Map additionalOAuthParams) {
108 | getOAuthParams().setAdditionalParams(additionalOAuthParams);
109 | mAnalytics.onLoginStarted(null);
110 | MailRuSdkServiceActivity.login(activity, RequestCodeOffset.LOGIN);
111 | }
112 |
113 | public void startLogin(Fragment fragment) {
114 | mAnalytics.onLoginStarted(null);
115 | MailRuSdkServiceActivity.login(fragment, RequestCodeOffset.LOGIN);
116 | }
117 |
118 | public void startLogin(Fragment fragment, Map additionalOAuthParams) {
119 | getOAuthParams().setAdditionalParams(additionalOAuthParams);
120 | mAnalytics.onLoginStarted(null);
121 | MailRuSdkServiceActivity.login(fragment, RequestCodeOffset.LOGIN);
122 | }
123 |
124 | public void requestOAuthTokens(AuthResult authResult,
125 | MailRuCallback callback) {
126 | ApiManager.getAccessToken(authResult.getAuthCode(), authResult.getCodeVerifier(), callback);
127 | }
128 |
129 | public void requestUserInfo(OAuthTokensResult result,
130 | MailRuCallback callback) {
131 | ApiManager.getUserInfo(new InMemoryTokensStorage(result.getAccessToken(), result.getRefreshToken()), callback);
132 | }
133 |
134 | public boolean handleAuthResult(int requestCode,
135 | @Status int resultCode,
136 | Intent data,
137 | final MailRuCallback callback) {
138 | return handleActivityResult(requestCode, resultCode, data, new MailRuCallback() {
139 | @Override
140 | public void onResult(@NonNull AuthResult authResult) {
141 | requestOAuthTokens(authResult, new MailRuCallback() {
142 | @Override
143 | public void onResult(@NonNull OAuthTokensResult oAuthTokensResult) {
144 | callback.onResult(oAuthTokensResult);
145 | }
146 |
147 | @Override
148 | public void onError(@NonNull Integer integer) {
149 | callback.onError(integer);
150 | }
151 | });
152 | }
153 |
154 | @Override
155 | public void onError(@NonNull AuthError authError) {
156 | callback.onError(authError == AuthError.NETWORK_ERROR ?
157 | CommonErrorCodes.NETWORK_ERROR : CommonErrorCodes.REQUEST_CANCELLED);
158 | }
159 | });
160 | }
161 |
162 | public boolean handleActivityResult(int requestCode,
163 | @Status int resultCode,
164 | Intent data,
165 | MailRuCallback callback) {
166 | if (requestCode == getLoginRequestCode()) {
167 | String code = hasExtra(data, MailRuSdkServiceActivity.AUTH_RESULT_EXTRA) ?
168 | data.getStringExtra(MailRuSdkServiceActivity.AUTH_RESULT_EXTRA) : "";
169 |
170 | String codeVerifier = hasExtra(data, MailRuSdkServiceActivity.AUTH_RESULT_EXTRA_CODE_VERIFIER)
171 | ? data.getStringExtra(MailRuSdkServiceActivity.AUTH_RESULT_EXTRA_CODE_VERIFIER) : null;
172 |
173 | Analytics.Type from = hasExtra(data, MailRuSdkServiceActivity.EXTRA_AUTH_TYPE) ?
174 | (Analytics.Type) data.getSerializableExtra(MailRuSdkServiceActivity.EXTRA_AUTH_TYPE) : Analytics.Type.WEB;
175 |
176 | if (resultCode == MailRuAuthSdk.STATUS_OK) {
177 | mAnalytics.onLoginSuccess(from);
178 | callback.onResult(new AuthResult(code, codeVerifier));
179 | } else {
180 | AuthError error = AuthError.fromCode(resultCode);
181 | mAnalytics.onLoginFailed(from, error.name());
182 | callback.onError(error);
183 | }
184 | return true;
185 | }
186 | return false;
187 | }
188 |
189 | private boolean hasExtra(@Nullable Intent intent, String extraName) {
190 | return intent != null && intent.hasExtra(extraName);
191 | }
192 |
193 | @Retention(RetentionPolicy.SOURCE)
194 | @IntDef({STATUS_OK, STATUS_ERROR, STATUS_ACCESS_DENIED, STATUS_CANCELLED})
195 | public @interface Status {
196 | }
197 | }
198 |
--------------------------------------------------------------------------------
/sdk/src/main/java/ru/mail/auth/sdk/browser/BrowserSelector.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2015 The AppAuth for Android Authors. All Rights Reserved.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5 | * in compliance with the License. You may obtain a copy of the License at
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software distributed under the
10 | * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
11 | * express or implied. See the License for the specific language governing permissions and
12 | * limitations under the License.
13 | */
14 |
15 | package ru.mail.auth.sdk.browser;
16 |
17 | import android.annotation.SuppressLint;
18 | import android.content.Context;
19 | import android.content.Intent;
20 | import android.content.pm.PackageInfo;
21 | import android.content.pm.PackageManager;
22 | import android.content.pm.PackageManager.NameNotFoundException;
23 | import android.content.pm.ResolveInfo;
24 | import android.net.Uri;
25 | import android.os.Build.VERSION;
26 | import android.os.Build.VERSION_CODES;
27 | import androidx.annotation.NonNull;
28 | import androidx.annotation.Nullable;
29 | import androidx.annotation.VisibleForTesting;
30 |
31 | import java.util.ArrayList;
32 | import java.util.Iterator;
33 | import java.util.List;
34 |
35 | /**
36 | * Utility class to obtain the browser package name to be used for
37 | * OAuth calls. It prioritizes browsers which support
38 | * [custom tabs](https://developer.chrome.com/multidevice/android/customtabs). To mitigate
39 | * man-in-the-middle attacks by malicious apps pretending to be browsers for the specific URI we
40 | * query, only those which are registered as a handler for _all_ HTTP and HTTPS URIs will be
41 | * used.
42 | */
43 | public final class BrowserSelector {
44 |
45 | private static final String SCHEME_HTTP = "http";
46 | private static final String SCHEME_HTTPS = "https";
47 |
48 | /**
49 | * The service we expect to find on a web browser that indicates it supports custom tabs.
50 | */
51 | @VisibleForTesting
52 | // HACK: Using a StringBuilder prevents Jetifier from tempering with our constants.
53 | @SuppressWarnings("StringBufferReplaceableByString")
54 | private static final String ACTION_CUSTOM_TABS_CONNECTION = new StringBuilder("android")
55 | .append(".support.customtabs.action.CustomTabsService").toString();
56 |
57 | /**
58 | * An arbitrary (but unregistrable, per
59 | * IANA rules) web intent used to query
60 | * for installed web browsers on the system.
61 | */
62 | @VisibleForTesting
63 | static final Intent BROWSER_INTENT = new Intent(
64 | Intent.ACTION_VIEW,
65 | Uri.parse("http://www.example.com"));
66 |
67 | /**
68 | * Retrieves the full list of browsers installed on the device. Two entries will exist
69 | * for each browser that supports custom tabs, with the {@link BrowserDescriptor#useCustomTab}
70 | * flag set to `true` in one and `false` in the other. The list is in the
71 | * order returned by the package manager, so indirectly reflects the user's preferences
72 | * (i.e. their default browser, if set, should be the first entry in the list).
73 | */
74 | @SuppressLint("PackageManagerGetSignatures")
75 | @NonNull
76 | public static List getAllBrowsers(Context context) {
77 | PackageManager pm = context.getPackageManager();
78 | List browsers = new ArrayList<>();
79 | String defaultBrowserPackage = null;
80 |
81 | int queryFlag = PackageManager.GET_RESOLVED_FILTER;
82 | if (VERSION.SDK_INT >= VERSION_CODES.M) {
83 | queryFlag |= PackageManager.MATCH_ALL;
84 | }
85 | // When requesting all matching activities for an intent from the package manager,
86 | // the user's preferred browser is not guaranteed to be at the head of this list.
87 | // Therefore, the preferred browser must be separately determined and the resultant
88 | // list of browsers reordered to restored this desired property.
89 | ResolveInfo resolvedDefaultActivity =
90 | pm.resolveActivity(BROWSER_INTENT, 0);
91 | if (resolvedDefaultActivity != null) {
92 | defaultBrowserPackage = resolvedDefaultActivity.activityInfo.packageName;
93 | }
94 | List resolvedActivityList =
95 | pm.queryIntentActivities(BROWSER_INTENT, queryFlag);
96 |
97 | for (ResolveInfo info : resolvedActivityList) {
98 | // ignore handlers which are not browsers
99 | if (!isFullBrowser(info)) {
100 | continue;
101 | }
102 |
103 | try {
104 | int defaultBrowserIndex = 0;
105 | PackageInfo packageInfo = pm.getPackageInfo(
106 | info.activityInfo.packageName,
107 | PackageManager.GET_SIGNATURES);
108 |
109 | if (hasWarmupService(pm, info.activityInfo.packageName)) {
110 | BrowserDescriptor customTabBrowserDescriptor =
111 | new BrowserDescriptor(packageInfo, true);
112 | if (info.activityInfo.packageName.equals(defaultBrowserPackage)) {
113 | // If the default browser is having a WarmupService,
114 | // will it be added to the beginning of the list.
115 | browsers.add(defaultBrowserIndex, customTabBrowserDescriptor);
116 | defaultBrowserIndex++;
117 | } else {
118 | browsers.add(customTabBrowserDescriptor);
119 | }
120 | }
121 |
122 | BrowserDescriptor fullBrowserDescriptor =
123 | new BrowserDescriptor(packageInfo, false);
124 | if (info.activityInfo.packageName.equals(defaultBrowserPackage)) {
125 | // The default browser is added to the beginning of the list.
126 | // If there is support for Custom Tabs, will the one disabling Custom Tabs
127 | // be added as the second entry.
128 | browsers.add(defaultBrowserIndex, fullBrowserDescriptor);
129 | } else {
130 | browsers.add(fullBrowserDescriptor);
131 | }
132 | } catch (NameNotFoundException e) {
133 | // a descriptor cannot be generated without the package info
134 | }
135 | }
136 |
137 | return browsers;
138 | }
139 |
140 | /**
141 | * Searches through all browsers for the best match based on the supplied browser matcher.
142 | * Custom tab supporting browsers are preferred, if the matcher permits them, and browsers
143 | * are evaluated in the order returned by the package manager, which should indirectly match
144 | * the user's preferences.
145 | *
146 | * @param context {@link Context} to use for accessing {@link PackageManager}.
147 | * @return The package name recommended to use for connecting to custom tabs related components.
148 | */
149 | @SuppressLint("PackageManagerGetSignatures")
150 | @Nullable
151 | public static BrowserDescriptor select(Context context, BrowserMatcher browserMatcher) {
152 | List allBrowsers = getAllBrowsers(context);
153 | BrowserDescriptor bestMatch = null;
154 | for (BrowserDescriptor browser : allBrowsers) {
155 | if (!browserMatcher.matches(browser)) {
156 | continue;
157 | }
158 |
159 | if (browser.useCustomTab) {
160 | // directly return the first custom tab supporting browser that is matched
161 | return browser;
162 | }
163 |
164 | if (bestMatch == null) {
165 | // store this as the best match for use if we don't find any matching
166 | // custom tab supporting browsers
167 | bestMatch = browser;
168 | }
169 | }
170 | return bestMatch;
171 | }
172 |
173 | private static boolean hasWarmupService(PackageManager pm, String packageName) {
174 | Intent serviceIntent = new Intent();
175 | serviceIntent.setAction(ACTION_CUSTOM_TABS_CONNECTION);
176 | serviceIntent.setPackage(packageName);
177 | return (pm.resolveService(serviceIntent, 0) != null);
178 | }
179 |
180 | private static boolean isFullBrowser(ResolveInfo resolveInfo) {
181 | // The filter must match ACTION_VIEW, CATEGORY_BROWSEABLE, and at least one scheme,
182 | if (!resolveInfo.filter.hasAction(Intent.ACTION_VIEW)
183 | || !resolveInfo.filter.hasCategory(Intent.CATEGORY_BROWSABLE)
184 | || resolveInfo.filter.schemesIterator() == null) {
185 | return false;
186 | }
187 |
188 | // The filter must not be restricted to any particular set of authorities
189 | if (resolveInfo.filter.authoritiesIterator() != null) {
190 | return false;
191 | }
192 |
193 | // The filter must support both HTTP and HTTPS.
194 | boolean supportsHttp = false;
195 | boolean supportsHttps = false;
196 | Iterator schemeIter = resolveInfo.filter.schemesIterator();
197 | while (schemeIter.hasNext()) {
198 | String scheme = schemeIter.next();
199 | supportsHttp |= SCHEME_HTTP.equals(scheme);
200 | supportsHttps |= SCHEME_HTTPS.equals(scheme);
201 |
202 | if (supportsHttp && supportsHttps) {
203 | return true;
204 | }
205 | }
206 |
207 | // at least one of HTTP or HTTPS is not supported
208 | return false;
209 | }
210 | }
--------------------------------------------------------------------------------
/NOTICE.txt:
--------------------------------------------------------------------------------
1 | Mail.ru Group AuthSDK
2 |
3 | THIRD-PARTY SOFTWARE NOTICES AND INFORMATION
4 | Do Not Translate or Localize
5 |
6 | This project incorporates components from the projects listed below.
7 | The original copyright notices and the licenses under which Mail.ru Group received such components are set forth below.
8 |
9 | 1. AppAuth-Android version 0.7.1 (https://github.com/openid/AppAuth-Android)
10 |
11 | Apache License
12 | Version 2.0, January 2004
13 | http://www.apache.org/licenses/
14 |
15 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
16 |
17 | 1. Definitions.
18 |
19 | "License" shall mean the terms and conditions for use, reproduction,
20 | and distribution as defined by Sections 1 through 9 of this document.
21 |
22 | "Licensor" shall mean the copyright owner or entity authorized by
23 | the copyright owner that is granting the License.
24 |
25 | "Legal Entity" shall mean the union of the acting entity and all
26 | other entities that control, are controlled by, or are under common
27 | control with that entity. For the purposes of this definition,
28 | "control" means (i) the power, direct or indirect, to cause the
29 | direction or management of such entity, whether by contract or
30 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
31 | outstanding shares, or (iii) beneficial ownership of such entity.
32 |
33 | "You" (or "Your") shall mean an individual or Legal Entity
34 | exercising permissions granted by this License.
35 |
36 | "Source" form shall mean the preferred form for making modifications,
37 | including but not limited to software source code, documentation
38 | source, and configuration files.
39 |
40 | "Object" form shall mean any form resulting from mechanical
41 | transformation or translation of a Source form, including but
42 | not limited to compiled object code, generated documentation,
43 | and conversions to other media types.
44 |
45 | "Work" shall mean the work of authorship, whether in Source or
46 | Object form, made available under the License, as indicated by a
47 | copyright notice that is included in or attached to the work
48 | (an example is provided in the Appendix below).
49 |
50 | "Derivative Works" shall mean any work, whether in Source or Object
51 | form, that is based on (or derived from) the Work and for which the
52 | editorial revisions, annotations, elaborations, or other modifications
53 | represent, as a whole, an original work of authorship. For the purposes
54 | of this License, Derivative Works shall not include works that remain
55 | separable from, or merely link (or bind by name) to the interfaces of,
56 | the Work and Derivative Works thereof.
57 |
58 | "Contribution" shall mean any work of authorship, including
59 | the original version of the Work and any modifications or additions
60 | to that Work or Derivative Works thereof, that is intentionally
61 | submitted to Licensor for inclusion in the Work by the copyright owner
62 | or by an individual or Legal Entity authorized to submit on behalf of
63 | the copyright owner. For the purposes of this definition, "submitted"
64 | means any form of electronic, verbal, or written communication sent
65 | to the Licensor or its representatives, including but not limited to
66 | communication on electronic mailing lists, source code control systems,
67 | and issue tracking systems that are managed by, or on behalf of, the
68 | Licensor for the purpose of discussing and improving the Work, but
69 | excluding communication that is conspicuously marked or otherwise
70 | designated in writing by the copyright owner as "Not a Contribution."
71 |
72 | "Contributor" shall mean Licensor and any individual or Legal Entity
73 | on behalf of whom a Contribution has been received by Licensor and
74 | subsequently incorporated within the Work.
75 |
76 | 2. Grant of Copyright License. Subject to the terms and conditions of
77 | this License, each Contributor hereby grants to You a perpetual,
78 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
79 | copyright license to reproduce, prepare Derivative Works of,
80 | publicly display, publicly perform, sublicense, and distribute the
81 | Work and such Derivative Works in Source or Object form.
82 |
83 | 3. Grant of Patent License. Subject to the terms and conditions of
84 | this License, each Contributor hereby grants to You a perpetual,
85 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
86 | (except as stated in this section) patent license to make, have made,
87 | use, offer to sell, sell, import, and otherwise transfer the Work,
88 | where such license applies only to those patent claims licensable
89 | by such Contributor that are necessarily infringed by their
90 | Contribution(s) alone or by combination of their Contribution(s)
91 | with the Work to which such Contribution(s) was submitted. If You
92 | institute patent litigation against any entity (including a
93 | cross-claim or counterclaim in a lawsuit) alleging that the Work
94 | or a Contribution incorporated within the Work constitutes direct
95 | or contributory patent infringement, then any patent licenses
96 | granted to You under this License for that Work shall terminate
97 | as of the date such litigation is filed.
98 |
99 | 4. Redistribution. You may reproduce and distribute copies of the
100 | Work or Derivative Works thereof in any medium, with or without
101 | modifications, and in Source or Object form, provided that You
102 | meet the following conditions:
103 |
104 | (a) You must give any other recipients of the Work or
105 | Derivative Works a copy of this License; and
106 |
107 | (b) You must cause any modified files to carry prominent notices
108 | stating that You changed the files; and
109 |
110 | (c) You must retain, in the Source form of any Derivative Works
111 | that You distribute, all copyright, patent, trademark, and
112 | attribution notices from the Source form of the Work,
113 | excluding those notices that do not pertain to any part of
114 | the Derivative Works; and
115 |
116 | (d) If the Work includes a "NOTICE" text file as part of its
117 | distribution, then any Derivative Works that You distribute must
118 | include a readable copy of the attribution notices contained
119 | within such NOTICE file, excluding those notices that do not
120 | pertain to any part of the Derivative Works, in at least one
121 | of the following places: within a NOTICE text file distributed
122 | as part of the Derivative Works; within the Source form or
123 | documentation, if provided along with the Derivative Works; or,
124 | within a display generated by the Derivative Works, if and
125 | wherever such third-party notices normally appear. The contents
126 | of the NOTICE file are for informational purposes only and
127 | do not modify the License. You may add Your own attribution
128 | notices within Derivative Works that You distribute, alongside
129 | or as an addendum to the NOTICE text from the Work, provided
130 | that such additional attribution notices cannot be construed
131 | as modifying the License.
132 |
133 | You may add Your own copyright statement to Your modifications and
134 | may provide additional or different license terms and conditions
135 | for use, reproduction, or distribution of Your modifications, or
136 | for any such Derivative Works as a whole, provided Your use,
137 | reproduction, and distribution of the Work otherwise complies with
138 | the conditions stated in this License.
139 |
140 | 5. Submission of Contributions. Unless You explicitly state otherwise,
141 | any Contribution intentionally submitted for inclusion in the Work
142 | by You to the Licensor shall be under the terms and conditions of
143 | this License, without any additional terms or conditions.
144 | Notwithstanding the above, nothing herein shall supersede or modify
145 | the terms of any separate license agreement you may have executed
146 | with Licensor regarding such Contributions.
147 |
148 | 6. Trademarks. This License does not grant permission to use the trade
149 | names, trademarks, service marks, or product names of the Licensor,
150 | except as required for reasonable and customary use in describing the
151 | origin of the Work and reproducing the content of the NOTICE file.
152 |
153 | 7. Disclaimer of Warranty. Unless required by applicable law or
154 | agreed to in writing, Licensor provides the Work (and each
155 | Contributor provides its Contributions) on an "AS IS" BASIS,
156 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
157 | implied, including, without limitation, any warranties or conditions
158 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
159 | PARTICULAR PURPOSE. You are solely responsible for determining the
160 | appropriateness of using or redistributing the Work and assume any
161 | risks associated with Your exercise of permissions under this License.
162 |
163 | 8. Limitation of Liability. In no event and under no legal theory,
164 | whether in tort (including negligence), contract, or otherwise,
165 | unless required by applicable law (such as deliberate and grossly
166 | negligent acts) or agreed to in writing, shall any Contributor be
167 | liable to You for damages, including any direct, indirect, special,
168 | incidental, or consequential damages of any character arising as a
169 | result of this License or out of the use or inability to use the
170 | Work (including but not limited to damages for loss of goodwill,
171 | work stoppage, computer failure or malfunction, or any and all
172 | other commercial damages or losses), even if such Contributor
173 | has been advised of the possibility of such damages.
174 |
175 | 9. Accepting Warranty or Additional Liability. While redistributing
176 | the Work or Derivative Works thereof, You may choose to offer,
177 | and charge a fee for, acceptance of support, warranty, indemnity,
178 | or other liability obligations and/or rights consistent with this
179 | License. However, in accepting such obligations, You may act only
180 | on Your own behalf and on Your sole responsibility, not on behalf
181 | of any other Contributor, and only if You agree to indemnify,
182 | defend, and hold each Contributor harmless for any liability
183 | incurred by, or claims asserted against, such Contributor by reason
184 | of your accepting any such warranty or additional liability.
185 |
186 | END OF TERMS AND CONDITIONS
187 |
188 |
--------------------------------------------------------------------------------