26

I've begun using the recommended HTTPUrlConnection and moved away from the DefaultHTTPClient. One of the things that I haven't been able to glue back together is the use of a persistent cookie store. I'd like to simply attach a custom cookie handler/manager to my connection to store the cookies. The Android documentation hasn't been very helpful as it wraps up the subject about cookies in two lines.

I've been using LoopJ's PersistentCookieStore earlier and that worked beautifully.

Any idea on how I could set up a persistent cookie store in Android that I can attach to my HTTPUrlConnection that saves and retrieves cookies automatically?

Thanks

Michal
  • 14,455
  • 9
  • 68
  • 97
Mridang Agarwalla
  • 38,521
  • 65
  • 199
  • 353

4 Answers4

26

Its' taken me a few hours but I managed to build a custom cookie storage myself.

You have to attach this by doing this:

public class application extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
       CookieManager cmrCookieMan = new CookieManager(new MyCookieStore(this.objContext), CookiePolicy.ACCEPT_ALL);
       CookieHandler.setDefault(cmrCookieMan);
       }
    }

Here's the actual storage:

/*
 * This is a custom cookie storage for the application. This
 * will store all the cookies to the shared preferences so that it persists
 * across application restarts.
 */
class MyCookieStore implements CookieStore {

    /*
     * The memory storage of the cookies
     */
    private Map<URI, List<HttpCookie>> mapCookies = new HashMap<URI, List<HttpCookie>>();
    /*
     * The instance of the shared preferences
     */
    private final SharedPreferences spePreferences;

    /*
     * @see java.net.CookieStore#add(java.net.URI, java.net.HttpCookie)
     */
    public void add(URI uri, HttpCookie cookie) {

        System.out.println("add");
        System.out.println(cookie.toString());

        List<HttpCookie> cookies = mapCookies.get(uri);
        if (cookies == null) {
            cookies = new ArrayList<HttpCookie>();
            mapCookies.put(uri, cookies);
        }
        cookies.add(cookie);

        Editor ediWriter = spePreferences.edit();
        HashSet<String> setCookies = new HashSet<String>();
        setCookies.add(cookie.toString());
        ediWriter.putStringSet(uri.toString(), spePreferences.getStringSet(uri.toString(), setCookies));
        ediWriter.commit();

    }

   /*
    * Constructor
    * 
    * @param  ctxContext the context of the Activity
    */
    @SuppressWarnings("unchecked")
    public MyCookieStore(Context ctxContext) {

        spePreferences = ctxContext.getSharedPreferences("CookiePrefsFile", 0);
        Map<String, ?> prefsMap = spePreferences.getAll();

        for(Map.Entry<String, ?> entry : prefsMap.entrySet()) {

            for (String strCookie : (HashSet<String>) entry.getValue()) {

                if (!mapCookies.containsKey(entry.getKey())) {

                    List<HttpCookie> lstCookies = new ArrayList<HttpCookie>();
                    lstCookies.addAll(HttpCookie.parse(strCookie));

                    try {

                        mapCookies.put(new URI(entry.getKey()), lstCookies);

                    } catch (URISyntaxException e) {

                        e.printStackTrace();

                    }

                } else {

                    List<HttpCookie> lstCookies = mapCookies.get(entry.getKey());
                    lstCookies.addAll(HttpCookie.parse(strCookie));

                    try {

                        mapCookies.put(new URI(entry.getKey()), lstCookies);

                    } catch (URISyntaxException e) {

                        e.printStackTrace();

                    }

                }

                System.out.println(entry.getKey() + ": " + strCookie);

            }

        }

    }

    /*
     * @see java.net.CookieStore#get(java.net.URI)
     */
    public List<HttpCookie> get(URI uri) {

        List<HttpCookie> lstCookies = mapCookies.get(uri);

        if (lstCookies == null)
            mapCookies.put(uri, new ArrayList<HttpCookie>());

        return mapCookies.get(uri);

    }

    /*
     * @see java.net.CookieStore#removeAll()
     */
    public boolean removeAll() {

        mapCookies.clear();
        return true;

    }        

    /*
     * @see java.net.CookieStore#getCookies()
     */
    public List<HttpCookie> getCookies() {

        Collection<List<HttpCookie>> values = mapCookies.values();

        List<HttpCookie> result = new ArrayList<HttpCookie>();
        for (List<HttpCookie> value : values) {                
            result.addAll(value);                
        }

        return result;

    }

    /*
     * @see java.net.CookieStore#getURIs()
     */
    public List<URI> getURIs() {

        Set<URI> keys = mapCookies.keySet();
        return new ArrayList<URI>(keys);

    }

    /*
     * @see java.net.CookieStore#remove(java.net.URI, java.net.HttpCookie)
     */
    public boolean remove(URI uri, HttpCookie cookie) {

        List<HttpCookie> lstCookies = mapCookies.get(uri);

        if (lstCookies == null)
            return false;

        return lstCookies.remove(cookie);

    }

}
Josh
  • 5,781
  • 1
  • 39
  • 67
