Android Open Source - dropbox-android-sdk Abstract Session






From Project

Back to project page dropbox-android-sdk.

License

The source code is released under:

Copyright (c) 2009-2011 Dropbox Inc., http://www.dropbox.com/ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "So...

If you think the Android project dropbox-android-sdk listed in this page is inappropriate, such as containing malicious code/tools or violating the copyright, please email info at java2s dot com, thanks.

Java Source Code

/*
 * Copyright (c) 2009-2011 Dropbox, Inc.
 *//  w  w w  . java  2s .co  m
 * 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.dropbox.client2.session;

import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.security.KeyManagementException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
import java.util.Locale;
import java.util.concurrent.TimeUnit;
import java.util.zip.GZIPInputStream;

import org.apache.http.ConnectionReuseStrategy;
import org.apache.http.Header;
import org.apache.http.HeaderElement;
import org.apache.http.HeaderElementIterator;
import org.apache.http.HeaderIterator;
import org.apache.http.HttpEntity;
import org.apache.http.HttpException;
import org.apache.http.HttpRequest;
import org.apache.http.HttpRequestInterceptor;
import org.apache.http.HttpResponse;
import org.apache.http.HttpResponseInterceptor;
import org.apache.http.HttpVersion;
import org.apache.http.ParseException;
import org.apache.http.ProtocolVersion;
import org.apache.http.TokenIterator;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.conn.ClientConnectionRequest;
import org.apache.http.conn.ConnectionKeepAliveStrategy;
import org.apache.http.conn.params.ConnManagerParams;
import org.apache.http.conn.params.ConnPerRoute;
import org.apache.http.conn.routing.HttpRoute;
import org.apache.http.conn.scheme.PlainSocketFactory;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.conn.scheme.SchemeRegistry;
import org.apache.http.conn.ssl.SSLSocketFactory;
import org.apache.http.entity.HttpEntityWrapper;
import org.apache.http.impl.DefaultConnectionReuseStrategy;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
import org.apache.http.message.BasicHeaderElementIterator;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.HttpConnectionParams;
import org.apache.http.params.HttpParams;
import org.apache.http.params.HttpProtocolParams;
import org.apache.http.protocol.HTTP;
import org.apache.http.protocol.HttpContext;

import com.dropbox.client2.DropboxAPI;
import com.dropbox.client2.SecureSSLSocketFactory;

/**
 * Keeps track of a logged in user and contains configuration options for the
 * {@link DropboxAPI}. This is a base class to use for creating your own
 * {@link Session}s.
 */
public abstract class AbstractSession implements Session {

    private static final String API_SERVER = "api.dropbox.com";
    private static final String CONTENT_SERVER = "api-content.dropbox.com";
    private static final String WEB_SERVER = "www.dropbox.com";

    /** How long connections are kept alive. */
    private static final int KEEP_ALIVE_DURATION_SECS = 20;

    /** How often the monitoring thread checks for connections to close. */
    private static final int KEEP_ALIVE_MONITOR_INTERVAL_SECS = 5;

    /** The default timeout for client connections. */
    private static final int DEFAULT_TIMEOUT_MILLIS = 30000; // 30 seconds

    private final AccessType accessType;
    private final AppKeyPair appKeyPair;
    private AccessTokenPair oauth1AccessToken = null;
    private String oauth2AccessToken = null;

    private HttpClient client = null;

    /**
     * Creates a new session with the given app key and secret.
     * The session will not be linked because it has no access token pair.
     */
    public AbstractSession(AppKeyPair appKeyPair) {
        this(appKeyPair, (AccessTokenPair) null);
    }

    /**
     * Creates a new session with the given app key and secret.
     * The session will be linked to the account corresponding to the
     * given OAuth 1 access token pair.
     */
    public AbstractSession(AppKeyPair appKeyPair, AccessTokenPair oauth1AccessToken) {
        this(appKeyPair, AccessType.AUTO, oauth1AccessToken);
    }

