mirror of
https://github.com/zhigang1992/react-native.git
synced 2026-02-08 17:22:05 +08:00
Support cookies on Android
Summary: This adds a persistent cookie store that shares cookies with WebView. Add a `ForwardingCookieHandler` to OkHttp that uses the underlying Android webkit `CookieManager`. Use a `LazyCookieHandler` to defer initialization of `CookieManager` as this will in turn trigger initialization of the Chromium stack in KitKat+ which takes some time. This was we will incur this cost on a background network thread instead of during startup. Also add a `clearCookies()` method to the network module. Add a cookies example to the XHR example. This example should also work for iOS (except for the clear cookies part). They are for now just scoped to Android. Closes #2792. public Reviewed By: andreicoman11 Differential Revision: D2615550 fb-gh-sync-id: ff726a35f0fc3c7124d2f755448fe24c9d1caf21
This commit is contained in:
committed by
facebook-github-bot-6
parent
f57c2a9140
commit
274c5c78c4
@@ -80,7 +80,8 @@ public class FrescoModule extends ReactContextBaseJavaModule implements
|
||||
}
|
||||
|
||||
Context context = this.getReactApplicationContext().getApplicationContext();
|
||||
OkHttpClient okHttpClient = OkHttpClientProvider.getOkHttpClient();
|
||||
OkHttpClient okHttpClient =
|
||||
OkHttpClientProvider.getCookieAwareOkHttpClient(getReactApplicationContext());
|
||||
ImagePipelineConfig.Builder builder =
|
||||
OkHttpImagePipelineConfigFactory.newBuilder(context, okHttpClient);
|
||||
|
||||
|
||||
@@ -0,0 +1,230 @@
|
||||
// Copyright 2004-present Facebook. All Rights Reserved.
|
||||
|
||||
package com.facebook.react.modules.network;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.CookieHandler;
|
||||
import java.net.URI;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.os.Message;
|
||||
import android.text.TextUtils;
|
||||
import android.webkit.CookieManager;
|
||||
import android.webkit.CookieSyncManager;
|
||||
import android.webkit.ValueCallback;
|
||||
|
||||
import com.facebook.react.bridge.Callback;
|
||||
import com.facebook.react.bridge.GuardedAsyncTask;
|
||||
import com.facebook.react.bridge.GuardedResultAsyncTask;
|
||||
import com.facebook.react.bridge.ReactContext;
|
||||
|
||||
/**
|
||||
* Cookie handler that forwards all cookies to the WebView CookieManager.
|
||||
*
|
||||
* This class relies on CookieManager to persist cookies to disk so cookies may be lost if the
|
||||
* application is terminated before it syncs.
|
||||
*/
|
||||
public class ForwardingCookieHandler extends CookieHandler {
|
||||
private static final String VERSION_ZERO_HEADER = "Set-cookie";
|
||||
private static final String VERSION_ONE_HEADER = "Set-cookie2";
|
||||
private static final String COOKIE_HEADER = "Cookie";
|
||||
|
||||
// As CookieManager was synchronous before API 21 this class emulates the async behavior on <21.
|
||||
private static final boolean USES_LEGACY_STORE = Build.VERSION.SDK_INT < 21;
|
||||
|
||||
private final CookieSaver mCookieSaver;
|
||||
private final ReactContext mContext;
|
||||
private @Nullable CookieManager mCookieManager;
|
||||
|
||||
public ForwardingCookieHandler(ReactContext context) {
|
||||
mContext = context;
|
||||
mCookieSaver = new CookieSaver();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, List<String>> get(URI uri, Map<String, List<String>> headers)
|
||||
throws IOException {
|
||||
String cookies = getCookieManager().getCookie(uri.toString());
|
||||
if (TextUtils.isEmpty(cookies)) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
|
||||
return Collections.singletonMap(COOKIE_HEADER, Collections.singletonList(cookies));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void put(URI uri, Map<String, List<String>> headers) throws IOException {
|
||||
String url = uri.toString();
|
||||
for (Map.Entry<String, List<String>> entry : headers.entrySet()) {
|
||||
String key = entry.getKey();
|
||||
if (key != null && isCookieHeader(key)) {
|
||||
addCookies(url, entry.getValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void clearCookies(final Callback callback) {
|
||||
if (USES_LEGACY_STORE) {
|
||||
new GuardedResultAsyncTask<Boolean>(mContext) {
|
||||
@Override
|
||||
protected Boolean doInBackgroundGuarded() {
|
||||
getCookieManager().removeAllCookie();
|
||||
mCookieSaver.onCookiesModified();
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecuteGuarded(Boolean result) {
|
||||
callback.invoke(result);
|
||||
}
|
||||
}.execute();
|
||||
} else {
|
||||
clearCookiesAsync(callback);
|
||||
}
|
||||
}
|
||||
|
||||
private void clearCookiesAsync(final Callback callback) {
|
||||
getCookieManager().removeAllCookies(
|
||||
new ValueCallback<Boolean>() {
|
||||
@Override
|
||||
public void onReceiveValue(Boolean value) {
|
||||
mCookieSaver.onCookiesModified();
|
||||
callback.invoke(value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void destroy() {
|
||||
if (USES_LEGACY_STORE) {
|
||||
getCookieManager().removeExpiredCookie();
|
||||
mCookieSaver.persistCookies();
|
||||
}
|
||||
}
|
||||
|
||||
private void addCookies(final String url, final List<String> cookies) {
|
||||
if (USES_LEGACY_STORE) {
|
||||
runInBackground(
|
||||
new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
for (String cookie : cookies) {
|
||||
getCookieManager().setCookie(url, cookie);
|
||||
}
|
||||
mCookieSaver.onCookiesModified();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
for (String cookie : cookies) {
|
||||
addCookieAsync(url, cookie);
|
||||
}
|
||||
mCookieSaver.onCookiesModified();
|
||||
}
|
||||
}
|
||||
|
||||
@TargetApi(21)
|
||||
private void addCookieAsync(String url, String cookie) {
|
||||
getCookieManager().setCookie(url, cookie, null);
|
||||
}
|
||||
|
||||
private static boolean isCookieHeader(String name) {
|
||||
return name.equalsIgnoreCase(VERSION_ZERO_HEADER) || name.equalsIgnoreCase(VERSION_ONE_HEADER);
|
||||
}
|
||||
|
||||
private void runInBackground(final Runnable runnable) {
|
||||
new GuardedAsyncTask<Void, Void>(mContext) {
|
||||
@Override
|
||||
protected void doInBackgroundGuarded(Void... params) {
|
||||
runnable.run();
|
||||
}
|
||||
}.execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiating CookieManager in KitKat+ will load the Chromium task taking a 100ish ms so we
|
||||
* do it lazily to make sure it's done on a background thread as needed.
|
||||
*/
|
||||
private CookieManager getCookieManager() {
|
||||
if (mCookieManager == null) {
|
||||
possiblyWorkaroundSyncManager(mContext);
|
||||
mCookieManager = CookieManager.getInstance();
|
||||
|
||||
if (USES_LEGACY_STORE) {
|
||||
mCookieManager.removeExpiredCookie();
|
||||
}
|
||||
}
|
||||
|
||||
return mCookieManager;
|
||||
}
|
||||
|
||||
private static void possiblyWorkaroundSyncManager(Context context) {
|
||||
if (USES_LEGACY_STORE) {
|
||||
// This is to work around a bug where CookieManager may fail to instantiate if
|
||||
// CookieSyncManager has never been created. Note that the sync() may not be required but is
|
||||
// here of legacy reasons.
|
||||
CookieSyncManager syncManager = CookieSyncManager.createInstance(context);
|
||||
syncManager.sync();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Responsible for flushing cookies to disk. Flushes to disk with a maximum delay of 30 seconds.
|
||||
* This class is only active if we are on API < 21.
|
||||
*/
|
||||
private class CookieSaver {
|
||||
private static final int MSG_PERSIST_COOKIES = 1;
|
||||
|
||||
private static final int TIMEOUT = 30 * 1000; // 30 seconds
|
||||
|
||||
private final Handler mHandler;
|
||||
|
||||
public CookieSaver() {
|
||||
mHandler = new Handler(Looper.getMainLooper(), new Handler.Callback() {
|
||||
@Override
|
||||
public boolean handleMessage(Message msg) {
|
||||
if (msg.what == MSG_PERSIST_COOKIES) {
|
||||
persistCookies();
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void onCookiesModified() {
|
||||
if (USES_LEGACY_STORE) {
|
||||
mHandler.sendEmptyMessageDelayed(MSG_PERSIST_COOKIES, TIMEOUT);
|
||||
}
|
||||
}
|
||||
|
||||
public void persistCookies() {
|
||||
mHandler.removeMessages(MSG_PERSIST_COOKIES);
|
||||
runInBackground(
|
||||
new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (USES_LEGACY_STORE) {
|
||||
CookieSyncManager syncManager = CookieSyncManager.getInstance();
|
||||
syncManager.sync();
|
||||
} else {
|
||||
flush();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@TargetApi(21)
|
||||
private void flush() {
|
||||
getCookieManager().flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import javax.annotation.Nullable;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.Reader;
|
||||
import java.net.CookieHandler;
|
||||
|
||||
import com.facebook.react.bridge.Arguments;
|
||||
import com.facebook.react.bridge.GuardedAsyncTask;
|
||||
@@ -72,19 +73,19 @@ public final class NetworkingModule extends ReactContextBaseJavaModule {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param reactContext the ReactContext of the application
|
||||
* @param context the ReactContext of the application
|
||||
*/
|
||||
public NetworkingModule(ReactApplicationContext reactContext) {
|
||||
this(reactContext, null, OkHttpClientProvider.getOkHttpClient());
|
||||
public NetworkingModule(final ReactApplicationContext context) {
|
||||
this(context, null, OkHttpClientProvider.getCookieAwareOkHttpClient(context));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param reactContext the ReactContext of the application
|
||||
* @param context the ReactContext of the application
|
||||
* @param defaultUserAgent the User-Agent header that will be set for all requests where the
|
||||
* caller does not provide one explicitly
|
||||
*/
|
||||
public NetworkingModule(ReactApplicationContext reactContext, String defaultUserAgent) {
|
||||
this(reactContext, defaultUserAgent, OkHttpClientProvider.getOkHttpClient());
|
||||
public NetworkingModule(ReactApplicationContext context, String defaultUserAgent) {
|
||||
this(context, defaultUserAgent, OkHttpClientProvider.getCookieAwareOkHttpClient(context));
|
||||
}
|
||||
|
||||
public NetworkingModule(ReactApplicationContext reactContext, OkHttpClient client) {
|
||||
@@ -100,6 +101,11 @@ public final class NetworkingModule extends ReactContextBaseJavaModule {
|
||||
public void onCatalystInstanceDestroy() {
|
||||
mShuttingDown = true;
|
||||
mClient.cancel(null);
|
||||
|
||||
CookieHandler cookieHandler = mClient.getCookieHandler();
|
||||
if (cookieHandler instanceof ForwardingCookieHandler) {
|
||||
((ForwardingCookieHandler) cookieHandler).destroy();
|
||||
}
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
@@ -311,6 +317,14 @@ public final class NetworkingModule extends ReactContextBaseJavaModule {
|
||||
}.execute();
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void clearCookies(com.facebook.react.bridge.Callback callback) {
|
||||
CookieHandler cookieHandler = mClient.getCookieHandler();
|
||||
if (cookieHandler instanceof ForwardingCookieHandler) {
|
||||
((ForwardingCookieHandler) cookieHandler).clearCookies(callback);
|
||||
}
|
||||
}
|
||||
|
||||
private @Nullable MultipartBuilder constructMultipartBody(
|
||||
ReadableArray body,
|
||||
String contentType,
|
||||
|
||||
@@ -9,7 +9,12 @@
|
||||
|
||||
package com.facebook.react.modules.network;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import com.facebook.react.bridge.ReactContext;
|
||||
|
||||
import com.squareup.okhttp.OkHttpClient;
|
||||
|
||||
/**
|
||||
@@ -19,18 +24,33 @@ import com.squareup.okhttp.OkHttpClient;
|
||||
public class OkHttpClientProvider {
|
||||
|
||||
// Centralized OkHttpClient for all networking requests.
|
||||
private static OkHttpClient sClient;
|
||||
private static @Nullable OkHttpClient sClient;
|
||||
private static ForwardingCookieHandler sCookieHandler;
|
||||
|
||||
public static OkHttpClient getOkHttpClient() {
|
||||
if (sClient == null) {
|
||||
// TODO: #7108751 plug in stetho
|
||||
sClient = new OkHttpClient();
|
||||
|
||||
// No timeouts by default
|
||||
sClient.setConnectTimeout(0, TimeUnit.MILLISECONDS);
|
||||
sClient.setReadTimeout(0, TimeUnit.MILLISECONDS);
|
||||
sClient.setWriteTimeout(0, TimeUnit.MILLISECONDS);
|
||||
sClient = createClient();
|
||||
}
|
||||
return sClient;
|
||||
}
|
||||
|
||||
public static OkHttpClient getCookieAwareOkHttpClient(ReactContext context) {
|
||||
if (sCookieHandler == null) {
|
||||
sCookieHandler = new ForwardingCookieHandler(context);
|
||||
getOkHttpClient().setCookieHandler(sCookieHandler);
|
||||
}
|
||||
return getOkHttpClient();
|
||||
}
|
||||
|
||||
private static OkHttpClient createClient() {
|
||||
// TODO: #7108751 plug in stetho
|
||||
OkHttpClient client = new OkHttpClient();
|
||||
|
||||
// No timeouts by default
|
||||
client.setConnectTimeout(0, TimeUnit.MILLISECONDS);
|
||||
client.setReadTimeout(0, TimeUnit.MILLISECONDS);
|
||||
client.setWriteTimeout(0, TimeUnit.MILLISECONDS);
|
||||
|
||||
return client;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user