Mridang Agarwalla
  • 38,521
  • 65
  • 199
  • 353
  • This hasn't been thoroughly tested but I'll make edits as I go along. Please suggest improvements. – Mridang Agarwalla Sep 10 '12 at 13:38
  • 28
    Sometimes I wonder if I am crazy to expect something as fundamental as persistent cookies to be included in the SDK... I'll be sure to post back if I can suggest any improvements. – Herr Grumps Oct 11 '12 at 05:51
  • 8
    with this implementation, it will add cookies to the exact URI from which they came, meaning they will not be sent to any other URIs at that host. I recommend replacing the passed URI with a new URI consisting of just the host name in order to make this behave as expected. ex: with the current implementation, if you visit yourdomain.com/pageOne.htm, any cookies set will not be sent when you visit yourdomain.com/pageTwo.htm since the URIs are different. the implementation should trim the URI parameter down to just yourdomain.com in order for it to behave as (I) expected. – Keith Jul 15 '13 at 19:08
  • Cookies are not sync-ed to `SharedPreferences` on each delete, add or update. This way, when the app is killed and then it is restarted, the cookie store just created will be the same as the one before the app was killed. – gunar Jul 26 '13 at 06:42
  • @Keith That's awful... what is the point of the API passing the URI if it's going to pass the wrong one? :( – Trejkaz Oct 29 '13 at 23:49
  • 2
    I found the fatal problem in this approach. HttpCookie.toString() does not produce a string which can be parsed again with HttpCookie.parse(). toString() omits the Version parameter as it is designed to be sent in the Cookie: header. If parse() sees no Version parameter, it ignores the rest of the parameters, because it is designed to parse the Set-Cookie/Set-Cookie2 headers. It's tragic that whoever wrote this API made so many mistakes (making cookies non-serialisable, not providing a way to format a cookie for the other header, making the only way of setting it a static method, etc., etc.) – Trejkaz Oct 29 '13 at 23:53
  • This also is only supported on honeycomb and later due to it's usage of putStringSet – Sam Dozor Mar 13 '14 at 15:12
  • [Answering to a similar question](http://stackoverflow.com/a/27161496/980387) I made an implementaion of a persistent CookieStore with: 1. Support for full serialization/deserialization of Cookies (as commented before using toString() and parse() is not possible) and 2. Prevalence of cookie's domain and path attributes over the URI of the request as it should be according to [this](http://stackoverflow.com/a/1063760/980387) – franmontiel Nov 27 '14 at 18:48
  • This works, but before you start fooling around trying to find out how... you need to put the 2 lines that initialize the CookieManager inside the onCreate() method of an extended Application class... see my edit. – Josh Aug 21 '15 at 14:14
  • The line `if (!mapCookies.containsKey(entry.getKey())) {` passes a `String` while the `mapCookies` can contain only `URI`'s as keys. – Yaroslav Mytkalyk Nov 04 '15 at 10:54
1

I used the answer above but changed my add method to the following to handle more than one cookie from the same URI (this cookie store with GAE was treating the session token and the remember token as two separate cookies from the same URI for some reason):

public void add(URI uri, HttpCookie cookie) {


    List<HttpCookie> cookies = mapCookies.get(uri);
    if (cookies == null) {
        cookies = new ArrayList<HttpCookie>();
        mapCookies.put(uri, cookies);
    }
    cookies.add(cookie);

    Editor ediWriter = spePreferences.edit();
    HashSet<String> setCookies = new HashSet<String>();
    setCookies.add(cookie.toString());
    HashSet<String> emptyCookieSet = new HashSet<String>();
    if(spePreferences.contains(uri.toString())){
        emptyCookieSet = (HashSet<String>) spePreferences.getStringSet(uri.toString(), emptyCookieSet);
        if(!emptyCookieSet.isEmpty()){
            if(!emptyCookieSet.contains(cookie.toString())){
            emptyCookieSet.add(cookie.toString());
            ediWriter.putStringSet(uri.toString(), emptyCookieSet);
            }
        }
    }
    else{
        ediWriter.putStringSet(uri.toString(), setCookies);
    }
    ediWriter.commit();
} 

And to access and create a combined cookie:

MyCookieStore store = new MyCookieStore(this.context, false);
String cookie = TextUtils.join(",", store.get(new URI(URLString)));

Attach to connection:

URL urlToRequest = new URL(stringPath);
HttpURLConnection urlConnection = (HttpURLConnection) urlToRequest.openConnection();
urlConnection.setRequestProperty("Cookie", cookie); 
StiggyBr0
  • 93
  • 7
1

There are some basic problems in many custom CookieStore implementation.

The first problem is HttpCookie serialization into string - the HttpCookie.toString() method is not acceptable for this because its result is not suitable for the HttpCookie.parse(String header) method.

The second problem: most of CookieStore implementation (for example, here) does not takes into account the format of HttpCookie.maxAge field. This is a number of seconds to cookie live. But if you simply persist its value and after some time unpersist it, it will be wrong. You must convert maxAge field into something like "expire_at" and persist it instead of maxAge.

Sergey Brunov
  • 11,755
  • 7
  • 39
  • 71
Ruslan Yanchyshyn
  • 2,674
  • 1
  • 21
  • 21
1

Checkout the implementation in the link below. It saves the cookies by hostname like the original java.net.InMemoryCookieStore implementation does.

Besides that it contains a SerializableHttpCookie to be able to serialize the complete HashMap into the SharedPreferences.

https://gist.github.com/jacobtabak/78e226673d5a6a4c4367

Ben Groot
  • 4,915
  • 3
  • 38
  • 44