    /**
     * Creates a new session with the given app key and secret.
     * The session will be linked to the account corresponding to the
     * given OAuth 2 access token.
     */
    public AbstractSession(AppKeyPair appKeyPair, String oauth2AccessToken) {
        this(appKeyPair);
        this.oauth2AccessToken = oauth2AccessToken;
    }

    /**
     * Creates a new session with the given app key and secret, and access
     * type. The session will not be linked because it has no access token pair.
     *
     * @deprecated
     *    You don't need to specify the access type anymore.  Use
     *    {@link #AbstractSession(AppKeyPair)}.
     */
    public AbstractSession(AppKeyPair appKeyPair, AccessType type) {
        this(appKeyPair, type, null);
    }

    /**
     * Creates a new session with the given app key and secret, and access
     * type. The session will be linked to the account corresponding to the
     * given access token pair.
     *
     * @deprecated
     *    You don't need to specify the access type anymore.  Use
     *    {@link #AbstractSession(AppKeyPair, AccessTokenPair)}.
     */
    public AbstractSession(AppKeyPair appKeyPair, AccessType type,
            AccessTokenPair oauth1AccessToken) {
        if (appKeyPair == null) throw new IllegalArgumentException("'appKeyPair' must be non-null");
        if (type == null) throw new IllegalArgumentException("'type' must be non-null");

        this.appKeyPair = appKeyPair;
        this.accessType = type;
        this.oauth1AccessToken = oauth1AccessToken;
    }

    /**
     * Links the session with the given OAuth 1 access token and secret.
     */
    public void setAccessTokenPair(AccessTokenPair accessTokenPair) {
        if (accessTokenPair == null) throw new IllegalArgumentException("'oauth1AccessToken' must be non-null");
        this.oauth1AccessToken = accessTokenPair;
        this.oauth2AccessToken = null;  // Clear any OAuth 2 token.
    }

    public void setOAuth2AccessToken(String oauth2AccessToken) {
        if (oauth2AccessToken == null) throw new IllegalArgumentException("'oauth2AccessToken' must be non-null");
        this.oauth2AccessToken = oauth2AccessToken;
        this.oauth1AccessToken = null;  // Clear any OAuth 1 token.
    }

    @Override
    public AppKeyPair getAppKeyPair() {
        return appKeyPair;
    }

    public AccessTokenPair getAccessTokenPair() {
        return oauth1AccessToken;
    }

    public String getOAuth2AccessToken() {
        return oauth2AccessToken;
    }

    public AccessType getAccessType() {
        return accessType;
    }

    /**
     * {@inheritDoc}
     * <br/><br/>
     * The default implementation always returns {@code Locale.ENLISH}, but you
     * are highly encouraged to localize your application and return the system
     * locale instead. Note: as of the time this was written, Dropbox supports
     * the de, en, es, fr, and ja locales - if you use a locale other than
     * these, messages will be returned in English. However, it is good
     * practice to pass along the correct locale as we will add more languages
     * in the future.
     */
    @Override
    public Locale getLocale() {
        return Locale.ENGLISH;
    }

    @Override
    public boolean isLinked() {
        return (oauth1AccessToken != null) || (oauth2AccessToken != null);
    }

    @Override
    public void unlink() {
        oauth1AccessToken = null;
        oauth2AccessToken = null;
    }

    /**
     * Signs the request using either OAuth 1 or OAuth 2, depending on which
     * token was set (setAccessTokenPair vs setOAuth2AccessToken).
     *
     * @see Session#sign
     */
    @Override
    public void sign(HttpRequest request) {
        String v;
        if (oauth2AccessToken != null) {
            v = "Bearer " + oauth2AccessToken;
        } else {
            v = buildOAuth1Header(this.appKeyPair, this.oauth1AccessToken);
        }
        request.addHeader("Authorization", v);
    }

