diff options
author | hjd@chromium.org <hjd@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2014-05-19 17:21:27 +0000 |
---|---|---|
committer | hjd@chromium.org <hjd@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2014-05-19 17:21:27 +0000 |
commit | 976e410c08958691e4c2d887fcf83cb5993f636f (patch) | |
tree | 44c748a76ede1b35fbe7a76137b11c404b3b7d45 /android_webview | |
parent | 9a024901aec2050f12f81e4f2c6dddf1e946f4a4 (diff) | |
download | chromium_src-976e410c08958691e4c2d887fcf83cb5993f636f.zip chromium_src-976e410c08958691e4c2d887fcf83cb5993f636f.tar.gz chromium_src-976e410c08958691e4c2d887fcf83cb5993f636f.tar.bz2 |
Adds Asynchronous APIs to CookieManager
Creates asynchronous versions of SetCookie, removeAllCookie and
removeSessionCookie which take callbacks.
BUG=
TEST=AndroidWebviewTest
Review URL: https://codereview.chromium.org/273873003
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@271411 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'android_webview')
5 files changed, 728 insertions, 196 deletions
diff --git a/android_webview/java/src/org/chromium/android_webview/AwCookieManager.java b/android_webview/java/src/org/chromium/android_webview/AwCookieManager.java index ab91e5d..c7f04aa 100644 --- a/android_webview/java/src/org/chromium/android_webview/AwCookieManager.java +++ b/android_webview/java/src/org/chromium/android_webview/AwCookieManager.java @@ -4,6 +4,11 @@ package org.chromium.android_webview; +import android.os.Handler; +import android.os.Looper; +import android.webkit.ValueCallback; + +import org.chromium.base.CalledByNative; import org.chromium.base.JNINamespace; /** @@ -13,6 +18,22 @@ import org.chromium.base.JNINamespace; */ @JNINamespace("android_webview") public final class AwCookieManager { + + // TODO(hjd): remove after landing android update to use new calls. + public void removeExpiredCookie() { + removeExpiredCookies(); + } + + // TODO(hjd): remove after landing android update to use new calls. + public void removeAllCookie() { + removeAllCookies(); + } + + // TODO(hjd): remove after landing android update to use new calls. + public void removeSessionCookie() { + removeSessionCookies(); + } + /** * Control whether cookie is enabled or disabled * @param accept TRUE if accept cookie @@ -46,14 +67,42 @@ public final class AwCookieManager { } /** + * Synchronous version of setCookie. + */ + public void setCookie(String url, String value) { + nativeSetCookieSync(url, value); + } + + /** + * Deprecated synchronous version of removeSessionCookies. + */ + public void removeSessionCookies() { + nativeRemoveSessionCookiesSync(); + } + + /** + * Deprecated synchronous version of removeAllCookies. + */ + public void removeAllCookies() { + nativeRemoveAllCookiesSync(); + } + + /** * Set cookie for a given url. The old cookie with same host/path/name will * be removed. The new cookie will be added if it is not expired or it does * not have expiration which implies it is session cookie. - * @param url The url which cookie is set for - * @param value The value for set-cookie: in http response header + * @param url The url which cookie is set for. + * @param value The value for set-cookie: in http response header. + * @param callback A callback called with the success status after the cookie is set. */ - public void setCookie(final String url, final String value) { - nativeSetCookie(url, value); + public void setCookie(final String url, final String value, + final ValueCallback<Boolean> callback) { + try { + nativeSetCookie(url, value, CookieCallback.convert(callback)); + } catch (IllegalStateException e) { + throw new IllegalStateException( + "SetCookie must be called on a thread with a running Looper."); + } } /** @@ -69,17 +118,31 @@ public final class AwCookieManager { } /** - * Remove all session cookies, which are cookies without expiration date + * Remove all session cookies, the cookies without an expiration date. + * The value of the callback is true iff at least one cookie was removed. + * @param callback A callback called after the cookies (if any) are removed. */ - public void removeSessionCookie() { - nativeRemoveSessionCookie(); + public void removeSessionCookies(ValueCallback<Boolean> callback) { + try { + nativeRemoveSessionCookies(CookieCallback.convert(callback)); + } catch (IllegalStateException e) { + throw new IllegalStateException( + "removeSessionCookies must be called on a thread with a running Looper."); + } } /** - * Remove all cookies + * Remove all cookies. + * The value of the callback is true iff at least one cookie was removed. + * @param callback A callback called after the cookies (if any) are removed. */ - public void removeAllCookie() { - nativeRemoveAllCookie(); + public void removeAllCookies(ValueCallback<Boolean> callback) { + try { + nativeRemoveAllCookies(CookieCallback.convert(callback)); + } catch (IllegalStateException e) { + throw new IllegalStateException( + "removeAllCookies must be called on a thread with a running Looper."); + } } /** @@ -92,8 +155,8 @@ public final class AwCookieManager { /** * Remove all expired cookies */ - public void removeExpiredCookie() { - nativeRemoveExpiredCookie(); + public void removeExpiredCookies() { + nativeRemoveExpiredCookies(); } public void flushCookieStore() { @@ -120,18 +183,67 @@ public final class AwCookieManager { nativeSetAcceptFileSchemeCookies(accept); } + @CalledByNative + public static void invokeBooleanCookieCallback(CookieCallback<Boolean> callback, + boolean result) { + callback.onReceiveValue(result); + } + + /** + * CookieCallback is a bridge that knows how to call a ValueCallback on its original thread. + * We need to arrange for the users ValueCallback#onReceiveValue to be called on the original + * thread after the work is done. When the API is called we construct a CookieCallback which + * remembers the handler of the current thread. Later the native code uses + * invokeBooleanCookieCallback to call CookieCallback#onReceiveValue which posts a Runnable + * on the handler of the original thread which in turn calls ValueCallback#onReceiveValue. + */ + private static class CookieCallback<T> { + ValueCallback<T> mCallback; + Handler mHandler; + + public CookieCallback(ValueCallback<T> callback, Handler handler) { + mCallback = callback; + mHandler = handler; + } + + public static<T> CookieCallback<T> convert(ValueCallback<T> callback) throws + IllegalStateException { + if (callback == null) { + return null; + } + if (Looper.myLooper() == null) { + throw new IllegalStateException( + "CookieCallback.convert should be called on a thread with a running Looper."); + } + return new CookieCallback<T>(callback, new Handler()); + } + + public void onReceiveValue(final T t) { + mHandler.post(new Runnable() { + @Override + public void run() { + mCallback.onReceiveValue(t); + } + }); + } + } + private native void nativeSetAcceptCookie(boolean accept); private native boolean nativeAcceptCookie(); private native void nativeSetAcceptThirdPartyCookie(boolean accept); private native boolean nativeAcceptThirdPartyCookie(); - private native void nativeSetCookie(String url, String value); + private native void nativeSetCookie(String url, String value, + CookieCallback<Boolean> callback); + private native void nativeSetCookieSync(String url, String value); private native String nativeGetCookie(String url); - private native void nativeRemoveSessionCookie(); - private native void nativeRemoveAllCookie(); - private native void nativeRemoveExpiredCookie(); + private native void nativeRemoveSessionCookies(CookieCallback<Boolean> callback); + private native void nativeRemoveSessionCookiesSync(); + private native void nativeRemoveAllCookies(CookieCallback<Boolean> callback); + private native void nativeRemoveAllCookiesSync(); + private native void nativeRemoveExpiredCookies(); private native void nativeFlushCookieStore(); private native boolean nativeHasCookies(); diff --git a/android_webview/javatests/src/org/chromium/android_webview/test/CookieManagerStartupTest.java b/android_webview/javatests/src/org/chromium/android_webview/test/CookieManagerStartupTest.java index 3a26d37..8cad692 100644 --- a/android_webview/javatests/src/org/chromium/android_webview/test/CookieManagerStartupTest.java +++ b/android_webview/javatests/src/org/chromium/android_webview/test/CookieManagerStartupTest.java @@ -6,11 +6,13 @@ package org.chromium.android_webview.test; import android.content.Context; import android.test.suitebuilder.annotation.MediumTest; +import android.test.suitebuilder.annotation.SmallTest; import org.chromium.android_webview.AwBrowserProcess; import org.chromium.android_webview.AwContents; import org.chromium.android_webview.AwCookieManager; import org.chromium.android_webview.test.util.CommonResources; +import org.chromium.android_webview.test.util.CookieUtils; import org.chromium.base.test.util.Feature; import org.chromium.content.app.ContentMain; import org.chromium.net.test.util.TestWebServer; @@ -64,10 +66,11 @@ public class CookieManagerStartupTest extends AwTestBase { String path = "/cookie_test.html"; String url = webServer.setResponse(path, CommonResources.ABOUT_HTML, null); + CookieUtils.clearCookies(this, mCookieManager); + mCookieManager.setAcceptCookie(true); - mCookieManager.removeAllCookie(); assertTrue(mCookieManager.acceptCookie()); - assertFalse(mCookieManager.hasCookies()); + mCookieManager.setCookie(url, "count=41"); startChromium(); @@ -83,4 +86,23 @@ public class CookieManagerStartupTest extends AwTestBase { } } + @SmallTest + @Feature({"AndroidWebView", "Privacy"}) + public void testAllowFileSchemeCookies() throws Throwable { + assertFalse(mCookieManager.allowFileSchemeCookies()); + mCookieManager.setAcceptFileSchemeCookies(true); + assertTrue(mCookieManager.allowFileSchemeCookies()); + mCookieManager.setAcceptFileSchemeCookies(false); + assertFalse(mCookieManager.allowFileSchemeCookies()); + } + + @SmallTest + @Feature({"AndroidWebView", "Privacy"}) + public void testAllowCookies() throws Throwable { + assertTrue(mCookieManager.acceptCookie()); + mCookieManager.setAcceptCookie(false); + assertFalse(mCookieManager.acceptCookie()); + mCookieManager.setAcceptCookie(true); + assertTrue(mCookieManager.acceptCookie()); + } } diff --git a/android_webview/javatests/src/org/chromium/android_webview/test/CookieManagerTest.java b/android_webview/javatests/src/org/chromium/android_webview/test/CookieManagerTest.java index d105966..f9687ee 100644 --- a/android_webview/javatests/src/org/chromium/android_webview/test/CookieManagerTest.java +++ b/android_webview/javatests/src/org/chromium/android_webview/test/CookieManagerTest.java @@ -6,11 +6,14 @@ package org.chromium.android_webview.test; import android.test.MoreAsserts; import android.test.suitebuilder.annotation.MediumTest; -import android.test.suitebuilder.annotation.SmallTest; import android.util.Pair; +import android.webkit.ValueCallback; + +import static org.chromium.android_webview.test.util.CookieUtils.TestValueCallback; import org.chromium.android_webview.AwContents; import org.chromium.android_webview.AwCookieManager; +import org.chromium.android_webview.test.util.CookieUtils; import org.chromium.android_webview.test.util.JSUtils; import org.chromium.base.test.util.Feature; import org.chromium.net.test.util.TestWebServer; @@ -43,16 +46,17 @@ public class CookieManagerTest extends AwTestBase { mAwContents = testContainerView.getAwContents(); mAwContents.getSettings().setJavaScriptEnabled(true); assertNotNull(mCookieManager); - } - @SmallTest - @Feature({"AndroidWebView", "Privacy"}) - public void testAllowFileSchemeCookies() throws Throwable { - assertFalse(mCookieManager.allowFileSchemeCookies()); - mCookieManager.setAcceptFileSchemeCookies(true); - assertTrue(mCookieManager.allowFileSchemeCookies()); - mCookieManager.setAcceptFileSchemeCookies(false); - assertFalse(mCookieManager.allowFileSchemeCookies()); + // All tests start with no cookies. + try { + clearCookies(); + } catch (Throwable e) { + throw new RuntimeException("Could not clear cookies."); + } + + // But setting cookies enabled. + mCookieManager.setAcceptCookie(true); + assertTrue(mCookieManager.acceptCookie()); } @MediumTest @@ -67,12 +71,10 @@ public class CookieManagerTest extends AwTestBase { String url = webServer.setResponse(path, responseStr, null); mCookieManager.setAcceptCookie(false); - mCookieManager.removeAllCookie(); assertFalse(mCookieManager.acceptCookie()); - assertFalse(mCookieManager.hasCookies()); loadUrlSync(mAwContents, mContentsClient.getOnPageFinishedHelper(), url); - setCookie("test1", "value1"); + setCookieWithJavaScript("test1", "value1"); assertNull(mCookieManager.getCookie(url)); List<Pair<String, String>> responseHeaders = new ArrayList<Pair<String, String>>(); @@ -87,7 +89,7 @@ public class CookieManagerTest extends AwTestBase { url = webServer.setResponse(path, responseStr, null); loadUrlSync(mAwContents, mContentsClient.getOnPageFinishedHelper(), url); - setCookie("test2", "value2"); + setCookieWithJavaScript("test2", "value2"); waitForCookie(url); String cookie = mCookieManager.getCookie(url); assertNotNull(cookie); @@ -102,15 +104,12 @@ public class CookieManagerTest extends AwTestBase { cookie = mCookieManager.getCookie(url); assertNotNull(cookie); validateCookies(cookie, "test2", "header-test2"); - - // clean up all cookies - mCookieManager.removeAllCookie(); } finally { if (webServer != null) webServer.shutdown(); } } - private void setCookie(final String name, final String value) + private void setCookieWithJavaScript(final String name, final String value) throws Throwable { JSUtils.executeJavaScriptAndWaitForResult( this, mAwContents, @@ -121,41 +120,82 @@ public class CookieManagerTest extends AwTestBase { "; expires=' + expirationDate.toUTCString();"); } - private void waitForCookie(final String url) throws Exception { - poll(new Callable<Boolean>() { - @Override - public Boolean call() throws Exception { - return mCookieManager.getCookie(url) != null; - } - }); - } - - private void validateCookies(String responseCookie, String... expectedCookieNames) { - String[] cookies = responseCookie.split(";"); - Set<String> foundCookieNames = new HashSet<String>(); - for (String cookie : cookies) { - foundCookieNames.add(cookie.substring(0, cookie.indexOf("=")).trim()); - } - MoreAsserts.assertEquals( - foundCookieNames, new HashSet<String>(Arrays.asList(expectedCookieNames))); + @MediumTest + @Feature({"AndroidWebView", "Privacy"}) + public void testRemoveAllCookies() throws Exception { + mCookieManager.setCookie("http://www.example.com", "name=test"); + assertTrue(mCookieManager.hasCookies()); + mCookieManager.removeAllCookies(); + assertFalse(mCookieManager.hasCookies()); } @MediumTest @Feature({"AndroidWebView", "Privacy"}) - public void testRemoveAllCookie() throws Exception { - // enable cookie - mCookieManager.setAcceptCookie(true); - assertTrue(mCookieManager.acceptCookie()); + public void testRemoveSessionCookies() throws Exception { + final String url = "http://www.example.com"; + final String sessionCookie = "cookie1=peter"; + final String normalCookie = "cookie2=sue"; - // first there should be no cookie stored - mCookieManager.removeAllCookie(); - mCookieManager.flushCookieStore(); - assertFalse(mCookieManager.hasCookies()); + mCookieManager.setCookie(url, sessionCookie); + mCookieManager.setCookie(url, makeExpiringCookie(normalCookie, 600)); + mCookieManager.removeSessionCookies(); + + String allCookies = mCookieManager.getCookie(url); + assertFalse(allCookies.contains(sessionCookie)); + assertTrue(allCookies.contains(normalCookie)); + } + + @MediumTest + @Feature({"AndroidWebView", "Privacy"}) + public void testSetCookie() throws Throwable { String url = "http://www.example.com"; String cookie = "name=test"; mCookieManager.setCookie(url, cookie); assertEquals(cookie, mCookieManager.getCookie(url)); + } + + @MediumTest + @Feature({"AndroidWebView", "Privacy"}) + public void testHasCookie() throws Throwable { + assertFalse(mCookieManager.hasCookies()); + mCookieManager.setCookie("http://www.example.com", "name=test"); + assertTrue(mCookieManager.hasCookies()); + } + + @MediumTest + @Feature({"AndroidWebView", "Privacy"}) + public void testSetCookieCallback() throws Throwable { + final String url = "http://www.example.com"; + final String cookie = "name=test"; + final String brokenUrl = "foo"; + + final TestValueCallback<Boolean> callback = new TestValueCallback<Boolean>(); + int callCount = callback.getOnReceiveValueHelper().getCallCount(); + + setCookieOnUiThread(url, cookie, callback); + callback.getOnReceiveValueHelper().waitForCallback(callCount); + assertTrue(callback.getValue()); + assertEquals(cookie, mCookieManager.getCookie(url)); + + callCount = callback.getOnReceiveValueHelper().getCallCount(); + + setCookieOnUiThread(brokenUrl, cookie, callback); + callback.getOnReceiveValueHelper().waitForCallback(callCount); + assertFalse(callback.getValue()); + assertEquals(null, mCookieManager.getCookie(brokenUrl)); + } + + @MediumTest + @Feature({"AndroidWebView", "Privacy"}) + public void testSetCookieNullCallback() throws Throwable { + mCookieManager.setAcceptCookie(true); + assertTrue(mCookieManager.acceptCookie()); + + final String url = "http://www.example.com"; + final String cookie = "name=test"; + + mCookieManager.setCookie(url, cookie, null); poll(new Callable<Boolean>() { @Override @@ -163,9 +203,38 @@ public class CookieManagerTest extends AwTestBase { return mCookieManager.hasCookies(); } }); + } - // clean up all cookies - mCookieManager.removeAllCookie(); + @MediumTest + @Feature({"AndroidWebView", "Privacy"}) + public void testRemoveAllCookiesCallback() throws Throwable { + TestValueCallback<Boolean> callback = new TestValueCallback<Boolean>(); + int callCount = callback.getOnReceiveValueHelper().getCallCount(); + + mCookieManager.setCookie("http://www.example.com", "name=test"); + + // When we remove all cookies the first time some cookies are removed. + removeAllCookiesOnUiThread(callback); + callback.getOnReceiveValueHelper().waitForCallback(callCount); + assertTrue(callback.getValue()); + assertFalse(mCookieManager.hasCookies()); + + callCount = callback.getOnReceiveValueHelper().getCallCount(); + + // The second time none are removed. + removeAllCookiesOnUiThread(callback); + callback.getOnReceiveValueHelper().waitForCallback(callCount); + assertFalse(callback.getValue()); + } + + @MediumTest + @Feature({"AndroidWebView", "Privacy"}) + public void testRemoveAllCookiesNullCallback() throws Throwable { + mCookieManager.setCookie("http://www.example.com", "name=test"); + + mCookieManager.removeAllCookies(null); + + // Eventually the cookies are removed. poll(new Callable<Boolean>() { @Override public Boolean call() throws Exception { @@ -176,67 +245,113 @@ public class CookieManagerTest extends AwTestBase { @MediumTest @Feature({"AndroidWebView", "Privacy"}) - @SuppressWarnings("deprecation") - public void testCookieExpiration() throws Exception { - // enable cookie - mCookieManager.setAcceptCookie(true); - assertTrue(mCookieManager.acceptCookie()); - mCookieManager.removeAllCookie(); - assertFalse(mCookieManager.hasCookies()); - + public void testRemoveSessionCookiesCallback() throws Throwable { final String url = "http://www.example.com"; - final String cookie1 = "cookie1=peter"; - final String cookie2 = "cookie2=sue"; - final String cookie3 = "cookie3=marc"; + final String sessionCookie = "cookie1=peter"; + final String normalCookie = "cookie2=sue"; - mCookieManager.setCookie(url, cookie1); // session cookie + TestValueCallback<Boolean> callback = new TestValueCallback<Boolean>(); + int callCount = callback.getOnReceiveValueHelper().getCallCount(); - Date date = new Date(); - date.setTime(date.getTime() + 1000 * 600); - String value2 = cookie2 + "; expires=" + date.toGMTString(); - mCookieManager.setCookie(url, value2); // expires in 10min + mCookieManager.setCookie(url, sessionCookie); + mCookieManager.setCookie(url, makeExpiringCookie(normalCookie, 600)); - long expiration = 3000; - date = new Date(); - date.setTime(date.getTime() + expiration); - String value3 = cookie3 + "; expires=" + date.toGMTString(); - mCookieManager.setCookie(url, value3); // expires in 3s + // When there is a session cookie then it is removed. + removeSessionCookiesOnUiThread(callback); + callback.getOnReceiveValueHelper().waitForCallback(callCount); + assertTrue(callback.getValue()); + String allCookies = mCookieManager.getCookie(url); + assertTrue(!allCookies.contains(sessionCookie)); + assertTrue(allCookies.contains(normalCookie)); + callCount = callback.getOnReceiveValueHelper().getCallCount(); + + // If there are no session cookies then none are removed. + removeSessionCookiesOnUiThread(callback); + callback.getOnReceiveValueHelper().waitForCallback(callCount); + assertFalse(callback.getValue()); + } + + @MediumTest + @Feature({"AndroidWebView", "Privacy"}) + public void testRemoveSessionCookiesNullCallback() throws Throwable { + final String url = "http://www.example.com"; + final String sessionCookie = "cookie1=peter"; + final String normalCookie = "cookie2=sue"; + + mCookieManager.setCookie(url, sessionCookie); + mCookieManager.setCookie(url, makeExpiringCookie(normalCookie, 600)); String allCookies = mCookieManager.getCookie(url); - assertTrue(allCookies.contains(cookie1)); - assertTrue(allCookies.contains(cookie2)); - assertTrue(allCookies.contains(cookie3)); + assertTrue(allCookies.contains(sessionCookie)); + assertTrue(allCookies.contains(normalCookie)); - mCookieManager.removeSessionCookie(); - poll(new Callable<Boolean>() { - @Override - public Boolean call() throws Exception { - String c = mCookieManager.getCookie(url); - return !c.contains(cookie1) && c.contains(cookie2) && c.contains(cookie3); - } - }); + mCookieManager.removeSessionCookies(null); - Thread.sleep(expiration + 1000); // wait for cookie to expire - mCookieManager.removeExpiredCookie(); + // Eventually the session cookie is removed. poll(new Callable<Boolean>() { @Override public Boolean call() throws Exception { String c = mCookieManager.getCookie(url); - return !c.contains(cookie1) && c.contains(cookie2) && !c.contains(cookie3); + return !c.contains(sessionCookie) && c.contains(normalCookie); } }); + } + + @MediumTest + @Feature({"AndroidWebView", "Privacy"}) + public void testExpiredCookiesAreNotSet() throws Exception { + final String url = "http://www.example.com"; + final String cookie = "cookie1=peter"; + + mCookieManager.setCookie(url, makeExpiringCookie(cookie, -1)); + assertNull(mCookieManager.getCookie(url)); + } + + @MediumTest + @Feature({"AndroidWebView", "Privacy"}) + public void testCookiesExpire() throws Exception { + final String url = "http://www.example.com"; + final String cookie = "cookie1=peter"; - mCookieManager.removeAllCookie(); + mCookieManager.setCookie(url, makeExpiringCookieMs(cookie, 1200)); + + // The cookie exists: + assertTrue(mCookieManager.hasCookies()); + + // But eventually expires: poll(new Callable<Boolean>() { @Override public Boolean call() throws Exception { - return mCookieManager.getCookie(url) == null; + return !mCookieManager.hasCookies(); } }); } @MediumTest @Feature({"AndroidWebView", "Privacy"}) + public void testCookieExpiration() throws Exception { + final String url = "http://www.example.com"; + final String sessionCookie = "cookie1=peter"; + final String longCookie = "cookie2=marc"; + + mCookieManager.setCookie(url, sessionCookie); + mCookieManager.setCookie(url, makeExpiringCookie(longCookie, 600)); + + String allCookies = mCookieManager.getCookie(url); + assertTrue(allCookies.contains(sessionCookie)); + assertTrue(allCookies.contains(longCookie)); + + // Removing expired cookies doesn't have an observable effect but since people will still + // be calling it for a while it shouldn't break anything either. + mCookieManager.removeExpiredCookies(); + + allCookies = mCookieManager.getCookie(url); + assertTrue(allCookies.contains(sessionCookie)); + assertTrue(allCookies.contains(longCookie)); + } + + @MediumTest + @Feature({"AndroidWebView", "Privacy"}) public void testThirdPartyCookie() throws Throwable { TestWebServer webServer = null; try { @@ -254,9 +369,7 @@ public class CookieManagerTest extends AwTestBase { // Turn global allow on. mCookieManager.setAcceptCookie(true); - mCookieManager.removeAllCookie(); assertTrue(mCookieManager.acceptCookie()); - assertFalse(mCookieManager.hasCookies()); // When third party cookies are disabled... mCookieManager.setAcceptThirdPartyCookie(false); @@ -327,9 +440,7 @@ public class CookieManagerTest extends AwTestBase { webServer = new TestWebServer(false); mCookieManager.setAcceptCookie(true); - mCookieManager.removeAllCookie(); assertTrue(mCookieManager.acceptCookie()); - assertFalse(mCookieManager.hasCookies()); // When third party cookies are disabled... mCookieManager.setAcceptThirdPartyCookie(false); @@ -397,4 +508,71 @@ public class CookieManagerTest extends AwTestBase { private String toThirdPartyUrl(String url) { return url.replace("localhost", "127.0.0.1"); } + + private void setCookieOnUiThread(final String url, final String cookie, + final ValueCallback<Boolean> callback) throws Throwable { + runTestOnUiThread(new Runnable() { + @Override + public void run() { + mCookieManager.setCookie(url, cookie, callback); + } + }); + } + + private void removeSessionCookiesOnUiThread(final ValueCallback<Boolean> callback) + throws Throwable { + runTestOnUiThread(new Runnable() { + @Override + public void run() { + mCookieManager.removeSessionCookies(callback); + } + }); + } + + private void removeAllCookiesOnUiThread(final ValueCallback<Boolean> callback) + throws Throwable { + runTestOnUiThread(new Runnable() { + @Override + public void run() { + mCookieManager.removeAllCookies(callback); + } + }); + } + + /** + * Clears all cookies synchronously. + */ + private void clearCookies() throws Throwable { + CookieUtils.clearCookies(this, mCookieManager); + } + + private void waitForCookie(final String url) throws Exception { + poll(new Callable<Boolean>() { + @Override + public Boolean call() throws Exception { + return mCookieManager.getCookie(url) != null; + } + }); + } + + private void validateCookies(String responseCookie, String... expectedCookieNames) { + String[] cookies = responseCookie.split(";"); + Set<String> foundCookieNames = new HashSet<String>(); + for (String cookie : cookies) { + foundCookieNames.add(cookie.substring(0, cookie.indexOf("=")).trim()); + } + MoreAsserts.assertEquals( + foundCookieNames, new HashSet<String>(Arrays.asList(expectedCookieNames))); + } + + private String makeExpiringCookie(String cookie, int secondsTillExpiry) { + return makeExpiringCookieMs(cookie, secondsTillExpiry * 1000); + } + + @SuppressWarnings("deprecation") + private String makeExpiringCookieMs(String cookie, int millisecondsTillExpiry) { + Date date = new Date(); + date.setTime(date.getTime() + millisecondsTillExpiry); + return cookie + "; expires=" + date.toGMTString(); + } } diff --git a/android_webview/javatests/src/org/chromium/android_webview/test/util/CookieUtils.java b/android_webview/javatests/src/org/chromium/android_webview/test/util/CookieUtils.java new file mode 100644 index 0000000..15484f7 --- /dev/null +++ b/android_webview/javatests/src/org/chromium/android_webview/test/util/CookieUtils.java @@ -0,0 +1,84 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.android_webview.test.util; + +import android.webkit.ValueCallback; + +import junit.framework.Assert; + +import org.chromium.android_webview.AwCookieManager; +import org.chromium.android_webview.test.AwTestBase; +import org.chromium.content.browser.test.util.CallbackHelper; + +/** + * Useful functions for testing the CookieManager. + */ +public class CookieUtils { + private CookieUtils() { + } + + + /** + * A CallbackHelper for use with setCookie/removeXXXCookie. + */ + public static class TestValueCallback<T> implements ValueCallback<T> { + /** + * We only have one intresting method on ValueCallback: onReceiveValue. + */ + public static class OnReceiveValueHelper<T> extends CallbackHelper { + private T mValue; + + public T getValue() { + assert getCallCount() > 0; + return mValue; + } + + public void notifyCalled(T value) { + mValue = value; + notifyCalled(); + } + } + + private OnReceiveValueHelper<T> mOnReceiveValueHelper; + + public TestValueCallback() { + mOnReceiveValueHelper = new OnReceiveValueHelper<T>(); + } + + public OnReceiveValueHelper getOnReceiveValueHelper() { + return mOnReceiveValueHelper; + } + + @Override + public void onReceiveValue(T value) { + mOnReceiveValueHelper.notifyCalled(value); + } + + public T getValue() { + return mOnReceiveValueHelper.getValue(); + } + } + + /** + * Clear all cookies from the CookieManager synchronously then assert they are gone. + * @param cookieManager the CookieManager on which to remove cookies. + * @param timeoutMs the timeout in milliseconds for waiting for the callback to complete. + */ + public static void clearCookies(AwTestBase awTestBase, final AwCookieManager cookieManager) + throws Throwable { + + final TestValueCallback<Boolean> callback = new TestValueCallback<Boolean>(); + int callCount = callback.getOnReceiveValueHelper().getCallCount(); + + awTestBase.runTestOnUiThread(new Runnable() { + @Override + public void run() { + cookieManager.removeAllCookies(callback); + } + }); + callback.getOnReceiveValueHelper().waitForCallback(callCount); + Assert.assertFalse(cookieManager.hasCookies()); + } +} diff --git a/android_webview/native/cookie_manager.cc b/android_webview/native/cookie_manager.cc index adb0ec8..90fa23f 100644 --- a/android_webview/native/cookie_manager.cc +++ b/android_webview/native/cookie_manager.cc @@ -35,8 +35,10 @@ #include "net/url_request/url_request_context.h" using base::FilePath; +using base::WaitableEvent; using base::android::ConvertJavaStringToUTF8; using base::android::ConvertJavaStringToUTF16; +using base::android::ScopedJavaGlobalRef; using content::BrowserThread; using net::CookieList; using net::CookieMonster; @@ -46,15 +48,67 @@ using net::CookieMonster; // depending on the URLRequestContext. // See issue http://crbug.com/157683 -// All functions on the CookieManager can be called from any thread, including -// threads without a message loop. BrowserThread::IO is used to call methods -// on CookieMonster that needs to be called, and called back, on a chrome -// thread. +// On the CookieManager methods without a callback and methods with a callback +// when that callback is null can be called from any thread, including threads +// without a message loop. Methods with a non-null callback must be called on +// a thread with a running message loop. namespace android_webview { namespace { +typedef base::Callback<void(bool)> BoolCallback; +typedef base::Callback<void(int)> IntCallback; + +// Holds a Java BooleanCookieCallback, knows how to invoke it and turn it +// into a base callback. +class BoolCookieCallbackHolder { + public: + BoolCookieCallbackHolder(JNIEnv* env, jobject callback) { + callback_.Reset(env, callback); + } + + void Invoke(bool result) { + if (!callback_.is_null()) { + JNIEnv* env = base::android::AttachCurrentThread(); + Java_AwCookieManager_invokeBooleanCookieCallback( + env, callback_.obj(), result); + } + } + + static BoolCallback ConvertToCallback( + scoped_ptr<BoolCookieCallbackHolder> me) { + return base::Bind(&BoolCookieCallbackHolder::Invoke, + base::Owned(me.release())); + } + + private: + ScopedJavaGlobalRef<jobject> callback_; + DISALLOW_COPY_AND_ASSIGN(BoolCookieCallbackHolder); +}; + +// Construct a closure which signals a waitable event if and when the closure +// is called the waitable event must still exist. +static base::Closure SignalEventClosure(WaitableEvent* completion) { + return base::Bind(&WaitableEvent::Signal, base::Unretained(completion)); +} + +static void DiscardBool(const base::Closure& f, bool b) { + f.Run(); +} + +static BoolCallback BoolCallbackAdapter(const base::Closure& f) { + return base::Bind(&DiscardBool, f); +} + +static void DiscardInt(const base::Closure& f, int i) { + f.Run(); +} + +static IntCallback IntCallbackAdapter(const base::Closure& f) { + return base::Bind(&DiscardInt, f); +} + // Are cookies allowed for file:// URLs by default? const bool kDefaultFileSchemeAllowed = false; @@ -98,11 +152,17 @@ class CookieManager { bool AcceptCookie(); void SetAcceptThirdPartyCookie(bool accept); bool AcceptThirdPartyCookie(); - void SetCookie(const GURL& host, const std::string& cookie_value); + void SetCookie(const GURL& host, + const std::string& cookie_value, + scoped_ptr<BoolCookieCallbackHolder> callback); + void SetCookieSync(const GURL& host, + const std::string& cookie_value); std::string GetCookie(const GURL& host); - void RemoveSessionCookie(); - void RemoveAllCookie(); - void RemoveExpiredCookie(); + void RemoveSessionCookies(scoped_ptr<BoolCookieCallbackHolder> callback); + void RemoveAllCookies(scoped_ptr<BoolCookieCallbackHolder> callback); + void RemoveAllCookiesSync(); + void RemoveSessionCookiesSync(); + void RemoveExpiredCookies(); void FlushCookieStore(); bool HasCookies(); bool AllowFileSchemeCookies(); @@ -114,32 +174,31 @@ class CookieManager { CookieManager(); ~CookieManager(); - typedef base::Callback<void(base::WaitableEvent*)> CookieTask; - void ExecCookieTask(const CookieTask& task); + void ExecCookieTaskSync(const base::Callback<void(BoolCallback)>& task); + void ExecCookieTaskSync(const base::Callback<void(IntCallback)>& task); + void ExecCookieTaskSync(const base::Callback<void(base::Closure)>& task); + void ExecCookieTask(const base::Closure& task); - void SetCookieAsyncHelper( + void SetCookieHelper( const GURL& host, const std::string& value, - base::WaitableEvent* completion); - void SetCookieCompleted(base::WaitableEvent* completion, bool success); + BoolCallback callback); - void GetCookieValueAsyncHelper( - const GURL& host, - std::string* result, - base::WaitableEvent* completion); - void GetCookieValueCompleted(base::WaitableEvent* completion, + void GetCookieValueAsyncHelper(const GURL& host, + std::string* result, + base::Closure complete); + void GetCookieValueCompleted(base::Closure complete, std::string* result, const std::string& value); - void RemoveSessionCookieAsyncHelper(base::WaitableEvent* completion); - void RemoveAllCookieAsyncHelper(base::WaitableEvent* completion); - void RemoveCookiesCompleted(base::WaitableEvent* completion, int num_deleted); + void RemoveSessionCookiesHelper(BoolCallback callback); + void RemoveAllCookiesHelper(BoolCallback callback); + void RemoveCookiesCompleted(BoolCallback callback, int num_deleted); - void FlushCookieStoreAsyncHelper(base::WaitableEvent* completion); + void FlushCookieStoreAsyncHelper(base::Closure complete); - void HasCookiesAsyncHelper(bool* result, - base::WaitableEvent* completion); - void HasCookiesCompleted(base::WaitableEvent* completion, + void HasCookiesAsyncHelper(bool* result, base::Closure complete); + void HasCookiesCompleted(base::Closure complete, bool* result, const CookieList& cookies); @@ -221,22 +280,53 @@ void CookieManager::EnsureCookieMonsterExistsLocked() { cookie_monster_backend_thread_->message_loop_proxy()); } -// Executes the |task| on the |cookie_monster_proxy_| message loop. -void CookieManager::ExecCookieTask(const CookieTask& task) { - base::WaitableEvent completion(false, false); - base::AutoLock lock(cookie_monster_lock_); +// Executes the |task| on the |cookie_monster_proxy_| message loop and +// waits for it to complete before returning. - EnsureCookieMonsterExistsLocked(); +// To execute a CookieTask synchronously you must arrange for Signal to be +// called on the waitable event at some point. You can call the bool or int +// versions of ExecCookieTaskSync, these will supply the caller with a dummy +// callback which takes an int/bool, throws it away and calls Signal. +// Alternatively you can call the version which supplies a Closure in which +// case you must call Run on it when you want the unblock the calling code. - cookie_monster_proxy_->PostTask(FROM_HERE, base::Bind(task, &completion)); +// Ignore a bool callback. +void CookieManager::ExecCookieTaskSync( + const base::Callback<void(BoolCallback)>& task) { + WaitableEvent completion(false, false); + ExecCookieTask( + base::Bind(task, BoolCallbackAdapter(SignalEventClosure(&completion)))); + ScopedAllowWaitForLegacyWebViewApi wait; + completion.Wait(); +} + +// Ignore an int callback. +void CookieManager::ExecCookieTaskSync( + const base::Callback<void(IntCallback)>& task) { + WaitableEvent completion(false, false); + ExecCookieTask( + base::Bind(task, IntCallbackAdapter(SignalEventClosure(&completion)))); + ScopedAllowWaitForLegacyWebViewApi wait; + completion.Wait(); +} - // We always wait for the posted task to complete, even when it doesn't return - // a value, because previous versions of the CookieManager API were - // synchronous in most/all cases and the caller may be relying on this. +// Call the supplied closure when you want to signal that the blocked code can +// continue. +void CookieManager::ExecCookieTaskSync( + const base::Callback<void(base::Closure)>& task) { + WaitableEvent completion(false, false); + ExecCookieTask(base::Bind(task, SignalEventClosure(&completion))); ScopedAllowWaitForLegacyWebViewApi wait; completion.Wait(); } +// Executes the |task| on the |cookie_monster_proxy_| message loop. +void CookieManager::ExecCookieTask(const base::Closure& task) { + base::AutoLock lock(cookie_monster_lock_); + EnsureCookieMonsterExistsLocked(); + cookie_monster_proxy_->PostTask(FROM_HERE, task); +} + scoped_refptr<net::CookieStore> CookieManager::CreateBrowserThreadCookieStore( AwBrowserContext* browser_context) { base::AutoLock lock(cookie_monster_lock_); @@ -283,49 +373,51 @@ bool CookieManager::AcceptThirdPartyCookie() { return AwCookieAccessPolicy::GetInstance()->GetThirdPartyAllowAccess(); } -void CookieManager::SetCookie(const GURL& host, +void CookieManager::SetCookie( + const GURL& host, + const std::string& cookie_value, + scoped_ptr<BoolCookieCallbackHolder> callback_holder) { + BoolCallback callback = + BoolCookieCallbackHolder::ConvertToCallback(callback_holder.Pass()); + ExecCookieTask(base::Bind(&CookieManager::SetCookieHelper, + base::Unretained(this), + host, + cookie_value, + callback)); +} + +void CookieManager::SetCookieSync(const GURL& host, const std::string& cookie_value) { - ExecCookieTask(base::Bind(&CookieManager::SetCookieAsyncHelper, + ExecCookieTaskSync(base::Bind(&CookieManager::SetCookieHelper, base::Unretained(this), host, cookie_value)); } -void CookieManager::SetCookieAsyncHelper( +void CookieManager::SetCookieHelper( const GURL& host, const std::string& value, - base::WaitableEvent* completion) { + const BoolCallback callback) { net::CookieOptions options; options.set_include_httponly(); cookie_monster_->SetCookieWithOptionsAsync( - host, value, options, - base::Bind(&CookieManager::SetCookieCompleted, - base::Unretained(this), - completion)); -} - -void CookieManager::SetCookieCompleted(base::WaitableEvent* completion, - bool success) { - // The CookieManager API does not return a value for SetCookie, - // so we don't need to propagate the |success| value back to the caller. - completion->Signal(); + host, value, options, callback); } std::string CookieManager::GetCookie(const GURL& host) { std::string cookie_value; - ExecCookieTask(base::Bind(&CookieManager::GetCookieValueAsyncHelper, + ExecCookieTaskSync(base::Bind(&CookieManager::GetCookieValueAsyncHelper, base::Unretained(this), host, &cookie_value)); - return cookie_value; } void CookieManager::GetCookieValueAsyncHelper( const GURL& host, std::string* result, - base::WaitableEvent* completion) { + base::Closure complete) { net::CookieOptions options; options.set_include_httponly(); @@ -334,70 +426,85 @@ void CookieManager::GetCookieValueAsyncHelper( options, base::Bind(&CookieManager::GetCookieValueCompleted, base::Unretained(this), - completion, + complete, result)); } -void CookieManager::GetCookieValueCompleted(base::WaitableEvent* completion, +void CookieManager::GetCookieValueCompleted(base::Closure complete, std::string* result, const std::string& value) { *result = value; - completion->Signal(); + complete.Run(); } -void CookieManager::RemoveSessionCookie() { - ExecCookieTask(base::Bind(&CookieManager::RemoveSessionCookieAsyncHelper, +void CookieManager::RemoveSessionCookies( + scoped_ptr<BoolCookieCallbackHolder> callback_holder) { + BoolCallback callback = + BoolCookieCallbackHolder::ConvertToCallback(callback_holder.Pass()); + ExecCookieTask(base::Bind(&CookieManager::RemoveSessionCookiesHelper, + base::Unretained(this), + callback)); +} + +void CookieManager::RemoveSessionCookiesSync() { + ExecCookieTaskSync(base::Bind(&CookieManager::RemoveSessionCookiesHelper, base::Unretained(this))); } -void CookieManager::RemoveSessionCookieAsyncHelper( - base::WaitableEvent* completion) { +void CookieManager::RemoveSessionCookiesHelper( + BoolCallback callback) { cookie_monster_->DeleteSessionCookiesAsync( base::Bind(&CookieManager::RemoveCookiesCompleted, base::Unretained(this), - completion)); + callback)); } -void CookieManager::RemoveCookiesCompleted(base::WaitableEvent* completion, - int num_deleted) { - // The CookieManager API does not return a value for removeSessionCookie or - // removeAllCookie, so we don't need to propagate the |num_deleted| value back - // to the caller. - completion->Signal(); +void CookieManager::RemoveCookiesCompleted( + BoolCallback callback, + int num_deleted) { + callback.Run(num_deleted > 0); } -void CookieManager::RemoveAllCookie() { - ExecCookieTask(base::Bind(&CookieManager::RemoveAllCookieAsyncHelper, +void CookieManager::RemoveAllCookies( + scoped_ptr<BoolCookieCallbackHolder> callback_holder) { + BoolCallback callback = + BoolCookieCallbackHolder::ConvertToCallback(callback_holder.Pass()); + ExecCookieTask(base::Bind(&CookieManager::RemoveAllCookiesHelper, + base::Unretained(this), + callback)); +} + +void CookieManager::RemoveAllCookiesSync() { + ExecCookieTaskSync(base::Bind(&CookieManager::RemoveAllCookiesHelper, base::Unretained(this))); } -void CookieManager::RemoveAllCookieAsyncHelper( - base::WaitableEvent* completion) { +void CookieManager::RemoveAllCookiesHelper( + const BoolCallback callback) { cookie_monster_->DeleteAllAsync( base::Bind(&CookieManager::RemoveCookiesCompleted, base::Unretained(this), - completion)); + callback)); } -void CookieManager::RemoveExpiredCookie() { +void CookieManager::RemoveExpiredCookies() { // HasCookies will call GetAllCookiesAsync, which in turn will force a GC. HasCookies(); } -void CookieManager::FlushCookieStoreAsyncHelper( - base::WaitableEvent* completion) { - cookie_monster_->FlushStore(base::Bind(&base::WaitableEvent::Signal, - base::Unretained(completion))); -} - void CookieManager::FlushCookieStore() { - ExecCookieTask(base::Bind(&CookieManager::FlushCookieStoreAsyncHelper, + ExecCookieTaskSync(base::Bind(&CookieManager::FlushCookieStoreAsyncHelper, base::Unretained(this))); } +void CookieManager::FlushCookieStoreAsyncHelper( + base::Closure complete) { + cookie_monster_->FlushStore(complete); +} + bool CookieManager::HasCookies() { bool has_cookies; - ExecCookieTask(base::Bind(&CookieManager::HasCookiesAsyncHelper, + ExecCookieTaskSync(base::Bind(&CookieManager::HasCookiesAsyncHelper, base::Unretained(this), &has_cookies)); return has_cookies; @@ -406,19 +513,19 @@ bool CookieManager::HasCookies() { // TODO(kristianm): Simplify this, copying the entire list around // should not be needed. void CookieManager::HasCookiesAsyncHelper(bool* result, - base::WaitableEvent* completion) { + base::Closure complete) { cookie_monster_->GetAllCookiesAsync( base::Bind(&CookieManager::HasCookiesCompleted, base::Unretained(this), - completion, + complete, result)); } -void CookieManager::HasCookiesCompleted(base::WaitableEvent* completion, +void CookieManager::HasCookiesCompleted(base::Closure complete, bool* result, const CookieList& cookies) { *result = cookies.size() != 0; - completion->Signal(); + complete.Run(); } bool CookieManager::AllowFileSchemeCookies() { @@ -467,11 +574,26 @@ static jboolean AcceptThirdPartyCookie(JNIEnv* env, jobject obj) { return CookieManager::GetInstance()->AcceptThirdPartyCookie(); } -static void SetCookie(JNIEnv* env, jobject obj, jstring url, jstring value) { +static void SetCookie(JNIEnv* env, + jobject obj, + jstring url, + jstring value, + jobject java_callback) { + GURL host(ConvertJavaStringToUTF16(env, url)); + std::string cookie_value(ConvertJavaStringToUTF8(env, value)); + scoped_ptr<BoolCookieCallbackHolder> callback( + new BoolCookieCallbackHolder(env, java_callback)); + CookieManager::GetInstance()->SetCookie(host, cookie_value, callback.Pass()); +} + +static void SetCookieSync(JNIEnv* env, + jobject obj, + jstring url, + jstring value) { GURL host(ConvertJavaStringToUTF16(env, url)); std::string cookie_value(ConvertJavaStringToUTF8(env, value)); - CookieManager::GetInstance()->SetCookie(host, cookie_value); + CookieManager::GetInstance()->SetCookieSync(host, cookie_value); } static jstring GetCookie(JNIEnv* env, jobject obj, jstring url) { @@ -482,16 +604,30 @@ static jstring GetCookie(JNIEnv* env, jobject obj, jstring url) { CookieManager::GetInstance()->GetCookie(host)).Release(); } -static void RemoveSessionCookie(JNIEnv* env, jobject obj) { - CookieManager::GetInstance()->RemoveSessionCookie(); +static void RemoveSessionCookies(JNIEnv* env, + jobject obj, + jobject java_callback) { + scoped_ptr<BoolCookieCallbackHolder> callback( + new BoolCookieCallbackHolder(env, java_callback)); + CookieManager::GetInstance()->RemoveSessionCookies(callback.Pass()); +} + +static void RemoveSessionCookiesSync(JNIEnv* env, jobject obj) { + CookieManager::GetInstance()->RemoveSessionCookiesSync(); +} + +static void RemoveAllCookies(JNIEnv* env, jobject obj, jobject java_callback) { + scoped_ptr<BoolCookieCallbackHolder> callback( + new BoolCookieCallbackHolder(env, java_callback)); + CookieManager::GetInstance()->RemoveAllCookies(callback.Pass()); } -static void RemoveAllCookie(JNIEnv* env, jobject obj) { - CookieManager::GetInstance()->RemoveAllCookie(); +static void RemoveAllCookiesSync(JNIEnv* env, jobject obj) { + CookieManager::GetInstance()->RemoveAllCookiesSync(); } -static void RemoveExpiredCookie(JNIEnv* env, jobject obj) { - CookieManager::GetInstance()->RemoveExpiredCookie(); +static void RemoveExpiredCookies(JNIEnv* env, jobject obj) { + CookieManager::GetInstance()->RemoveExpiredCookies(); } static void FlushCookieStore(JNIEnv* env, jobject obj) { |