summaryrefslogtreecommitdiffstats
path: root/mobile/android/base/java/org/mozilla/gecko/db/LocalURLMetadata.java
blob: 7f2c4a736717fdd7c42150a2042f205ad4e5b09f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- */
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 */
package org.mozilla.gecko.db;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.json.JSONException;
import org.json.JSONObject;
import org.mozilla.gecko.GeckoAppShell;
import org.mozilla.gecko.icons.decoders.LoadFaviconResult;
import org.mozilla.gecko.util.ThreadUtils;

import android.content.ContentResolver;
import android.content.ContentValues;
import android.database.Cursor;
import android.net.Uri;
import android.util.Log;
import android.util.LruCache;

// Holds metadata info about URLs. Supports some helper functions for getting back a HashMap of key value data.
public class LocalURLMetadata implements URLMetadata {
    private static final String LOGTAG = "GeckoURLMetadata";
    private final Uri uriWithProfile;

    public LocalURLMetadata(String mProfile) {
        uriWithProfile = DBUtils.appendProfileWithDefault(mProfile, URLMetadataTable.CONTENT_URI);
    }

    // A list of columns in the table. It's used to simplify some loops for reading/writing data.
    private static final Set<String> COLUMNS;
    static {
        final HashSet<String> tempModel = new HashSet<>(4);
        tempModel.add(URLMetadataTable.URL_COLUMN);
        tempModel.add(URLMetadataTable.TILE_IMAGE_URL_COLUMN);
        tempModel.add(URLMetadataTable.TILE_COLOR_COLUMN);
        tempModel.add(URLMetadataTable.TOUCH_ICON_COLUMN);
        COLUMNS = Collections.unmodifiableSet(tempModel);
    }

    // Store a cache of recent results. This number is chosen to match the max number of tiles on about:home
    private static final int CACHE_SIZE = 9;
    // Note: Members of this cache are unmodifiable.
    private final LruCache<String, Map<String, Object>> cache = new LruCache<String, Map<String, Object>>(CACHE_SIZE);

    /**
     * Converts a JSON object into a unmodifiable Map of known metadata properties.
     * Will throw away any properties that aren't stored in the database.
     *
     * Incoming data can include a list like: {touchIconList:{56:"http://x.com/56.png", 76:"http://x.com/76.png"}}.
     * This will then be filtered to find the most appropriate touchIcon, i.e. the closest icon size that is larger
     * than (or equal to) the preferred homescreen launcher icon size, which is then stored in the "touchIcon" property.
     */
    @Override
    public Map<String, Object> fromJSON(JSONObject obj) {
        Map<String, Object> data = new HashMap<String, Object>();

        for (String key : COLUMNS) {
            if (obj.has(key)) {
                data.put(key, obj.optString(key));
            }
        }


        try {
            JSONObject icons;
            if (obj.has("touchIconList") &&
                    (icons = obj.getJSONObject("touchIconList")).length() > 0) {
                int preferredSize = GeckoAppShell.getPreferredIconSize();

                Iterator<String> keys = icons.keys();

                ArrayList<Integer> sizes = new ArrayList<Integer>(icons.length());
                while (keys.hasNext()) {
                    sizes.add(new Integer(keys.next()));
                }

                final int bestSize = LoadFaviconResult.selectBestSizeFromList(sizes, preferredSize);
                final String iconURL = icons.getString(Integer.toString(bestSize));

                data.put(URLMetadataTable.TOUCH_ICON_COLUMN, iconURL);
            }
        } catch (JSONException e) {
            Log.w(LOGTAG, "Exception processing touchIconList for LocalURLMetadata; ignoring.", e);
        }

        return Collections.unmodifiableMap(data);
    }

    /**
     * Converts a Cursor into a unmodifiable Map of known metadata properties.
     * Will throw away any properties that aren't stored in the database.
     * Will also not iterate through multiple rows in the cursor.
     */
    private Map<String, Object> fromCursor(Cursor c) {
        Map<String, Object> data = new HashMap<String, Object>();

        String[] columns = c.getColumnNames();
        for (String column : columns) {
            if (COLUMNS.contains(column)) {
                try {
                    data.put(column, c.getString(c.getColumnIndexOrThrow(column)));
                } catch (Exception ex) {
                    Log.i(LOGTAG, "Error getting data for " + column, ex);
                }
            }
        }

        return Collections.unmodifiableMap(data);
    }