    private static String buildOAuth1Header(AppKeyPair appKeyPair,
            AccessTokenPair signingTokenPair) {
        StringBuilder buf = new StringBuilder();
        buf.append("OAuth oauth_version=\"1.0\"");
        buf.append(", oauth_signature_method=\"PLAINTEXT\"");
        buf.append(", oauth_consumer_key=\"").append(
                encode(appKeyPair.key)).append("\"");

        /*
         * TODO: This is hacky.  The 'signingTokenPair' is null only in auth
         * step 1, when we acquire a request token.  We really should have two
         * different buildOAuthHeader functions for the two different
         * situations.
         */
        String sig;
        if (signingTokenPair != null) {
            buf.append(", oauth_token=\"").append(
                    encode(signingTokenPair.key)).append("\"");
            sig = encode(appKeyPair.secret) + "&"
                    + encode(signingTokenPair.secret);
        } else {
            sig = encode(appKeyPair.secret) + "&";
        }
        buf.append(", oauth_signature=\"").append(sig).append("\"");

        // Note: Don't need nonce or timestamp since we do everything over SSL.
        return buf.toString();
    }

    private static String encode(String s) {
        try {
            return URLEncoder.encode(s, "UTF-8");
        } catch (UnsupportedEncodingException ex) {
            AssertionError ae = new AssertionError("UTF-8 isn't available");
            ae.initCause(ex);
            throw ae;
        }
    }

    /**
     * {@inheritDoc}
     * <br/><br/>
     * The default implementation always returns null.
     */
    @Override
    public synchronized ProxyInfo getProxyInfo() {
        return null;
    }

    /**
     * {@inheritDoc}
     * <br/><br/>
     * The default implementation does all of this and more, including using
     * a connection pool and killing connections after a timeout to use less
     * battery power on mobile devices. It's unlikely that you'll want to
     * change this behavior.
     */
    @Override
    public synchronized HttpClient getHttpClient() {
        if (client == null) {
            // Set up default connection params. There are two routes to
            // Dropbox - api server and content server.
            HttpParams connParams = new BasicHttpParams();
            ConnManagerParams.setMaxConnectionsPerRoute(connParams, new ConnPerRoute() {
                @Override
                public int getMaxForRoute(HttpRoute route) {
                    return 10;
                }
            });
            ConnManagerParams.setMaxTotalConnections(connParams, 20);

            // Initialize SecureSSLSocketFactory for SSL connections
            SSLSocketFactory sslSocketFactory = null;
            try {
                sslSocketFactory = new SecureSSLSocketFactory();
            } catch (KeyManagementException e) {
                throw new RuntimeException(e);
            } catch (UnrecoverableKeyException e) {
                throw new RuntimeException(e);
            } catch (NoSuchAlgorithmException e) {
                throw new RuntimeException(e);
            } catch (KeyStoreException e) {
                throw new RuntimeException(e);
            } catch (CertificateException e) {
                throw new RuntimeException(e);
            } catch (IOException e) {
                throw new RuntimeException(e);
            }

            // Set up scheme registry.
            SchemeRegistry schemeRegistry = new SchemeRegistry();
            schemeRegistry.register(
                    new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));
            schemeRegistry.register(
                    new Scheme("https", sslSocketFactory, 443));

            DBClientConnManager cm = new DBClientConnManager(connParams,
                    schemeRegistry);

            // Set up client params.
            HttpParams httpParams = new BasicHttpParams();
            HttpConnectionParams.setConnectionTimeout(httpParams, DEFAULT_TIMEOUT_MILLIS);
            HttpConnectionParams.setSoTimeout(httpParams, DEFAULT_TIMEOUT_MILLIS);
            HttpConnectionParams.setSocketBufferSize(httpParams, 8192);
            HttpProtocolParams.setUserAgent(httpParams,
                    "OfficialDropboxJavaSDK/" + DropboxAPI.SDK_VERSION);

            DefaultHttpClient c = new DefaultHttpClient(cm, httpParams) {
                @Override
                protected ConnectionKeepAliveStrategy createConnectionKeepAliveStrategy() {
                    return new DBKeepAliveStrategy();
                }

                @Override
                protected ConnectionReuseStrategy createConnectionReuseStrategy() {
                    return new DBConnectionReuseStrategy();
                }
            };

            c.addRequestInterceptor(new HttpRequestInterceptor() {
                @Override
                public void process(
                        final HttpRequest request, final HttpContext context)
                        throws HttpException, IOException {
                    if (!request.containsHeader("Accept-Encoding")) {
                        request.addHeader("Accept-Encoding", "gzip");
                    }
                }
            });

            c.addResponseInterceptor(new HttpResponseInterceptor() {
                @Override
                public void process(
                        final HttpResponse response, final HttpContext context)
                        throws HttpException, IOException {
                    HttpEntity entity = response.getEntity();
                    if (entity != null) {
                        Header ceheader = entity.getContentEncoding();
                        if (ceheader != null) {
                            HeaderElement[] codecs = ceheader.getElements();
                            for (HeaderElement codec : codecs) {
                                if (codec.getName().equalsIgnoreCase("gzip")) {
                                    response.setEntity(
                                            new GzipDecompressingEntity(response.getEntity()));
                                    return;
                                }
                            }
                        }
                    }
                }
            });

            client = c;
        }

