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}