/* * Copyright (c) 2016 Uber Technologies, Inc. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package com.uber.sdk.android.rides; import android.content.Context; import android.content.Intent; import android.net.Uri; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.VisibleForTesting; import android.util.AttributeSet; import android.webkit.GeolocationPermissions; import android.webkit.WebChromeClient; import android.webkit.WebResourceError; import android.webkit.WebResourceRequest; import android.webkit.WebResourceResponse; import android.webkit.WebSettings; import android.webkit.WebView; import android.webkit.WebViewClient; import android.widget.LinearLayout; import com.uber.sdk.android.core.UberSdk; import com.uber.sdk.android.core.auth.AccessTokenManager; import com.uber.sdk.core.auth.AccessToken; import com.uber.sdk.core.auth.AccessTokenStorage; import com.uber.sdk.rides.client.AccessTokenSession; import com.uber.sdk.rides.client.SessionConfiguration; import java.util.HashMap; import java.util.Map; /** * The Uber Ride Request View: an embeddable view that provides the end-to-end Uber experience. * The primary way to interact with this view after construction is to call the load() function. */ public class RideRequestView extends LinearLayout { private static final String USER_AGENT_RIDE_VIEW = "rides-android-v0.6.1-ride_request_view"; @Nullable private AccessTokenSession accessTokenSession; @NonNull @VisibleForTesting RideParameters rideParameters = new RideParameters.Builder().build(); @Nullable private RideRequestViewCallback rideRequestViewCallback; private WebView webView; public RideRequestView(Context context) { this(context, null); } public RideRequestView(Context context, AttributeSet attrs) { super(context, attrs, 0); init(context); } /** * Stops all current loading and brings up a blank page. */ public void cancelLoad() { webView.stopLoading(); webView.loadUrl("about:blank"); } /** * Gets the {@link AccessToken} being used to authorize the {@link RideRequestView}. * * @return */ @Nullable public AccessTokenSession getSession() { return accessTokenSession; } /** * Loads the ride widget. * Requires that the {@link AccessToken} has been retrieved. */ public void load() { final SessionConfiguration config; final AccessTokenStorage storage; if (accessTokenSession == null && UberSdk.isInitialized()) { config = UberSdk.getDefaultSessionConfiguration(); storage = new AccessTokenManager(getContext()); accessTokenSession = new AccessTokenSession(config, storage); } else if (accessTokenSession != null) { config = accessTokenSession.getAuthenticator().getSessionConfiguration(); storage = accessTokenSession.getAuthenticator().getTokenStorage(); } else { config = null; storage = null; } if (config == null || storage == null || storage.getAccessToken() == null) { if (rideRequestViewCallback != null) { rideRequestViewCallback.onErrorReceived(RideRequestViewError.NO_ACCESS_TOKEN); } return; } webView.loadUrl(buildUrlFromRideParameters(getContext(), rideParameters, config), RideRequestView.getHeaders(storage.getAccessToken())); } /** * Set a custom {@link AccessTokenSession} to use for authenticating into the Ride Widget. * * @param accessTokenSession the {@link AccessTokenSession} to use for authorization */ public void setSession(@Nullable AccessTokenSession accessTokenSession) { this.accessTokenSession = accessTokenSession; } /** * Configure parameters for the Ride Request Control. * * @param rideParameters the {@link RideParameters} to use for presetting values */ public void setRideParameters(@NonNull RideParameters rideParameters) { this.rideParameters = rideParameters; } /** * Sets the callback for events occurring in the Ride Request Control such as errors. * * @param rideRequestViewCallback the {@link RideRequestViewCallback} */ public void setRideRequestViewCallback(@NonNull RideRequestViewCallback rideRequestViewCallback) { this.rideRequestViewCallback = rideRequestViewCallback; } /** * Builds a URL with necessary query parameters to load in the {@link WebView}. * * @param rideParameters the {@link RideParameters} to build into the query * @return the URL {@link String} to load in the {@link WebView} */ @NonNull @VisibleForTesting static String buildUrlFromRideParameters(@NonNull Context context, @NonNull RideParameters rideParameters, @NonNull SessionConfiguration loginConfiguration) { final String ENDPOINT = "components"; final String ENVIRONMENT_KEY = "env"; final String HTTPS = "https"; final String PATH = "rides/"; final String SANDBOX = "sandbox"; Uri.Builder builder = new Uri.Builder(); builder.scheme(HTTPS) .authority(ENDPOINT + "." + loginConfiguration.getEndpointRegion().getDomain()) .appendEncodedPath(PATH); if (rideParameters.getUserAgent() == null) { rideParameters.setUserAgent(USER_AGENT_RIDE_VIEW); } RequestDeeplink deeplink = new RequestDeeplink.Builder(context) .setSessionConfiguration(loginConfiguration) .setRideParameters(rideParameters).build(); Uri uri = deeplink.getUri(); builder.encodedQuery(uri.getEncodedQuery()); if (loginConfiguration.getEnvironment() == SessionConfiguration.Environment.SANDBOX) { builder.appendQueryParameter(ENVIRONMENT_KEY, SANDBOX); } return builder.build().toString(); } /** * Creates a {@link Map} of the headers needed to pass to the {@link WebView}. * * @param accessToken the {@link AccessToken} to use for the Authorization header. * @return a {@link Map} containing headers to pass to {@link WebView}. */ @NonNull @VisibleForTesting static Map<String, String> getHeaders(@NonNull AccessToken accessToken) { final String AUTH_HEADER = "Authorization"; final String BEARER = "Bearer"; Map<String, String> headers = new HashMap<String, String>(); headers.put(AUTH_HEADER, BEARER + " " + accessToken.getToken()); return headers; } /** * Initialize the layout, properties, and inner web view. */ private void init(@NonNull Context context) { inflate(getContext(), R.layout.ub__ride_request_view, this); webView = (WebView) findViewById(R.id.ub__ride_request_webview); webView.getSettings().setJavaScriptEnabled(true); webView.getSettings().setGeolocationEnabled(true); webView.getSettings().setAppCacheEnabled(true); webView.getSettings().setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK); webView.setWebChromeClient(new RideRequestWebChromeClient()); webView.setWebViewClient(new RideRequestWebViewClient(new RideRequestWebViewClientCallback() { @Override public void onErrorParsed(@NonNull RideRequestViewError error) { if (rideRequestViewCallback != null) { rideRequestViewCallback.onErrorReceived(error); } } })); } /** * Interface for {@link RideRequestWebViewClient} to communicate with this view. */ @VisibleForTesting interface RideRequestWebViewClientCallback { /** * Called when an error is received in the redirect URL of the {@link WebView}. * * @param error the {@link RideRequestViewError} that occurred. */ void onErrorParsed(@NonNull RideRequestViewError error); } /** * The {@link WebViewClient} that listens for errors in the URL of the {@link WebView}. */ @VisibleForTesting class RideRequestWebViewClient extends WebViewClient { private static final String ERROR_KEY = "error"; private static final String REDIRECT_URL = "uberconnect://oauth"; @NonNull private RideRequestWebViewClientCallback rideRequestWebViewClientCallback; /** * Construct the web view client to listen for and report back errors through a callback. * * @param callback the {@link com.uber.sdk.android.rides.RideRequestView.RideRequestWebViewClientCallback} */ @VisibleForTesting RideRequestWebViewClient(@NonNull RideRequestWebViewClientCallback callback) { rideRequestWebViewClientCallback = callback; } @Override public void onReceivedError( WebView view, WebResourceRequest request, WebResourceError error) { rideRequestWebViewClientCallback.onErrorParsed(RideRequestViewError.CONNECTIVITY_ISSUE); } @Override public void onReceivedHttpError(WebView view, WebResourceRequest request, WebResourceResponse errorResponse) { // This is a no-op necessary for testing as robolectric only supports up // to API 21 and this call was added in API 23 } @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { if (url.toLowerCase().startsWith(REDIRECT_URL)) { Uri uri = Uri.parse(url); Uri fragmentUri = new Uri.Builder().encodedQuery(uri.getFragment()).build(); String errorValue = fragmentUri.getQueryParameter(ERROR_KEY); RideRequestViewError error = RideRequestViewError.UNKNOWN; if (errorValue != null) { try { error = RideRequestViewError.valueOf(errorValue.toUpperCase()); } catch (IllegalArgumentException e) { error = RideRequestViewError.UNKNOWN; } } rideRequestWebViewClientCallback.onErrorParsed(error); return true; } else if (url.startsWith("http:") || url.startsWith("https:")) { return false; } else { Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); RideRequestView.this.getContext().startActivity(intent); return true; } } } /** * The {@link WebChromeClient} used for overriding the {@link GeolocationPermissions} prompt. */ private class RideRequestWebChromeClient extends WebChromeClient { /** * The default implementation does nothing, so permission is never obtained and passed to Javascript. * Overriding to always gain permission as {@link RideRequestView} assumes the app has already gained * location permissions. */ @Override public void onGeolocationPermissionsShowPrompt(String origin, GeolocationPermissions.Callback callback) { callback.invoke(origin, true, false); } } }