        return client;
    }

    /**
     * {@inheritDoc}
     * <br/><br/>
     * The default implementation always sets a 30 second timeout.
     */
    @Override
    public void setRequestTimeout(HttpUriRequest request) {
        HttpParams reqParams = request.getParams();
        HttpConnectionParams.setSoTimeout(reqParams, DEFAULT_TIMEOUT_MILLIS);
        HttpConnectionParams.setConnectionTimeout(reqParams, DEFAULT_TIMEOUT_MILLIS);
    }

    @Override
    public String getAPIServer() {
        return API_SERVER;
    }

    @Override
    public String getContentServer() {
        return CONTENT_SERVER;
    }

    @Override
    public String getWebServer() {
        return WEB_SERVER;
    }

    private static class DBKeepAliveStrategy implements ConnectionKeepAliveStrategy {
        @Override
        public long getKeepAliveDuration(HttpResponse response, HttpContext context) {
            // Keep-alive for the shorter of 20 seconds or what the server specifies.
            long timeout = KEEP_ALIVE_DURATION_SECS * 1000;

            HeaderElementIterator i = new BasicHeaderElementIterator(
                    response.headerIterator(HTTP.CONN_KEEP_ALIVE));
            while (i.hasNext()) {
                HeaderElement element = i.nextElement();
                String name = element.getName();
                String value = element.getValue();
                if (value != null && name.equalsIgnoreCase("timeout")) {
                    try {
                        timeout = Math.min(timeout, Long.parseLong(value) * 1000);
                    } catch (NumberFormatException e) {}
                }
            }

            return timeout;
        }
    }

    private static class DBConnectionReuseStrategy extends DefaultConnectionReuseStrategy {

        /**
         * Implements a patch out in 4.1.x and 4.2 that isn't available in 4.0.x which
         * fixes a bug where connections aren't reused when the response is gzipped.
         * See https://issues.apache.org/jira/browse/HTTPCORE-257 for info about the
         * issue, and http://svn.apache.org/viewvc?view=revision&revision=1124215 for
         * the patch which is copied here.
         */
        @Override
        public boolean keepAlive(final HttpResponse response,
                final HttpContext context) {
            if (response == null) {
                throw new IllegalArgumentException(
                        "HTTP response may not be null.");
            }
            if (context == null) {
                throw new IllegalArgumentException(
                        "HTTP context may not be null.");
            }

            // Check for a self-terminating entity. If the end of the entity
            // will
            // be indicated by closing the connection, there is no keep-alive.
            ProtocolVersion ver = response.getStatusLine().getProtocolVersion();
            Header teh = response.getFirstHeader(HTTP.TRANSFER_ENCODING);
            if (teh != null) {
                if (!HTTP.CHUNK_CODING.equalsIgnoreCase(teh.getValue())) {
                    return false;
                }
            } else {
                Header[] clhs = response.getHeaders(HTTP.CONTENT_LEN);
                // Do not reuse if not properly content-length delimited
                if (clhs == null || clhs.length != 1) {
                    return false;
                }
                Header clh = clhs[0];
                try {
                    int contentLen = Integer.parseInt(clh.getValue());
                    if (contentLen < 0) {
                        return false;
                    }
                } catch (NumberFormatException ex) {
                    return false;
                }
            }

            // Check for the "Connection" header. If that is absent, check for
            // the "Proxy-Connection" header. The latter is an unspecified and
            // broken but unfortunately common extension of HTTP.
            HeaderIterator hit = response.headerIterator(HTTP.CONN_DIRECTIVE);
            if (!hit.hasNext())
                hit = response.headerIterator("Proxy-Connection");

            // Experimental usage of the "Connection" header in HTTP/1.0 is
            // documented in RFC 2068, section 19.7.1. A token "keep-alive" is
            // used to indicate that the connection should be persistent.
            // Note that the final specification of HTTP/1.1 in RFC 2616 does
            // not
            // include this information. Neither is the "Connection" header
            // mentioned in RFC 1945, which informally describes HTTP/1.0.
            //
            // RFC 2616 specifies "close" as the only connection token with a
            // specific meaning: it disables persistent connections.
            //
            // The "Proxy-Connection" header is not formally specified anywhere,
            // but is commonly used to carry one token, "close" or "keep-alive".
            // The "Connection" header, on the other hand, is defined as a
            // sequence of tokens, where each token is a header name, and the
            // token "close" has the above-mentioned additional meaning.
            //
            // To get through this mess, we treat the "Proxy-Connection" header
            // in exactly the same way as the "Connection" header, but only if
            // the latter is missing. We scan the sequence of tokens for both
            // "close" and "keep-alive". As "close" is specified by RFC 2068,
            // it takes precedence and indicates a non-persistent connection.
            // If there is no "close" but a "keep-alive", we take the hint.

            if (hit.hasNext()) {
                try {
                    TokenIterator ti = createTokenIterator(hit);
                    boolean keepalive = false;
                    while (ti.hasNext()) {
                        final String token = ti.nextToken();
                        if (HTTP.CONN_CLOSE.equalsIgnoreCase(token)) {
                            return false;
                        } else if (HTTP.CONN_KEEP_ALIVE.equalsIgnoreCase(token)) {
                            // continue the loop, there may be a "close"
                            // afterwards
                            keepalive = true;
                        }
                    }
                    if (keepalive)
                        return true;
                    // neither "close" nor "keep-alive", use default policy

                } catch (ParseException px) {
                    // invalid connection header means no persistent connection
                    // we don't have logging in HttpCore, so the exception is
                    // lost
                    return false;
                }
            }

            // default since HTTP/1.1 is persistent, before it was
            // non-persistent
            return !ver.lessEquals(HttpVersion.HTTP_1_0);
        }
    }

    private static class DBClientConnManager extends ThreadSafeClientConnManager {
        public DBClientConnManager(HttpParams params, SchemeRegistry schreg) {
            super(params, schreg);
        }

        @Override
        public ClientConnectionRequest requestConnection(HttpRoute route,
                Object state) {
            IdleConnectionCloserThread.ensureRunning(this, KEEP_ALIVE_DURATION_SECS, KEEP_ALIVE_MONITOR_INTERVAL_SECS);
            return super.requestConnection(route, state);
        }
    }

    private static class IdleConnectionCloserThread extends Thread {
        private final DBClientConnManager manager;
        private final int idleTimeoutSeconds;
        private final int checkIntervalMs;
        private static IdleConnectionCloserThread thread = null;

        public IdleConnectionCloserThread(DBClientConnManager manager,
                int idleTimeoutSeconds, int checkIntervalSeconds) {
            super();
            this.manager = manager;
            this.idleTimeoutSeconds = idleTimeoutSeconds;
            this.checkIntervalMs = checkIntervalSeconds * 1000;
        }

        public static synchronized void ensureRunning(
                DBClientConnManager manager, int idleTimeoutSeconds,
                int checkIntervalSeconds) {
            if (thread == null) {
                thread = new IdleConnectionCloserThread(manager,
                        idleTimeoutSeconds, checkIntervalSeconds);
                thread.start();
            }
        }

        @Override
        public void run() {
            try {
                while (true) {
                    synchronized (this) {
                        wait(checkIntervalMs);
                    }
                    manager.closeExpiredConnections();
                    manager.closeIdleConnections(idleTimeoutSeconds, TimeUnit.SECONDS);
                    synchronized (IdleConnectionCloserThread.class) {
                        if (manager.getConnectionsInPool() == 0) {
                            thread = null;
                            return;
                        }
                    }
                }
            } catch (InterruptedException e) {
                thread = null;
            }
        }
    }

    private static class GzipDecompressingEntity extends HttpEntityWrapper {

        /*
         * From Apache HttpClient Examples.
         *
         * ====================================================================
         * Licensed to the Apache Software Foundation (ASF) under one
         * or more contributor license agreements.  See the NOTICE file
         * distributed with this work for additional information
         * regarding copyright ownership.  The ASF licenses this file
         * to you under the Apache License, Version 2.0 (the
         * "License"); you may not use this file except in compliance
         * with the License.  You may obtain a copy of the License at
         *
         *   http://www.apache.org/licenses/LICENSE-2.0
         *
         * Unless required by applicable law or agreed to in writing,
         * software distributed under the License is distributed on an
         * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
         * KIND, either express or implied.  See the License for the
         * specific language governing permissions and limitations
         * under the License.
         * ====================================================================
         *
         * This software consists of voluntary contributions made by many
         * individuals on behalf of the Apache Software Foundation.  For more
         * information on the Apache Software Foundation, please see
         * <http://www.apache.org/>.
         *
         */

        public GzipDecompressingEntity(final HttpEntity entity) {
            super(entity);
        }

        @Override
        public InputStream getContent()
            throws IOException, IllegalStateException {

            // the wrapped entity's getContent() decides about repeatability
            InputStream wrappedin = wrappedEntity.getContent();

            return new GZIPInputStream(wrappedin);
        }

        @Override
        public long getContentLength() {
            // length of ungzipped content is not known
            return -1;
        }

    }
}




