WebStorageSizeManager.java revision eb9032c67077aa5aa6ddf928bf14a45534f24776
179e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu/*
279e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu * Copyright (C) 2009 The Android Open Source Project
379e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu *
479e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu * Licensed under the Apache License, Version 2.0 (the "License");
579e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu * you may not use this file except in compliance with the License.
679e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu * You may obtain a copy of the License at
779e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu *
879e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu *      http://www.apache.org/licenses/LICENSE-2.0
979e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu *
1079e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu * Unless required by applicable law or agreed to in writing, software
1179e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu * distributed under the License is distributed on an "AS IS" BASIS,
1279e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1379e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu * See the License for the specific language governing permissions and
1479e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu * limitations under the License.
1579e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu */
1679e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu
1779e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescupackage com.android.browser;
1879e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu
1986943777260a399df282517241085d0c5e10ee88Andrei Popescuimport android.app.Notification;
2086943777260a399df282517241085d0c5e10ee88Andrei Popescuimport android.app.NotificationManager;
2186943777260a399df282517241085d0c5e10ee88Andrei Popescuimport android.app.PendingIntent;
2279e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescuimport android.content.Context;
2386943777260a399df282517241085d0c5e10ee88Andrei Popescuimport android.content.Intent;
2479e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescuimport android.os.StatFs;
2579e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescuimport android.util.Log;
2679e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescuimport android.webkit.WebStorage;
2779e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu
2879e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescuimport java.io.File;
2979e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescuimport java.util.Set;
3079e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu
3179e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu
3279e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu/**
3379e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu * Package level class for managing the disk size consumed by the WebDatabase
3479e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu * and ApplicationCaches APIs (henceforth called Web storage).
3579e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu *
3679e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu * Currently, the situation on the WebKit side is as follows:
3779e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu *  - WebDatabase enforces a quota for each origin.
3879e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu *  - Session/LocalStorage do not enforce any disk limits.
3979e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu *  - ApplicationCaches enforces a maximum size for all origins.
4079e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu *
4179e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu * The WebStorageSizeManager maintains a global limit for the disk space
4279e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu * consumed by the WebDatabase and ApplicationCaches. As soon as WebKit will
4379e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu * have a limit for Session/LocalStorage, this class will manage the space used
4479e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu * by those APIs as well.
4579e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu *
4679e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu * The global limit is computed as a function of the size of the partition where
4779e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu * these APIs store their data (they must store it on the same partition for
4879e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu * this to work) and the size of the available space on that partition.
4979e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu * The global limit is not subject to user configuration but we do provide
5079e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu * a debug-only setting.
5179e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu * TODO(andreip): implement the debug setting.
5279e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu *
5379e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu * The size of the disk space used for Web storage is initially divided between
5479e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu * WebDatabase and ApplicationCaches as follows:
5579e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu *
5679e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu * 75% for WebDatabase
5779e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu * 25% for ApplicationCaches
5879e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu *
5979e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu * When an origin's database usage reaches its current quota, WebKit invokes
6079e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu * the following callback function:
6179e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu * - exceededDatabaseQuota(Frame* frame, const String& database_name);
6279e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu * Note that the default quota for a new origin is 0, so we will receive the
6379e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu * 'exceededDatabaseQuota' callback before a new origin gets the chance to
6479e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu * create its first database.
6579e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu *
6679e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu * When the total ApplicationCaches usage reaches its current quota, WebKit
6779e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu * invokes the following callback function:
6879e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu * - void reachedMaxAppCacheSize(int64_t spaceNeeded);
6979e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu *
7079e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu * The WebStorageSizeManager's main job is to respond to the above two callbacks
7179e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu * by inspecting the amount of unused Web storage quota (i.e. global limit -
7279e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu * sum of all other origins' quota) and deciding if a quota increase for the
7379e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu * out-of-space origin is allowed or not.
7479e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu *
7525a1523642bead2f7e7f929ba9d6d1143dce06a0Ben Murdoch * The default quota for an origin is its estimated size. If we cannot satisfy
7625a1523642bead2f7e7f929ba9d6d1143dce06a0Ben Murdoch * the estimated size, then WebCore will not create the database.
7779e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu * Quota increases are done in steps, where the increase step is
7879e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu * min(QUOTA_INCREASE_STEP, unused_quota).
7979e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu *
8079e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu * When all the Web storage space is used, the WebStorageSizeManager creates
8179e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu * a system notification that will guide the user to the WebSettings UI. There,
8279e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu * the user can free some of the Web storage space by deleting all the data used
8379e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu * by an origin.
8479e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu */
8579e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescuclass WebStorageSizeManager {
8679e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu    // Logging flags.
8779e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu    private final static boolean LOGV_ENABLED = com.android.browser.Browser.LOGV_ENABLED;
8879e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu    private final static boolean LOGD_ENABLED = com.android.browser.Browser.LOGD_ENABLED;
8979e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu    private final static String LOGTAG = "browser";
9079e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu    // The default quota value for an origin.
91907c5763bb700c705108948328c8892500e59185Andrei Popescu    public final static long ORIGIN_DEFAULT_QUOTA = 3 * 1024 * 1024;  // 3MB
9279e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu    // The default value for quota increases.
93907c5763bb700c705108948328c8892500e59185Andrei Popescu    public final static long QUOTA_INCREASE_STEP = 1 * 1024 * 1024;  // 1MB
9486943777260a399df282517241085d0c5e10ee88Andrei Popescu    // Extra padding space for appcache maximum size increases. This is needed
9586943777260a399df282517241085d0c5e10ee88Andrei Popescu    // because WebKit sends us an estimate of the amount of space needed
9686943777260a399df282517241085d0c5e10ee88Andrei Popescu    // but this estimate may, currently, be slightly less than what is actually
9786943777260a399df282517241085d0c5e10ee88Andrei Popescu    // needed. We therefore add some 'padding'.
9886943777260a399df282517241085d0c5e10ee88Andrei Popescu    // TODO(andreip): fix this in WebKit.
9986943777260a399df282517241085d0c5e10ee88Andrei Popescu    public final static long APPCACHE_MAXSIZE_PADDING = 512 * 1024; // 512KB
10086943777260a399df282517241085d0c5e10ee88Andrei Popescu    // The system status bar notification id.
10186943777260a399df282517241085d0c5e10ee88Andrei Popescu    private final static int OUT_OF_SPACE_ID = 1;
102eb9032c67077aa5aa6ddf928bf14a45534f24776Nicolas Roard    // The time of the last out of space notification
103eb9032c67077aa5aa6ddf928bf14a45534f24776Nicolas Roard    private static long mLastOutOfSpaceNotificationTime = -1;
104eb9032c67077aa5aa6ddf928bf14a45534f24776Nicolas Roard    // Delay between two notification in ms
105eb9032c67077aa5aa6ddf928bf14a45534f24776Nicolas Roard    private final static long NOTIFICATION_INTERVAL = 5 * 60 * 1000;
106eb9032c67077aa5aa6ddf928bf14a45534f24776Nicolas Roard    // Delay in ms used when resetting the notification time
107eb9032c67077aa5aa6ddf928bf14a45534f24776Nicolas Roard    private final static long RESET_NOTIFICATION_INTERVAL = 3 * 1000;
10879e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu    // The application context.
109907c5763bb700c705108948328c8892500e59185Andrei Popescu    private final Context mContext;
11079e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu    // The global Web storage limit.
111907c5763bb700c705108948328c8892500e59185Andrei Popescu    private final long mGlobalLimit;
11279e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu    // The maximum size of the application cache file.
11379e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu    private long mAppCacheMaxSize;
11479e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu
11579e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu    /**
116907c5763bb700c705108948328c8892500e59185Andrei Popescu     * Interface used by the WebStorageSizeManager to obtain information
117907c5763bb700c705108948328c8892500e59185Andrei Popescu     * about the underlying file system. This functionality is separated
118907c5763bb700c705108948328c8892500e59185Andrei Popescu     * into its own interface mainly for testing purposes.
11979e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu     */
120907c5763bb700c705108948328c8892500e59185Andrei Popescu    public interface DiskInfo {
121907c5763bb700c705108948328c8892500e59185Andrei Popescu        /**
122907c5763bb700c705108948328c8892500e59185Andrei Popescu         * @return the size of the free space in the file system.
123907c5763bb700c705108948328c8892500e59185Andrei Popescu         */
124907c5763bb700c705108948328c8892500e59185Andrei Popescu        public long getFreeSpaceSizeBytes();
125907c5763bb700c705108948328c8892500e59185Andrei Popescu
126907c5763bb700c705108948328c8892500e59185Andrei Popescu        /**
127907c5763bb700c705108948328c8892500e59185Andrei Popescu         * @return the total size of the file system.
128907c5763bb700c705108948328c8892500e59185Andrei Popescu         */
129907c5763bb700c705108948328c8892500e59185Andrei Popescu        public long getTotalSizeBytes();
130907c5763bb700c705108948328c8892500e59185Andrei Popescu    };
131907c5763bb700c705108948328c8892500e59185Andrei Popescu
132907c5763bb700c705108948328c8892500e59185Andrei Popescu    private DiskInfo mDiskInfo;
133907c5763bb700c705108948328c8892500e59185Andrei Popescu    // For convenience, we provide a DiskInfo implementation that uses StatFs.
134907c5763bb700c705108948328c8892500e59185Andrei Popescu    public static class StatFsDiskInfo implements DiskInfo {
135907c5763bb700c705108948328c8892500e59185Andrei Popescu        private StatFs mFs;
136907c5763bb700c705108948328c8892500e59185Andrei Popescu
137907c5763bb700c705108948328c8892500e59185Andrei Popescu        public StatFsDiskInfo(String path) {
138907c5763bb700c705108948328c8892500e59185Andrei Popescu            mFs = new StatFs(path);
139907c5763bb700c705108948328c8892500e59185Andrei Popescu        }
140907c5763bb700c705108948328c8892500e59185Andrei Popescu
141907c5763bb700c705108948328c8892500e59185Andrei Popescu        public long getFreeSpaceSizeBytes() {
142907c5763bb700c705108948328c8892500e59185Andrei Popescu            return mFs.getAvailableBlocks() * mFs.getBlockSize();
143907c5763bb700c705108948328c8892500e59185Andrei Popescu        }
144907c5763bb700c705108948328c8892500e59185Andrei Popescu
145907c5763bb700c705108948328c8892500e59185Andrei Popescu        public long getTotalSizeBytes() {
146907c5763bb700c705108948328c8892500e59185Andrei Popescu            return mFs.getBlockCount() * mFs.getBlockSize();
147907c5763bb700c705108948328c8892500e59185Andrei Popescu        }
148907c5763bb700c705108948328c8892500e59185Andrei Popescu    };
149907c5763bb700c705108948328c8892500e59185Andrei Popescu
150907c5763bb700c705108948328c8892500e59185Andrei Popescu    /**
151907c5763bb700c705108948328c8892500e59185Andrei Popescu     * Interface used by the WebStorageSizeManager to obtain information
152907c5763bb700c705108948328c8892500e59185Andrei Popescu     * about the appcache file. This functionality is separated into its own
153907c5763bb700c705108948328c8892500e59185Andrei Popescu     * interface mainly for testing purposes.
154907c5763bb700c705108948328c8892500e59185Andrei Popescu     */
155907c5763bb700c705108948328c8892500e59185Andrei Popescu    public interface AppCacheInfo {
156907c5763bb700c705108948328c8892500e59185Andrei Popescu        /**
157907c5763bb700c705108948328c8892500e59185Andrei Popescu         * @return the current size of the appcache file.
158907c5763bb700c705108948328c8892500e59185Andrei Popescu         */
159907c5763bb700c705108948328c8892500e59185Andrei Popescu        public long getAppCacheSizeBytes();
160907c5763bb700c705108948328c8892500e59185Andrei Popescu    };
161907c5763bb700c705108948328c8892500e59185Andrei Popescu
162907c5763bb700c705108948328c8892500e59185Andrei Popescu    // For convenience, we provide an AppCacheInfo implementation.
163907c5763bb700c705108948328c8892500e59185Andrei Popescu    public static class WebKitAppCacheInfo implements AppCacheInfo {
164907c5763bb700c705108948328c8892500e59185Andrei Popescu        // The name of the application cache file. Keep in sync with
165907c5763bb700c705108948328c8892500e59185Andrei Popescu        // WebCore/loader/appcache/ApplicationCacheStorage.cpp
166907c5763bb700c705108948328c8892500e59185Andrei Popescu        private final static String APPCACHE_FILE = "ApplicationCache.db";
167907c5763bb700c705108948328c8892500e59185Andrei Popescu        private String mAppCachePath;
168907c5763bb700c705108948328c8892500e59185Andrei Popescu
169907c5763bb700c705108948328c8892500e59185Andrei Popescu        public WebKitAppCacheInfo(String path) {
170907c5763bb700c705108948328c8892500e59185Andrei Popescu            mAppCachePath = path;
171907c5763bb700c705108948328c8892500e59185Andrei Popescu        }
172907c5763bb700c705108948328c8892500e59185Andrei Popescu
173907c5763bb700c705108948328c8892500e59185Andrei Popescu        public long getAppCacheSizeBytes() {
174907c5763bb700c705108948328c8892500e59185Andrei Popescu            File file = new File(mAppCachePath
175907c5763bb700c705108948328c8892500e59185Andrei Popescu                    + File.separator
176907c5763bb700c705108948328c8892500e59185Andrei Popescu                    + APPCACHE_FILE);
177907c5763bb700c705108948328c8892500e59185Andrei Popescu            return file.length();
178907c5763bb700c705108948328c8892500e59185Andrei Popescu        }
179907c5763bb700c705108948328c8892500e59185Andrei Popescu    };
180907c5763bb700c705108948328c8892500e59185Andrei Popescu
181907c5763bb700c705108948328c8892500e59185Andrei Popescu    /**
182907c5763bb700c705108948328c8892500e59185Andrei Popescu     * Public ctor
183907c5763bb700c705108948328c8892500e59185Andrei Popescu     * @param ctx is the application context
184907c5763bb700c705108948328c8892500e59185Andrei Popescu     * @param diskInfo is the DiskInfo instance used to query the file system.
185907c5763bb700c705108948328c8892500e59185Andrei Popescu     * @param appCacheInfo is the AppCacheInfo used to query info about the
186907c5763bb700c705108948328c8892500e59185Andrei Popescu     * appcache file.
187907c5763bb700c705108948328c8892500e59185Andrei Popescu     */
188907c5763bb700c705108948328c8892500e59185Andrei Popescu    public WebStorageSizeManager(Context ctx, DiskInfo diskInfo,
189907c5763bb700c705108948328c8892500e59185Andrei Popescu            AppCacheInfo appCacheInfo) {
190907c5763bb700c705108948328c8892500e59185Andrei Popescu        mContext = ctx;
191907c5763bb700c705108948328c8892500e59185Andrei Popescu        mDiskInfo = diskInfo;
192907c5763bb700c705108948328c8892500e59185Andrei Popescu        mGlobalLimit = getGlobalLimit();
193907c5763bb700c705108948328c8892500e59185Andrei Popescu        // The initial max size of the app cache is either 25% of the global
194907c5763bb700c705108948328c8892500e59185Andrei Popescu        // limit or the current size of the app cache file, whichever is bigger.
195907c5763bb700c705108948328c8892500e59185Andrei Popescu        mAppCacheMaxSize = Math.max(mGlobalLimit / 4,
196907c5763bb700c705108948328c8892500e59185Andrei Popescu                appCacheInfo.getAppCacheSizeBytes());
19779e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu    }
19879e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu
19979e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu    /**
20079e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu     * Returns the maximum size of the application cache.
20179e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu     */
20279e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu    public long getAppCacheMaxSize() {
20379e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu        return mAppCacheMaxSize;
20479e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu    }
20579e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu
20679e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu    /**
20779e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu     * The origin has exceeded its database quota.
20879e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu     * @param url the URL that exceeded the quota
20979e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu     * @param databaseIdentifier the identifier of the database on
21079e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu     *     which the transaction that caused the quota overflow was run
21179e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu     * @param currentQuota the current quota for the origin.
21279e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu     * @param totalUsedQuota is the sum of all origins' quota.
21379e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu     * @param quotaUpdater The callback to run when a decision to allow or
21479e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu     *     deny quota has been made. Don't forget to call this!
21579e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu     */
21679e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu    public void onExceededDatabaseQuota(String url,
21725a1523642bead2f7e7f929ba9d6d1143dce06a0Ben Murdoch        String databaseIdentifier, long currentQuota, long estimatedSize,
21825a1523642bead2f7e7f929ba9d6d1143dce06a0Ben Murdoch        long totalUsedQuota, WebStorage.QuotaUpdater quotaUpdater) {
21979e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu        if(LOGV_ENABLED) {
22079e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu            Log.v(LOGTAG,
22179e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu                  "Received onExceededDatabaseQuota for "
22279e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu                  + url
22379e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu                  + ":"
22479e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu                  + databaseIdentifier
22579e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu                  + "(current quota: "
22679e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu                  + currentQuota
227907c5763bb700c705108948328c8892500e59185Andrei Popescu                  + ", total used quota: "
228907c5763bb700c705108948328c8892500e59185Andrei Popescu                  + totalUsedQuota
22979e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu                  + ")");
23079e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu        }
23179e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu        long totalUnusedQuota = mGlobalLimit - totalUsedQuota - mAppCacheMaxSize;
23279e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu
233907c5763bb700c705108948328c8892500e59185Andrei Popescu        if (totalUnusedQuota <= 0) {
23479e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu            // There definitely isn't any more space. Fire notifications
23586943777260a399df282517241085d0c5e10ee88Andrei Popescu            // if needed and exit.
23686943777260a399df282517241085d0c5e10ee88Andrei Popescu            if (totalUsedQuota > 0) {
23786943777260a399df282517241085d0c5e10ee88Andrei Popescu                // We only fire the notification if there are some other websites
23886943777260a399df282517241085d0c5e10ee88Andrei Popescu                // using some of the quota. This avoids the degenerate case where
23986943777260a399df282517241085d0c5e10ee88Andrei Popescu                // the first ever website to use Web storage tries to use more
24086943777260a399df282517241085d0c5e10ee88Andrei Popescu                // data than it is actually available. In such a case, showing
24186943777260a399df282517241085d0c5e10ee88Andrei Popescu                // the notification would not help at all since there is nothing
24286943777260a399df282517241085d0c5e10ee88Andrei Popescu                // the user can do.
24386943777260a399df282517241085d0c5e10ee88Andrei Popescu                scheduleOutOfSpaceNotification();
24486943777260a399df282517241085d0c5e10ee88Andrei Popescu            }
24579e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu            quotaUpdater.updateQuota(currentQuota);
24679e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu            if(LOGV_ENABLED) {
24779e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu                Log.v(LOGTAG, "onExceededDatabaseQuota: out of space.");
24879e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu            }
24979e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu            return;
25079e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu        }
25179e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu        // We have enough space inside mGlobalLimit.
25279e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu        long newOriginQuota = currentQuota;
25379e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu        if (newOriginQuota == 0) {
25425a1523642bead2f7e7f929ba9d6d1143dce06a0Ben Murdoch            // This is a new origin, give it the size it asked for if possible.
25525a1523642bead2f7e7f929ba9d6d1143dce06a0Ben Murdoch            // If we cannot satisfy the estimatedSize, we should return 0 as
25625a1523642bead2f7e7f929ba9d6d1143dce06a0Ben Murdoch            // returning a value less that what the site requested will lead
25725a1523642bead2f7e7f929ba9d6d1143dce06a0Ben Murdoch            // to webcore not creating the database.
25825a1523642bead2f7e7f929ba9d6d1143dce06a0Ben Murdoch            if (totalUnusedQuota >= estimatedSize) {
25925a1523642bead2f7e7f929ba9d6d1143dce06a0Ben Murdoch                newOriginQuota = estimatedSize;
26025a1523642bead2f7e7f929ba9d6d1143dce06a0Ben Murdoch            } else {
26125a1523642bead2f7e7f929ba9d6d1143dce06a0Ben Murdoch                if (LOGV_ENABLED) {
26225a1523642bead2f7e7f929ba9d6d1143dce06a0Ben Murdoch                    Log.v(LOGTAG,
26325a1523642bead2f7e7f929ba9d6d1143dce06a0Ben Murdoch                          "onExceededDatabaseQuota: Unable to satisfy" +
26425a1523642bead2f7e7f929ba9d6d1143dce06a0Ben Murdoch                          " estimatedSize for the new database " +
26525a1523642bead2f7e7f929ba9d6d1143dce06a0Ben Murdoch                          " (estimatedSize: " + estimatedSize +
26625a1523642bead2f7e7f929ba9d6d1143dce06a0Ben Murdoch                          ", unused quota: " + totalUnusedQuota);
26725a1523642bead2f7e7f929ba9d6d1143dce06a0Ben Murdoch                }
26825a1523642bead2f7e7f929ba9d6d1143dce06a0Ben Murdoch                newOriginQuota = 0;
26925a1523642bead2f7e7f929ba9d6d1143dce06a0Ben Murdoch            }
27079e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu        } else {
27179e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu            // This is an origin we have seen before. It wants a quota
27279e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu            // increase.
27379e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu            newOriginQuota +=
27479e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu                Math.min(QUOTA_INCREASE_STEP, totalUnusedQuota);
27579e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu        }
27679e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu        quotaUpdater.updateQuota(newOriginQuota);
27779e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu
27879e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu        if(LOGV_ENABLED) {
27979e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu            Log.v(LOGTAG, "onExceededDatabaseQuota set new quota to "
28079e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu                    + newOriginQuota);
28179e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu        }
28279e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu    }
28379e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu
28479e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu    /**
28579e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu     * The Application Cache has exceeded its max size.
28679e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu     * @param spaceNeeded is the amount of disk space that would be needed
28779e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu     * in order for the last appcache operation to succeed.
28879e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu     * @param totalUsedQuota is the sum of all origins' quota.
28979e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu     * @param quotaUpdater A callback to inform the WebCore thread that a new
29079e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu     * app cache size is available. This callback must always be executed at
29179e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu     * some point to ensure that the sleeping WebCore thread is woken up.
29279e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu     */
29379e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu    public void onReachedMaxAppCacheSize(long spaceNeeded, long totalUsedQuota,
29479e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu            WebStorage.QuotaUpdater quotaUpdater) {
29579e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu        if(LOGV_ENABLED) {
29679e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu            Log.v(LOGTAG, "Received onReachedMaxAppCacheSize with spaceNeeded "
29779e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu                  + spaceNeeded + " bytes.");
29879e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu        }
29979e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu
30079e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu        long totalUnusedQuota = mGlobalLimit - totalUsedQuota - mAppCacheMaxSize;
30179e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu
30286943777260a399df282517241085d0c5e10ee88Andrei Popescu        if (totalUnusedQuota < spaceNeeded + APPCACHE_MAXSIZE_PADDING) {
30379e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu            // There definitely isn't any more space. Fire notifications
30486943777260a399df282517241085d0c5e10ee88Andrei Popescu            // if needed and exit.
30586943777260a399df282517241085d0c5e10ee88Andrei Popescu            if (totalUsedQuota > 0) {
30686943777260a399df282517241085d0c5e10ee88Andrei Popescu                // We only fire the notification if there are some other websites
30786943777260a399df282517241085d0c5e10ee88Andrei Popescu                // using some of the quota. This avoids the degenerate case where
30886943777260a399df282517241085d0c5e10ee88Andrei Popescu                // the first ever website to use Web storage tries to use more
30986943777260a399df282517241085d0c5e10ee88Andrei Popescu                // data than it is actually available. In such a case, showing
31086943777260a399df282517241085d0c5e10ee88Andrei Popescu                // the notification would not help at all since there is nothing
31186943777260a399df282517241085d0c5e10ee88Andrei Popescu                // the user can do.
31286943777260a399df282517241085d0c5e10ee88Andrei Popescu                scheduleOutOfSpaceNotification();
31386943777260a399df282517241085d0c5e10ee88Andrei Popescu            }
31479e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu            quotaUpdater.updateQuota(0);
31579e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu            if(LOGV_ENABLED) {
31679e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu                Log.v(LOGTAG, "onReachedMaxAppCacheSize: out of space.");
31779e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu            }
31879e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu            return;
31979e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu        }
32079e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu        // There is enough space to accommodate spaceNeeded bytes.
32186943777260a399df282517241085d0c5e10ee88Andrei Popescu        mAppCacheMaxSize += spaceNeeded + APPCACHE_MAXSIZE_PADDING;
32279e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu        quotaUpdater.updateQuota(mAppCacheMaxSize);
32379e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu
32479e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu        if(LOGV_ENABLED) {
32579e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu            Log.v(LOGTAG, "onReachedMaxAppCacheSize set new max size to "
32679e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu                    + mAppCacheMaxSize);
32779e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu        }
32879e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu    }
32979e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu
330eb9032c67077aa5aa6ddf928bf14a45534f24776Nicolas Roard    // Reset the notification time; we use this iff the user
331eb9032c67077aa5aa6ddf928bf14a45534f24776Nicolas Roard    // use clear all; we reset it to some time in the future instead
332eb9032c67077aa5aa6ddf928bf14a45534f24776Nicolas Roard    // of just setting it to -1, as the clear all method is asynchronous
333eb9032c67077aa5aa6ddf928bf14a45534f24776Nicolas Roard    static void resetLastOutOfSpaceNotificationTime() {
334eb9032c67077aa5aa6ddf928bf14a45534f24776Nicolas Roard        mLastOutOfSpaceNotificationTime = System.currentTimeMillis() -
335eb9032c67077aa5aa6ddf928bf14a45534f24776Nicolas Roard            NOTIFICATION_INTERVAL + RESET_NOTIFICATION_INTERVAL;
336eb9032c67077aa5aa6ddf928bf14a45534f24776Nicolas Roard    }
337eb9032c67077aa5aa6ddf928bf14a45534f24776Nicolas Roard
33879e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu    // Computes the global limit as a function of the size of the data
33979e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu    // partition and the amount of free space on that partition.
340907c5763bb700c705108948328c8892500e59185Andrei Popescu    private long getGlobalLimit() {
341907c5763bb700c705108948328c8892500e59185Andrei Popescu        long freeSpace = mDiskInfo.getFreeSpaceSizeBytes();
342907c5763bb700c705108948328c8892500e59185Andrei Popescu        long fileSystemSize = mDiskInfo.getTotalSizeBytes();
34379e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu        return calculateGlobalLimit(fileSystemSize, freeSpace);
34479e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu    }
34579e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu
34679e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu    /*package*/ static long calculateGlobalLimit(long fileSystemSizeBytes,
34779e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu            long freeSpaceBytes) {
34879e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu        if (fileSystemSizeBytes <= 0
34979e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu                || freeSpaceBytes <= 0
35079e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu                || freeSpaceBytes > fileSystemSizeBytes) {
35179e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu            return 0;
35279e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu        }
35379e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu
35479e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu        long fileSystemSizeRatio =
35579e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu            2 << ((int) Math.floor(Math.log10(
35679e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu                    fileSystemSizeBytes / (1024 * 1024))));
35779e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu        long maxSizeBytes = (long) Math.min(Math.floor(
35879e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu                fileSystemSizeBytes / fileSystemSizeRatio),
35979e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu                Math.floor(freeSpaceBytes / 2));
36079e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu        // Round maxSizeBytes up to a multiple of 1024KB (but only if
36179e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu        // maxSizeBytes > 1MB).
36279e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu        long maxSizeStepBytes = 1024 * 1024;
36379e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu        if (maxSizeBytes < maxSizeStepBytes) {
36479e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu            return 0;
36579e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu        }
36679e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu        long roundingExtra = maxSizeBytes % maxSizeStepBytes == 0 ? 0 : 1;
36779e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu        return (maxSizeStepBytes
36879e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu                * ((maxSizeBytes / maxSizeStepBytes) + roundingExtra));
36979e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu    }
37079e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu
37179e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu    // Schedules a system notification that takes the user to the WebSettings
37279e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu    // activity when clicked.
37379e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu    private void scheduleOutOfSpaceNotification() {
37486943777260a399df282517241085d0c5e10ee88Andrei Popescu        if(LOGV_ENABLED) {
37586943777260a399df282517241085d0c5e10ee88Andrei Popescu            Log.v(LOGTAG, "scheduleOutOfSpaceNotification called.");
37686943777260a399df282517241085d0c5e10ee88Andrei Popescu        }
37786943777260a399df282517241085d0c5e10ee88Andrei Popescu        if (mContext == null) {
37886943777260a399df282517241085d0c5e10ee88Andrei Popescu            // mContext can be null if we're running unit tests.
37986943777260a399df282517241085d0c5e10ee88Andrei Popescu            return;
38086943777260a399df282517241085d0c5e10ee88Andrei Popescu        }
381eb9032c67077aa5aa6ddf928bf14a45534f24776Nicolas Roard        if ((mLastOutOfSpaceNotificationTime == -1) ||
382eb9032c67077aa5aa6ddf928bf14a45534f24776Nicolas Roard            (System.currentTimeMillis() - mLastOutOfSpaceNotificationTime > NOTIFICATION_INTERVAL)) {
383eb9032c67077aa5aa6ddf928bf14a45534f24776Nicolas Roard            // setup the notification boilerplate.
384eb9032c67077aa5aa6ddf928bf14a45534f24776Nicolas Roard            int icon = android.R.drawable.stat_sys_warning;
385eb9032c67077aa5aa6ddf928bf14a45534f24776Nicolas Roard            CharSequence title = mContext.getString(
386eb9032c67077aa5aa6ddf928bf14a45534f24776Nicolas Roard                    R.string.webstorage_outofspace_notification_title);
387eb9032c67077aa5aa6ddf928bf14a45534f24776Nicolas Roard            CharSequence text = mContext.getString(
388eb9032c67077aa5aa6ddf928bf14a45534f24776Nicolas Roard                    R.string.webstorage_outofspace_notification_text);
389eb9032c67077aa5aa6ddf928bf14a45534f24776Nicolas Roard            long when = System.currentTimeMillis();
390eb9032c67077aa5aa6ddf928bf14a45534f24776Nicolas Roard            Intent intent = new Intent(mContext, WebsiteSettingsActivity.class);
391eb9032c67077aa5aa6ddf928bf14a45534f24776Nicolas Roard            PendingIntent contentIntent =
392eb9032c67077aa5aa6ddf928bf14a45534f24776Nicolas Roard                PendingIntent.getActivity(mContext, 0, intent, 0);
393eb9032c67077aa5aa6ddf928bf14a45534f24776Nicolas Roard            Notification notification = new Notification(icon, title, when);
394eb9032c67077aa5aa6ddf928bf14a45534f24776Nicolas Roard            notification.setLatestEventInfo(mContext, title, text, contentIntent);
395eb9032c67077aa5aa6ddf928bf14a45534f24776Nicolas Roard            notification.flags |= Notification.FLAG_AUTO_CANCEL;
396eb9032c67077aa5aa6ddf928bf14a45534f24776Nicolas Roard            // Fire away.
397eb9032c67077aa5aa6ddf928bf14a45534f24776Nicolas Roard            String ns = Context.NOTIFICATION_SERVICE;
398eb9032c67077aa5aa6ddf928bf14a45534f24776Nicolas Roard            NotificationManager mgr =
399eb9032c67077aa5aa6ddf928bf14a45534f24776Nicolas Roard                (NotificationManager) mContext.getSystemService(ns);
400eb9032c67077aa5aa6ddf928bf14a45534f24776Nicolas Roard            if (mgr != null) {
401eb9032c67077aa5aa6ddf928bf14a45534f24776Nicolas Roard                mLastOutOfSpaceNotificationTime = System.currentTimeMillis();
402eb9032c67077aa5aa6ddf928bf14a45534f24776Nicolas Roard                mgr.notify(OUT_OF_SPACE_ID, notification);
403eb9032c67077aa5aa6ddf928bf14a45534f24776Nicolas Roard            }
40486943777260a399df282517241085d0c5e10ee88Andrei Popescu        }
40579e82b7ba72a7278911edf0dd7b03c65c4ec0e9dAndrei Popescu    }
40625a1523642bead2f7e7f929ba9d6d1143dce06a0Ben Murdoch}
407