1/*
2 * Copyright (C) 2009 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.webkit;
18
19import android.os.Handler;
20import android.os.Message;
21import android.util.Log;
22
23import java.util.Collection;
24import java.util.Map;
25import java.util.HashMap;
26import java.util.HashSet;
27import java.util.Iterator;
28import java.util.Set;
29
30/**
31 * Functionality for manipulating the webstorage databases.
32 */
33public final class WebStorage {
34
35    /**
36     * Encapsulates a callback function to be executed when a new quota is made
37     * available. We primarily want this to allow us to call back the sleeping
38     * WebCore thread from outside the WebViewCore class (as the native call
39     * is private). It is imperative that this the setDatabaseQuota method is
40     * executed once a decision to either allow or deny new quota is made,
41     * otherwise the WebCore thread will remain asleep.
42     */
43    public interface QuotaUpdater {
44        public void updateQuota(long newQuota);
45    };
46
47    // Log tag
48    private static final String TAG = "webstorage";
49
50    // Global instance of a WebStorage
51    private static WebStorage sWebStorage;
52
53    // Message ids
54    static final int UPDATE = 0;
55    static final int SET_QUOTA_ORIGIN = 1;
56    static final int DELETE_ORIGIN = 2;
57    static final int DELETE_ALL = 3;
58    static final int GET_ORIGINS = 4;
59    static final int GET_USAGE_ORIGIN = 5;
60    static final int GET_QUOTA_ORIGIN = 6;
61
62    // Message ids on the UI thread
63    static final int RETURN_ORIGINS = 0;
64    static final int RETURN_USAGE_ORIGIN = 1;
65    static final int RETURN_QUOTA_ORIGIN = 2;
66
67    private static final String ORIGINS = "origins";
68    private static final String ORIGIN = "origin";
69    private static final String CALLBACK = "callback";
70    private static final String USAGE = "usage";
71    private static final String QUOTA = "quota";
72
73    private Map <String, Origin> mOrigins;
74
75    private Handler mHandler = null;
76    private Handler mUIHandler = null;
77
78    /**
79     * Class containing the HTML5 database quota and usage for an origin.
80     */
81    public static class Origin {
82        private String mOrigin = null;
83        private long mQuota = 0;
84        private long mUsage = 0;
85
86        private Origin(String origin, long quota, long usage) {
87            mOrigin = origin;
88            mQuota = quota;
89            mUsage = usage;
90        }
91
92        private Origin(String origin, long quota) {
93            mOrigin = origin;
94            mQuota = quota;
95        }
96
97        private Origin(String origin) {
98            mOrigin = origin;
99        }
100
101        /**
102         * An origin string is created using WebCore::SecurityOrigin::toString().
103         * Note that WebCore::SecurityOrigin uses 0 (which is not printed) for
104         * the port if the port is the default for the protocol. Eg
105         * http://www.google.com and http://www.google.com:80 both record a port
106         * of 0 and hence toString() == 'http://www.google.com' for both.
107         * @return The origin string.
108         */
109        public String getOrigin() {
110            return mOrigin;
111        }
112
113        /**
114         * Returns the quota for this origin's HTML5 database.
115         * @return The quota in bytes.
116         */
117        public long getQuota() {
118            return mQuota;
119        }
120
121        /**
122         * Returns the usage for this origin's HTML5 database.
123         * @return The usage in bytes.
124         */
125        public long getUsage() {
126            return mUsage;
127        }
128    }
129
130    /**
131     * @hide
132     * Message handler, UI side
133     */
134    public void createUIHandler() {
135        if (mUIHandler == null) {
136            mUIHandler = new Handler() {
137                @Override
138                public void handleMessage(Message msg) {
139                    switch (msg.what) {
140                        case RETURN_ORIGINS: {
141                            Map values = (Map) msg.obj;
142                            Map origins = (Map) values.get(ORIGINS);
143                            ValueCallback<Map> callback = (ValueCallback<Map>) values.get(CALLBACK);
144                            callback.onReceiveValue(origins);
145                            } break;
146
147                        case RETURN_USAGE_ORIGIN: {
148                            Map values = (Map) msg.obj;
149                            ValueCallback<Long> callback = (ValueCallback<Long>) values.get(CALLBACK);
150                            callback.onReceiveValue((Long)values.get(USAGE));
151                            } break;
152
153                        case RETURN_QUOTA_ORIGIN: {
154                            Map values = (Map) msg.obj;
155                            ValueCallback<Long> callback = (ValueCallback<Long>) values.get(CALLBACK);
156                            callback.onReceiveValue((Long)values.get(QUOTA));
157                            } break;
158                    }
159                }
160            };
161        }
162    }
163
164    /**
165     * @hide
166     * Message handler, webcore side
167     */
168    public synchronized void createHandler() {
169        if (mHandler == null) {
170            mHandler = new Handler() {
171                @Override
172                public void handleMessage(Message msg) {
173                    switch (msg.what) {
174                        case SET_QUOTA_ORIGIN: {
175                            Origin website = (Origin) msg.obj;
176                            nativeSetQuotaForOrigin(website.getOrigin(),
177                                                    website.getQuota());
178                            } break;
179
180                        case DELETE_ORIGIN: {
181                            Origin website = (Origin) msg.obj;
182                            nativeDeleteOrigin(website.getOrigin());
183                            } break;
184
185                        case DELETE_ALL:
186                            nativeDeleteAllData();
187                            break;
188
189                        case GET_ORIGINS: {
190                            syncValues();
191                            ValueCallback callback = (ValueCallback) msg.obj;
192                            Map origins = new HashMap(mOrigins);
193                            Map values = new HashMap<String, Object>();
194                            values.put(CALLBACK, callback);
195                            values.put(ORIGINS, origins);
196                            postUIMessage(Message.obtain(null, RETURN_ORIGINS, values));
197                            } break;
198
199                        case GET_USAGE_ORIGIN: {
200                            syncValues();
201                            Map values = (Map) msg.obj;
202                            String origin = (String) values.get(ORIGIN);
203                            ValueCallback callback = (ValueCallback) values.get(CALLBACK);
204                            Origin website = mOrigins.get(origin);
205                            Map retValues = new HashMap<String, Object>();
206                            retValues.put(CALLBACK, callback);
207                            if (website != null) {
208                                long usage = website.getUsage();
209                                retValues.put(USAGE, new Long(usage));
210                            }
211                            postUIMessage(Message.obtain(null, RETURN_USAGE_ORIGIN, retValues));
212                            } break;
213
214                        case GET_QUOTA_ORIGIN: {
215                            syncValues();
216                            Map values = (Map) msg.obj;
217                            String origin = (String) values.get(ORIGIN);
218                            ValueCallback callback = (ValueCallback) values.get(CALLBACK);
219                            Origin website = mOrigins.get(origin);
220                            Map retValues = new HashMap<String, Object>();
221                            retValues.put(CALLBACK, callback);
222                            if (website != null) {
223                                long quota = website.getQuota();
224                                retValues.put(QUOTA, new Long(quota));
225                            }
226                            postUIMessage(Message.obtain(null, RETURN_QUOTA_ORIGIN, retValues));
227                            } break;
228
229                        case UPDATE:
230                            syncValues();
231                            break;
232                    }
233                }
234            };
235        }
236    }
237
238    /*
239     * When calling getOrigins(), getUsageForOrigin() and getQuotaForOrigin(),
240     * we need to get the values from webcore, but we cannot block while doing so
241     * as we used to do, as this could result in a full deadlock (other webcore
242     * messages received while we are still blocked here, see http://b/2127737).
243     *
244     * We have to do everything asynchronously, by providing a callback function.
245     * We post a message on the webcore thread (mHandler) that will get the result
246     * from webcore, and we post it back on the UI thread (using mUIHandler).
247     * We can then use the callback function to return the value.
248     */
249
250    /**
251     * Returns a list of origins having a database. The Map is of type
252     * Map<String, Origin>.
253     */
254    public void getOrigins(ValueCallback<Map> callback) {
255        if (callback != null) {
256            if (WebViewCore.THREAD_NAME.equals(Thread.currentThread().getName())) {
257                syncValues();
258                callback.onReceiveValue(mOrigins);
259            } else {
260                postMessage(Message.obtain(null, GET_ORIGINS, callback));
261            }
262        }
263    }
264
265    /**
266     * Returns a list of origins having a database
267     * should only be called from WebViewCore.
268     */
269    Collection<Origin> getOriginsSync() {
270        if (WebViewCore.THREAD_NAME.equals(Thread.currentThread().getName())) {
271            update();
272            return mOrigins.values();
273        }
274        return null;
275    }
276
277    /**
278     * Returns the use for a given origin
279     */
280    public void getUsageForOrigin(String origin, ValueCallback<Long> callback) {
281        if (callback == null) {
282            return;
283        }
284        if (origin == null) {
285            callback.onReceiveValue(null);
286            return;
287        }
288        if (WebViewCore.THREAD_NAME.equals(Thread.currentThread().getName())) {
289            syncValues();
290            Origin website = mOrigins.get(origin);
291            callback.onReceiveValue(new Long(website.getUsage()));
292        } else {
293            HashMap values = new HashMap<String, Object>();
294            values.put(ORIGIN, origin);
295            values.put(CALLBACK, callback);
296            postMessage(Message.obtain(null, GET_USAGE_ORIGIN, values));
297        }
298    }
299
300    /**
301     * Returns the quota for a given origin
302     */
303    public void getQuotaForOrigin(String origin, ValueCallback<Long> callback) {
304        if (callback == null) {
305            return;
306        }
307        if (origin == null) {
308            callback.onReceiveValue(null);
309            return;
310        }
311        if (WebViewCore.THREAD_NAME.equals(Thread.currentThread().getName())) {
312            syncValues();
313            Origin website = mOrigins.get(origin);
314            callback.onReceiveValue(new Long(website.getUsage()));
315        } else {
316            HashMap values = new HashMap<String, Object>();
317            values.put(ORIGIN, origin);
318            values.put(CALLBACK, callback);
319            postMessage(Message.obtain(null, GET_QUOTA_ORIGIN, values));
320        }
321    }
322
323    /**
324     * Set the quota for a given origin
325     */
326    public void setQuotaForOrigin(String origin, long quota) {
327        if (origin != null) {
328            if (WebViewCore.THREAD_NAME.equals(Thread.currentThread().getName())) {
329                nativeSetQuotaForOrigin(origin, quota);
330            } else {
331                postMessage(Message.obtain(null, SET_QUOTA_ORIGIN,
332                    new Origin(origin, quota)));
333            }
334        }
335    }
336
337    /**
338     * Delete a given origin
339     */
340    public void deleteOrigin(String origin) {
341        if (origin != null) {
342            if (WebViewCore.THREAD_NAME.equals(Thread.currentThread().getName())) {
343                nativeDeleteOrigin(origin);
344            } else {
345                postMessage(Message.obtain(null, DELETE_ORIGIN,
346                    new Origin(origin)));
347            }
348        }
349    }
350
351    /**
352     * Delete all databases
353     */
354    public void deleteAllData() {
355        if (WebViewCore.THREAD_NAME.equals(Thread.currentThread().getName())) {
356            nativeDeleteAllData();
357        } else {
358            postMessage(Message.obtain(null, DELETE_ALL));
359        }
360    }
361
362    /**
363     * Sets the maximum size of the ApplicationCache.
364     * This should only ever be called on the WebKit thread.
365     * @hide Pending API council approval
366     */
367    public void setAppCacheMaximumSize(long size) {
368        nativeSetAppCacheMaximumSize(size);
369    }
370
371    /**
372     * Utility function to send a message to our handler
373     */
374    private synchronized void postMessage(Message msg) {
375        if (mHandler != null) {
376            mHandler.sendMessage(msg);
377        }
378    }
379
380    /**
381     * Utility function to send a message to the handler on the UI thread
382     */
383    private void postUIMessage(Message msg) {
384        if (mUIHandler != null) {
385            mUIHandler.sendMessage(msg);
386        }
387    }
388
389    /**
390     * Get the global instance of WebStorage.
391     * @return A single instance of WebStorage.
392     */
393    public static WebStorage getInstance() {
394      if (sWebStorage == null) {
395          sWebStorage = new WebStorage();
396      }
397      return sWebStorage;
398    }
399
400    /**
401     * @hide
402     * Post a Sync request
403     */
404    public void update() {
405        if (WebViewCore.THREAD_NAME.equals(Thread.currentThread().getName())) {
406            syncValues();
407        } else {
408            postMessage(Message.obtain(null, UPDATE));
409        }
410    }
411
412    /**
413     * Run on the webcore thread
414     * set the local values with the current ones
415     */
416    private void syncValues() {
417        Set<String> tmp = nativeGetOrigins();
418        mOrigins = new HashMap<String, Origin>();
419        for (String origin : tmp) {
420            Origin website = new Origin(origin,
421                                 nativeGetQuotaForOrigin(origin),
422                                 nativeGetUsageForOrigin(origin));
423            mOrigins.put(origin, website);
424        }
425    }
426
427    // Native functions
428    private static native Set nativeGetOrigins();
429    private static native long nativeGetUsageForOrigin(String origin);
430    private static native long nativeGetQuotaForOrigin(String origin);
431    private static native void nativeSetQuotaForOrigin(String origin, long quota);
432    private static native void nativeDeleteOrigin(String origin);
433    private static native void nativeDeleteAllData();
434    private static native void nativeSetAppCacheMaximumSize(long size);
435}
436