Java Source Code List

.CopyBetweenAccounts.java
.SearchCache.java
com.dropbox.android.sample.DBRoulette.java
com.dropbox.android.sample.DownloadRandomPicture.java
com.dropbox.android.sample.UploadPicture.java
com.dropbox.client2.DropboxAPI.java
com.dropbox.client2.ProgressListener.java
com.dropbox.client2.RESTUtility.java
com.dropbox.client2.SdkVersion.java
com.dropbox.client2.SecureSSLSocketFactory.java
com.dropbox.client2.android.AndroidAuthSession.java
com.dropbox.client2.android.AuthActivity.java
com.dropbox.client2.exception.DropboxException.java
com.dropbox.client2.exception.DropboxFileSizeException.java
com.dropbox.client2.exception.DropboxIOException.java
com.dropbox.client2.exception.DropboxLocalStorageFullException.java
com.dropbox.client2.exception.DropboxParseException.java
com.dropbox.client2.exception.DropboxPartialFileException.java
com.dropbox.client2.exception.DropboxProxyChangeException.java
com.dropbox.client2.exception.DropboxSSLException.java
com.dropbox.client2.exception.DropboxServerException.java
com.dropbox.client2.exception.DropboxUnlinkedException.java
com.dropbox.client2.jsonextract.JsonBase.java
com.dropbox.client2.jsonextract.JsonExtractionException.java
com.dropbox.client2.jsonextract.JsonExtractor.java
com.dropbox.client2.jsonextract.JsonList.java
com.dropbox.client2.jsonextract.JsonMap.java
com.dropbox.client2.jsonextract.JsonThing.java
com.dropbox.client2.session.AbstractSession.java
com.dropbox.client2.session.AccessTokenPair.java
com.dropbox.client2.session.AppKeyPair.java
com.dropbox.client2.session.RequestTokenPair.java
com.dropbox.client2.session.Session.java
com.dropbox.client2.session.TokenPair.java
com.dropbox.client2.session.WebAuthSession.java
com.dropbox.client2.session.WebOAuth2Session.java