    /**
     * Returns an unmodifiable Map of url->Metadata (i.e. A second HashMap) for a list of urls.
     * Must not be called from UI or Gecko threads.
     */
    @Override
    public Map<String, Map<String, Object>> getForURLs(final ContentResolver cr,
                                                       final Collection<String> urls,
                                                       final List<String> requestedColumns) {
        ThreadUtils.assertNotOnUiThread();
        ThreadUtils.assertNotOnGeckoThread();

        final Map<String, Map<String, Object>> data = new HashMap<String, Map<String, Object>>();

        // Nothing to query for
        if (urls.isEmpty() || requestedColumns.isEmpty()) {
            Log.e(LOGTAG, "Queried metadata for nothing");
            return data;
        }

        // Search the cache for any of these urls
        List<String> urlsToQuery = new ArrayList<String>();
        for (String url : urls) {
            final Map<String, Object> hit = cache.get(url);
            if (hit != null) {
                // Cache hit: we've found the URL in the cache, however we may not have cached the desired columns
                // for that URL. Hence we need to check whether our cache hit contains those columns, and directly
                // retrieve the desired data if not. (E.g. the top sites panel retrieves the tile, and tilecolor. If
                // we later try to retrieve the touchIcon for a top-site the cache hit will only point to
                // tile+tilecolor, and not the required touchIcon. In this case we don't want to use the cache.)
                boolean useCache = true;
                for (String c: requestedColumns) {
                    if (!hit.containsKey(c)) {
                        useCache = false;
                    }
                }
                if (useCache) {
                    data.put(url, hit);
                } else {
                    urlsToQuery.add(url);
                }
            } else {
                urlsToQuery.add(url);
            }
        }

        // If everything was in the cache, we're done!
        if (urlsToQuery.size() == 0) {
            return Collections.unmodifiableMap(data);
        }

        final String selection = DBUtils.computeSQLInClause(urlsToQuery.size(), URLMetadataTable.URL_COLUMN);
        List<String> columns = requestedColumns;
        // We need the url to build our final HashMap, so we force it to be included in the query.
        if (!columns.contains(URLMetadataTable.URL_COLUMN)) {
            // The requestedColumns may be immutable (e.g. if the caller used Collections.singletonList), hence
            // we have to create a copy.
            columns = new ArrayList<String>(columns);
            columns.add(URLMetadataTable.URL_COLUMN);
        }

        final Cursor cursor = cr.query(uriWithProfile,
                                       columns.toArray(new String[columns.size()]), // columns,
                                       selection, // selection
                                       urlsToQuery.toArray(new String[urlsToQuery.size()]), // selectionargs
                                       null);
        try {
            if (!cursor.moveToFirst()) {
                return Collections.unmodifiableMap(data);
            }

            do {
                final Map<String, Object> metadata = fromCursor(cursor);
                final String url = cursor.getString(cursor.getColumnIndexOrThrow(URLMetadataTable.URL_COLUMN));

                data.put(url, metadata);
                cache.put(url, metadata);
            } while (cursor.moveToNext());

        } finally {
            cursor.close();
        }

        return Collections.unmodifiableMap(data);
    }

    /**
     * Saves a HashMap of metadata into the database. Will iterate through columns
     * in the Database and only save rows with matching keys in the HashMap.
     * Must not be called from UI or Gecko threads.
     */
    @Override
    public void save(final ContentResolver cr, final Map<String, Object> data) {
        ThreadUtils.assertNotOnUiThread();
        ThreadUtils.assertNotOnGeckoThread();

        try {
            ContentValues values = new ContentValues();

            for (String key : COLUMNS) {
                if (data.containsKey(key)) {
                    values.put(key, (String) data.get(key));
                }
            }

            if (values.size() == 0) {
                return;
            }

            Uri uri = uriWithProfile.buildUpon()
                                 .appendQueryParameter(BrowserContract.PARAM_INSERT_IF_NEEDED, "true")
                                 .build();
            cr.update(uri, values, URLMetadataTable.URL_COLUMN + "=?", new String[] {
                (String) data.get(URLMetadataTable.URL_COLUMN)
            });
        } catch (Exception ex) {
            Log.e(LOGTAG, "error saving", ex);
        }
    }
}