summaryrefslogtreecommitdiffstats
path: root/mobile/android/base/java/org/mozilla/gecko/GlobalPageMetadata.java
blob: d9d12962ca41809fa91cc70716f7b8bd90b7f5dc (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
/* 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;

import android.content.ContentProviderClient;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.VisibleForTesting;
import android.text.TextUtils;
import android.util.Log;

import org.json.JSONException;
import org.json.JSONObject;
import org.mozilla.gecko.db.BrowserContract;
import org.mozilla.gecko.db.BrowserDB;
import org.mozilla.gecko.util.BundleEventListener;
import org.mozilla.gecko.util.EventCallback;
import org.mozilla.gecko.util.ThreadUtils;

import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;

/**
 * Provides access to metadata information about websites.
 *
 * While storing, in case of timing issues preventing us from looking up History GUID by a given uri,
 * we queue up metadata and wait for GlobalHistory to let us know history record is now available.
 *
 * TODO Bug 1313515: selection of metadata for a given uri/history_GUID
 *
 * @author grisha
 */
/* package-local */ class GlobalPageMetadata implements BundleEventListener {
    private static final String LOG_TAG = "GeckoGlobalPageMetadata";

    private static final GlobalPageMetadata instance = new GlobalPageMetadata();

    private static final String KEY_HAS_IMAGE = "hasImage";
    private static final String KEY_METADATA_JSON = "metadataJSON";

    private static final int MAX_METADATA_QUEUE_SIZE = 15;

    private final Map<String, Bundle> queuedMetadata = Collections.synchronizedMap(new LimitedLinkedHashMap<String, Bundle>());

    public static GlobalPageMetadata getInstance() {
        return instance;
    }

    private static class LimitedLinkedHashMap<K, V> extends LinkedHashMap<K, V> {
        private static final long serialVersionUID = 6359725112736360244L;

        @Override
        protected boolean removeEldestEntry(Entry<K, V> eldest) {
            if (size() > MAX_METADATA_QUEUE_SIZE) {
                Log.w(LOG_TAG, "Page metadata queue is full. Dropping oldest metadata.");
                return true;
            }
            return false;
        }
    }

    private GlobalPageMetadata() {}

    public void init() {
        EventDispatcher
                .getInstance()
                .registerBackgroundThreadListener(this, GlobalHistory.EVENT_URI_AVAILABLE_IN_HISTORY);
    }

    public void add(BrowserDB db, ContentProviderClient contentProviderClient, String uri, boolean hasImage, @NonNull String metadataJSON) {
        ThreadUtils.assertOnBackgroundThread();

        // NB: Other than checking that JSON is valid and trimming it,
        // we do not process metadataJSON in any way, trusting our source.
        doAddOrQueue(db, contentProviderClient, uri, hasImage, metadataJSON);
    }

    @VisibleForTesting
    /*package-local */ void doAddOrQueue(BrowserDB db, ContentProviderClient contentProviderClient, String uri, boolean hasImage, @NonNull String metadataJSON) {
        final String preparedMetadataJSON;
        try {
            preparedMetadataJSON = prepareJSON(metadataJSON);
        } catch (JSONException e) {
            Log.e(LOG_TAG, "Couldn't process metadata JSON", e);
            return;
        }

        // Don't bother queuing this if deletions fails to find a corresponding history record.
        // If we can't delete metadata because it didn't exist yet, that's OK.
        if (preparedMetadataJSON.equals("{}")) {
            final int deleted = db.deletePageMetadata(contentProviderClient, uri);
            // We could delete none if history record for uri isn't present.
            // We must delete one if history record for uri is present.
            if (deleted != 0 && deleted != 1) {
                throw new IllegalStateException("Deleted unexpected number of page metadata records: " + deleted);
            }
            return;
        }

        // If we could insert page metadata, we're done.
        if (db.insertPageMetadata(contentProviderClient, uri, hasImage, preparedMetadataJSON)) {
            return;
        }

        // Otherwise, we need to queue it for future insertion when history record is available.
        Bundle bundledMetadata = new Bundle();
        bundledMetadata.putBoolean(KEY_HAS_IMAGE, hasImage);
        bundledMetadata.putString(KEY_METADATA_JSON, preparedMetadataJSON);
        queuedMetadata.put(uri, bundledMetadata);
    }

    @VisibleForTesting
    /* package-local */ int getMetadataQueueSize() {
        return queuedMetadata.size();
    }

    @Override
    public void handleMessage(String event, Bundle message, EventCallback callback) {
        ThreadUtils.assertOnBackgroundThread();

        if (!GlobalHistory.EVENT_URI_AVAILABLE_IN_HISTORY.equals(event)) {
            return;
        }

        final String uri = message.getString(GlobalHistory.EVENT_PARAM_URI);
        if (TextUtils.isEmpty(uri)) {
            return;
        }

        final Bundle bundledMetadata;
        synchronized (queuedMetadata) {
            if (!queuedMetadata.containsKey(uri)) {
                return;
            }

            bundledMetadata = queuedMetadata.get(uri);
            queuedMetadata.remove(uri);
        }

        insertMetadataBundleForUri(uri, bundledMetadata);
    }

    private void insertMetadataBundleForUri(String uri, Bundle bundledMetadata) {
        final boolean hasImage = bundledMetadata.getBoolean(KEY_HAS_IMAGE);
        final String metadataJSON = bundledMetadata.getString(KEY_METADATA_JSON);

        // Acquire CPC, must be released in this function.
        final ContentProviderClient contentProviderClient = GeckoAppShell.getApplicationContext()
                .getContentResolver()
                .acquireContentProviderClient(BrowserContract.PageMetadata.CONTENT_URI);

        // Pre-conditions...
        if (contentProviderClient == null) {
            Log.e(LOG_TAG, "Couldn't acquire content provider client");
            return;
        }

        if (TextUtils.isEmpty(metadataJSON)) {
            Log.e(LOG_TAG, "Metadata bundle contained empty metadata json");
            return;
        }

        // Insert!
        try {
            add(
                    BrowserDB.from(GeckoThread.getActiveProfile()),
                    contentProviderClient,
                    uri, hasImage, metadataJSON
            );
        } finally {
            contentProviderClient.release();
        }
    }

    private String prepareJSON(String json) throws JSONException {
        return (new JSONObject(json)).toString();
    }
}