001package com.gigya.android.sdk.ui.plugin;
002
003import android.annotation.SuppressLint;
004import android.content.Context;
005import android.net.Uri;
006import android.os.Build;
007import android.support.annotation.NonNull;
008import android.support.annotation.Nullable;
009import android.util.Base64;
010import android.view.View;
011import android.webkit.CookieManager;
012import android.webkit.CookieSyncManager;
013import android.webkit.JavascriptInterface;
014import android.webkit.ValueCallback;
015import android.webkit.WebView;
016
017import com.gigya.android.sdk.Config;
018import com.gigya.android.sdk.GigyaCallback;
019import com.gigya.android.sdk.GigyaLogger;
020import com.gigya.android.sdk.GigyaLoginCallback;
021import com.gigya.android.sdk.GigyaPluginCallback;
022import com.gigya.android.sdk.account.IAccountService;
023import com.gigya.android.sdk.account.models.GigyaAccount;
024import com.gigya.android.sdk.api.GigyaApiResponse;
025import com.gigya.android.sdk.api.IBusinessApiService;
026import com.gigya.android.sdk.network.GigyaError;
027import com.gigya.android.sdk.network.adapter.RestAdapter;
028import com.gigya.android.sdk.providers.IProviderFactory;
029import com.gigya.android.sdk.session.ISessionService;
030import com.gigya.android.sdk.session.ISessionVerificationService;
031import com.gigya.android.sdk.session.SessionInfo;
032import com.gigya.android.sdk.utils.ObjectUtils;
033import com.gigya.android.sdk.utils.UrlUtils;
034import com.google.gson.Gson;
035
036import org.json.JSONArray;
037
038import java.util.HashMap;
039import java.util.Locale;
040import java.util.Map;
041
042import static com.gigya.android.sdk.ui.plugin.PluginAuthEventDef.ADD_CONNECTION;
043import static com.gigya.android.sdk.ui.plugin.PluginAuthEventDef.CANCELED;
044import static com.gigya.android.sdk.ui.plugin.PluginAuthEventDef.LOGIN;
045import static com.gigya.android.sdk.ui.plugin.PluginAuthEventDef.LOGIN_STARTED;
046import static com.gigya.android.sdk.ui.plugin.PluginAuthEventDef.LOGOUT;
047import static com.gigya.android.sdk.ui.plugin.PluginAuthEventDef.REMOVE_CONNECTION;
048import static com.gigya.android.sdk.ui.plugin.PluginEventDef.AFTER_SCREEN_LOAD;
049import static com.gigya.android.sdk.ui.plugin.PluginEventDef.AFTER_SUBMIT;
050import static com.gigya.android.sdk.ui.plugin.PluginEventDef.AFTER_VALIDATION;
051import static com.gigya.android.sdk.ui.plugin.PluginEventDef.BEFORE_SCREEN_LOAD;
052import static com.gigya.android.sdk.ui.plugin.PluginEventDef.BEFORE_SUBMIT;
053import static com.gigya.android.sdk.ui.plugin.PluginEventDef.BEFORE_VALIDATION;
054import static com.gigya.android.sdk.ui.plugin.PluginEventDef.ERROR;
055import static com.gigya.android.sdk.ui.plugin.PluginEventDef.FIELD_CHANGED;
056import static com.gigya.android.sdk.ui.plugin.PluginEventDef.HIDE;
057import static com.gigya.android.sdk.ui.plugin.PluginEventDef.LOAD;
058import static com.gigya.android.sdk.ui.plugin.PluginEventDef.SUBMIT;
059
060public class GigyaWebBridge<A extends GigyaAccount> implements IGigyaWebBridge<A> {
061
062    private static final String LOG_TAG = "GigyaWebBridge";
063
064    private static final String EVALUATE_JS_PATH = "gigya._.apiAdapters.mobile.mobileCallbacks";
065
066    public enum Feature {
067        IS_SESSION_VALID("IS_SESSION_VALID"), SEND_REQUEST("SEND_REQUEST"), SEND_OAUTH_REQUEST("SEND_OAUTH_REQUEST"),
068        GET_IDS("GET_IDS"), ON_PLUGIN_EVENT("ON_PLUGIN_EVENT"), ON_CUSTOM_EVENT("ON_CUSTOM_EVENT"),
069        REGISTER_FOR_NAMESPACE_EVENTS("REGISTER_FOR_NAMESPACE_EVENTS"), ON_JS_EXCEPTION("ON_JS_EXCEPTION");
070
071        private String value;
072
073        Feature(final String value) {
074            this.value = value;
075        }
076
077        public String getValue() {
078            return value;
079        }
080
081        @NonNull
082        @Override
083        public String toString() {
084            return this.getValue();
085        }
086    }
087
088    final private Context _context;
089    final private Config _config;
090    final private ISessionService _sessionService;
091    final private IBusinessApiService<A> _businessApiService;
092    final private IAccountService<A> _accountService;
093    final private ISessionVerificationService _sessionVerificationService;
094    final private IProviderFactory _providerFactory;
095
096    private GigyaPluginFragment.IBridgeCallbacks<A> _invocationCallback;
097    private boolean _obfuscation = false;
098
099    public GigyaWebBridge(Context context,
100                          Config config,
101                          ISessionService sessionService,
102                          IBusinessApiService<A> businessApiService,
103                          IAccountService<A> accountService,
104                          ISessionVerificationService sessionVerificationService,
105                          IProviderFactory providerFactory) {
106        _context = context;
107        _config = config;
108        _sessionService = sessionService;
109        _businessApiService = businessApiService;
110        _accountService = accountService;
111        _sessionVerificationService = sessionVerificationService;
112        _providerFactory = providerFactory;
113    }
114
115    @Override
116    public void withObfuscation(boolean obfuscation) {
117        _obfuscation = obfuscation;
118    }
119
120    @Override
121    public void setInvocationCallback(@NonNull GigyaPluginFragment.IBridgeCallbacks<A> invocationCallback) {
122        _invocationCallback = invocationCallback;
123    }
124
125    //region ACTIONS
126
127    @Override
128    public boolean invoke(String action, String method, String queryStringParams) {
129        if (action == null) {
130            return false;
131        }
132
133        // Parse data map.
134        final Map<String, Object> data = new HashMap<>();
135        UrlUtils.parseUrlParameters(data, queryStringParams);
136
137        // Get parameters map.
138        final Map<String, Object> params = new HashMap<>();
139        UrlUtils.parseUrlParameters(params, deobfuscate((String) data.get("params")));
140
141        // Get settings map.
142        final Map<String, Object> settings = new HashMap<>();
143        UrlUtils.parseUrlParameters(settings, (String) data.get("settings"));
144
145        final Feature feature = Feature.valueOf(action.toUpperCase());
146        final String callbackId = (String) data.get("callbackID");
147
148        switch (feature) {
149            case GET_IDS:
150                getIds(callbackId);
151                break;
152            case IS_SESSION_VALID:
153                isSessionValid(callbackId);
154                break;
155            case SEND_REQUEST:
156            case SEND_OAUTH_REQUEST:
157                mapApisToRequests(feature, callbackId, method, params);
158                break;
159            case ON_PLUGIN_EVENT:
160                onPluginEvent(params);
161                break;
162            default:
163                break;
164        }
165        return true;
166    }
167
168    @Override
169    public boolean invoke(String url) {
170        Uri uri = Uri.parse(url);
171        if (uri == null || !UrlUtils.isGigyaScheme(uri.getScheme())) {
172            return false;
173        }
174        if (uri.getPath() == null) {
175            return false;
176        }
177        return invoke(uri.getHost(), uri.getPath().replace("/", ""), uri.getEncodedQuery());
178    }
179
180    @Override
181    public void invokeWebViewCallback(String id, String baseInvocation) {
182        GigyaLogger.debug(LOG_TAG, "evaluateJS: " + baseInvocation);
183        String value = obfuscate(baseInvocation, true);
184        final String invocation = String.format("javascript:%s['%s'](%s);", EVALUATE_JS_PATH, id, value);
185        if (_invocationCallback != null) {
186            _invocationCallback.invokeCallback(invocation);
187        }
188    }
189
190    @Override
191    public void getIds(String id) {
192        String ids = "{\"ucid\":\"" + _config.getUcid() + "\", \"gmid\":\"" + _config.getGmid() + "\"}";
193        GigyaLogger.debug(LOG_TAG, "getIds: " + ids);
194        invokeWebViewCallback(id, ids);
195    }
196
197    @Override
198    public void isSessionValid(String id) {
199        final boolean isValid = _sessionService.isValid();
200        GigyaLogger.debug(LOG_TAG, "isSessionValid: " + isValid);
201        invokeWebViewCallback(id, String.valueOf(isValid));
202    }
203
204    /*
205    Mapping requests directed from web sdk mobile adapter.
206    This is required because certain requests require specific flows.
207     */
208    private void mapApisToRequests(Feature feature,
209                                   String callbackId,
210                                   String api,
211                                   Map<String, Object> params) {
212        GigyaLogger.debug(LOG_TAG, "mapApisToRequests with api: " + api + " and params:\n<<<<" + params.toString() + "\n>>>>");
213        switch (api) {
214            case "socialize.logout":
215            case "accounts.logout":
216                logout(callbackId);
217                break;
218            case "socialize.addConnection":
219            case "accounts.addConnection":
220                final String providerToAdd = (String) params.get("provider");
221                addConnection(callbackId, providerToAdd);
222                break;
223            case "socialize.removeConnection":
224                final String providerToRemove = (String) params.get("provider");
225                removeConnection(callbackId, providerToRemove);
226                break;
227            default:
228                if (feature.equals(Feature.SEND_REQUEST)) {
229                    sendRequest(callbackId, api, params);
230                } else if (feature.equals(Feature.SEND_OAUTH_REQUEST)) {
231                    sendOAuthRequest(callbackId, api, params);
232                }
233                break;
234        }
235    }
236
237    @Override
238    public void sendOAuthRequest(final String callbackId, String api, Map<String, Object> params) {
239        GigyaLogger.debug(LOG_TAG, "sendOAuthRequest with api: " + api + " and params:\n<<<<" + params.toString() + "\n>>>>");
240        final String providerName = ObjectUtils.firstNonNull((String) params.get("provider"), "");
241        if (providerName.isEmpty()) {
242            return;
243        }
244
245        // Invoking login started (custom event) in order to show the web view progress bar.
246        _invocationCallback.onPluginAuthEvent(PluginAuthEventDef.LOGIN_STARTED, null);
247
248        _businessApiService.login(
249                providerName,
250                params,
251                new GigyaLoginCallback<A>() {
252                    @Override
253                    public void onSuccess(A account) {
254                        GigyaLogger.debug(LOG_TAG, "sendOAuthRequest: onSuccess with:\n" + account.toString());
255                        String invocation = "{\"errorCode\":" + account.getErrorCode() + ",\"userInfo\":" + new Gson().toJson(account) + "}";
256                        invokeWebViewCallback(callbackId, invocation);
257                        _invocationCallback.onPluginAuthEvent(PluginAuthEventDef.LOGIN, account);
258                    }
259
260                    @Override
261                    public void onError(GigyaError error) {
262                        invokeWebViewCallback(callbackId, error.getData());
263                    }
264
265                    @Override
266                    public void onOperationCanceled() {
267                        invokeWebViewCallback(callbackId, GigyaError.cancelledOperation().getData());
268                        _invocationCallback.onPluginAuthEvent(PluginAuthEventDef.CANCELED, null);
269                    }
270                });
271    }
272
273    @Override
274    public void onPluginEvent(Map<String, Object> params) {
275        final String containerId = (String) params.get("sourceContainerID");
276        if (containerId != null) {
277            _invocationCallback.onPluginEvent(new GigyaPluginEvent(params), containerId);
278        }
279    }
280
281    //endregion
282
283    //region APIS
284
285    @Override
286    public void sendRequest(final String callbackId, final String api, Map<String, Object> params) {
287        _businessApiService.send(
288                api,
289                params,
290                RestAdapter.POST,
291                GigyaApiResponse.class,
292                new GigyaCallback<GigyaApiResponse>() {
293                    @Override
294                    public void onSuccess(GigyaApiResponse response) {
295                        if (response.getErrorCode() == 0) {
296                            // Check if generic send was a login/register request.
297                            if (response.containsNested("sessionInfo.sessionSecret")) {
298                                A parsed = response.parseTo(_accountService.getAccountSchema());
299                                final SessionInfo newSession = response.getField("sessionInfo", SessionInfo.class);
300                                _sessionService.setSession(newSession);
301                                _accountService.setAccount(response.asJson());
302                                _invocationCallback.onPluginAuthEvent(PluginAuthEventDef.LOGIN, parsed);
303                            }
304                            invokeWebViewCallback(callbackId, response.asJson());
305                        } else {
306                            onError(GigyaError.fromResponse(response));
307                        }
308                    }
309
310                    @Override
311                    public void onError(GigyaError error) {
312                        invokeWebViewCallback(callbackId, error.getData());
313                    }
314                });
315    }
316
317    /*
318    Send logout request via business API service.
319     */
320    private void logout(final String callbackId) {
321        GigyaLogger.debug(LOG_TAG, "Sending logout request");
322        _businessApiService.logout(
323                new GigyaCallback<GigyaApiResponse>() {
324                    @Override
325                    public void onSuccess(GigyaApiResponse response) {
326                        if (response.getErrorCode() == 0) {
327                            invokeWebViewCallback(callbackId, response.asJson());
328                            _invocationCallback.onPluginAuthEvent(PluginAuthEventDef.LOGOUT, null);
329
330                            // Cleaning up.
331                            _sessionService.clear(true);
332                            _providerFactory.logoutFromUsedSocialProviders();
333                            _sessionVerificationService.stop();
334                            clearCookies();
335
336                        } else {
337                            onError(GigyaError.fromResponse(response));
338                        }
339                    }
340
341                    @Override
342                    public void onError(GigyaError error) {
343                        invokeWebViewCallback(callbackId, error.getData());
344                    }
345                });
346    }
347
348    private void clearCookies() {
349        CookieManager cookieManager = CookieManager.getInstance();
350        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
351            cookieManager.flush();
352        } else {
353            CookieSyncManager.createInstance(_context);
354            cookieManager.removeAllCookie();
355        }
356    }
357
358    /*
359    Send removeConnection request via business API service.
360     */
361    private void removeConnection(final String callbackId, String provider) {
362        GigyaLogger.debug(LOG_TAG, "Sending removeConnection api request with provider: " + provider);
363        _businessApiService.removeConnection(
364                provider,
365                new GigyaCallback<GigyaApiResponse>() {
366                    @Override
367                    public void onSuccess(GigyaApiResponse response) {
368                        if (response.getErrorCode() == 0) {
369                            invokeWebViewCallback(callbackId, response.asJson());
370                            _invocationCallback.onPluginAuthEvent(PluginAuthEventDef.REMOVE_CONNECTION, null);
371                        } else {
372                            onError(GigyaError.fromResponse(response));
373                        }
374                    }
375
376                    @Override
377                    public void onError(GigyaError error) {
378                        invokeWebViewCallback(callbackId, error.getData());
379                    }
380                });
381    }
382
383    /*
384    Send addConnection request via business API service.
385    On successful response request userInfo in order to invoke the web sdk callback.
386     */
387    private void addConnection(final String callbackId, String provider) {
388        GigyaLogger.debug(LOG_TAG, "Sending addConnection api request with provider: " + provider);
389        _businessApiService.addConnection(
390                provider,
391                new GigyaLoginCallback<A>() {
392                    @Override
393                    public void onSuccess(A response) {
394                        getUserInfoAndInvoke(callbackId);
395                        _invocationCallback.onPluginAuthEvent(PluginAuthEventDef.ADD_CONNECTION, response);
396                    }
397
398                    @Override
399                    public void onError(GigyaError error) {
400                        invokeWebViewCallback(callbackId, error.getData());
401                    }
402                });
403    }
404
405    /*
406    Add connection requests requires us to invoke a "socialize.getUserInfo" json response in order
407    To correctly refresh the screenset.
408     */
409    private void getUserInfoAndInvoke(final String callbackId) {
410        _businessApiService.send(
411                "socialize.getUserInfo",
412                null,
413                RestAdapter.POST,
414                GigyaApiResponse.class,
415                new GigyaCallback<GigyaApiResponse>() {
416                    @Override
417                    public void onSuccess(GigyaApiResponse obj) {
418                        invokeWebViewCallback(callbackId, obj.asJson());
419                    }
420
421                    @Override
422                    public void onError(GigyaError error) {
423                        invokeWebViewCallback(callbackId, error.getData());
424                    }
425                });
426    }
427
428
429    //endregion
430
431    //region OBFUSCATION
432
433    @SuppressWarnings("CharsetObjectCanBeUsed")
434    private String obfuscate(String string, boolean quote) {
435        if (_obfuscation) {
436            // by default, using obfuscation strategy of base64
437            try {
438                byte[] data = string.getBytes("UTF-8");
439                String base64 = Base64.encodeToString(data, Base64.DEFAULT);
440                if (quote) {
441                    return "\"" + base64 + "\"";
442                } else {
443                    return base64;
444                }
445            } catch (Exception ex) {
446                ex.printStackTrace();
447            }
448        }
449        return string;
450    }
451
452    @SuppressWarnings("CharsetObjectCanBeUsed")
453    private String deobfuscate(String base64String) {
454        if (_obfuscation) {
455            try {
456                byte[] data = Base64.decode(base64String, Base64.DEFAULT);
457                return new String(data, "UTF-8");
458            } catch (Exception ex) {
459                ex.printStackTrace();
460            }
461        }
462        return base64String;
463    }
464
465    //endregion
466
467    //region ATTACH
468
469    /**
470     * Attach a WebView instance to WebBridge.
471     * Allows external use of the Gigya web bridge.
472     *
473     * @param webView        WebView instance.
474     * @param pluginCallback Plugin callback used for JS and event interactions.
475     * @param progressView   Optional progress view that will be triggered (VISIBLE/GONE) according to event life cycle.
476     */
477    @SuppressLint("AddJavascriptInterface")
478    @Override
479    public void attachTo(
480            @NonNull final WebView webView,
481            @NonNull final GigyaPluginCallback<A> pluginCallback,
482            @Nullable final View progressView) {
483
484        if (android.os.Build.VERSION.SDK_INT < 17) {
485            GigyaLogger.error(LOG_TAG, "WebBridge invocation is only available for Android >= 17");
486            return;
487        }
488        webView.addJavascriptInterface(new Object() {
489
490            private static final String ADAPTER_NAME = "mobile";
491
492            @JavascriptInterface
493            public String getAPIKey() {
494                return _config.getApiKey();
495            }
496
497            @JavascriptInterface
498            public String getAdapterName() {
499                return ADAPTER_NAME;
500            }
501
502            @JavascriptInterface
503            public String getObfuscationStrategy() {
504                return _obfuscation ? "base64" : "";
505            }
506
507            @JavascriptInterface
508            public String getFeatures() {
509                JSONArray features = new JSONArray();
510                for (GigyaWebBridge.Feature feature : GigyaWebBridge.Feature.values()) {
511                    features.put(feature.toString().toLowerCase(Locale.ROOT));
512                }
513                return features.toString();
514            }
515
516            @JavascriptInterface
517            public boolean sendToMobile(String action, String method, String queryStringParams) {
518                return invoke(action, method, queryStringParams);
519            }
520        }, "__gigAPIAdapterSettings");
521
522
523        _invocationCallback = new GigyaPluginFragment.IBridgeCallbacks<A>() {
524            @Override
525            public void invokeCallback(final String invocation) {
526                webView.post(new Runnable() {
527                    @Override
528                    public void run() {
529                        if (android.os.Build.VERSION.SDK_INT > 18) {
530                            webView.evaluateJavascript(invocation, new ValueCallback<String>() {
531                                @Override
532                                public void onReceiveValue(String value) {
533                                    GigyaLogger.debug("evaluateJavascript Callback", value);
534                                }
535                            });
536                        } else {
537                            webView.loadUrl(invocation);
538                        }
539                    }
540                });
541            }
542
543            @Override
544            public void onPluginEvent(final GigyaPluginEvent event, final String containerID) {
545                final @PluginEventDef.PluginEvent String eventName = event.getEvent();
546                if (eventName == null) {
547                    return;
548                }
549                webView.post(new Runnable() {
550                    @Override
551                    public void run() {
552                        switch (eventName) {
553                            case BEFORE_SCREEN_LOAD:
554                                if (progressView != null) {
555                                    progressView.setVisibility(View.VISIBLE);
556                                }
557                                pluginCallback.onBeforeScreenLoad(event);
558                                break;
559                            case LOAD:
560                                if (progressView != null) {
561                                    progressView.setVisibility(View.INVISIBLE);
562                                }
563                                break;
564                            case AFTER_SCREEN_LOAD:
565                                if (progressView != null) {
566                                    progressView.setVisibility(View.INVISIBLE);
567                                }
568                                pluginCallback.onAfterScreenLoad(event);
569                                break;
570                            case FIELD_CHANGED:
571                                pluginCallback.onFieldChanged(event);
572                                break;
573                            case BEFORE_VALIDATION:
574                                pluginCallback.onBeforeValidation(event);
575                                break;
576                            case AFTER_VALIDATION:
577                                pluginCallback.onAfterValidation(event);
578                                break;
579                            case BEFORE_SUBMIT:
580                                pluginCallback.onBeforeSubmit(event);
581                                break;
582                            case SUBMIT:
583                                pluginCallback.onSubmit(event);
584                                break;
585                            case AFTER_SUBMIT:
586                                pluginCallback.onAfterSubmit(event);
587                                break;
588                            case HIDE:
589                                final String reason = (String) event.getEventMap().get("reason");
590                                pluginCallback.onHide(event, reason);
591                                break;
592                            case ERROR:
593                                pluginCallback.onError(event);
594                                break;
595                            default:
596                                break;
597                        }
598                    }
599                });
600            }
601
602            @Override
603            public void onPluginAuthEvent(final String method, final @Nullable A accountObj) {
604                webView.post(new Runnable() {
605                    @Override
606                    public void run() {
607                        switch (method) {
608                            case LOGIN_STARTED:
609                                if (progressView != null) {
610                                    progressView.setVisibility(View.VISIBLE);
611                                }
612                                break;
613                            case LOGIN:
614                                if (progressView != null) {
615                                    progressView.setVisibility(View.INVISIBLE);
616                                }
617                                if (accountObj != null) {
618                                    pluginCallback.onLogin(accountObj);
619                                }
620                                break;
621                            case LOGOUT:
622                                pluginCallback.onLogout();
623                                break;
624                            case ADD_CONNECTION:
625                                pluginCallback.onConnectionAdded();
626                                break;
627                            case REMOVE_CONNECTION:
628                                pluginCallback.onConnectionRemoved();
629                                break;
630                            case CANCELED:
631                                if (progressView != null) {
632                                    progressView.setVisibility(View.INVISIBLE);
633                                }
634                                pluginCallback.onCanceled();
635                                break;
636                            default:
637                                break;
638                        }
639                    }
640                });
641
642            }
643        };
644    }
645
646    /**
647     * Detach a WebView instance from this web bridge instance.
648     * Use to avoid leaking the enclosing context.
649     *
650     * @param webView Current attached WebView instance.
651     */
652    @Override
653    public void detachFrom(@NonNull final WebView webView) {
654        webView.loadUrl("about:blank");
655        webView.setWebViewClient(null);
656        webView.setWebChromeClient(null);
657        if (_invocationCallback != null) {
658            _invocationCallback = null;
659        }
660    }
661
662    //endregion
663}