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

import java.lang.ref.SoftReference;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.Queue;
import java.util.Set;

import org.mozilla.gecko.db.BrowserDB;
import org.mozilla.gecko.reader.ReaderModeUtils;
import org.mozilla.gecko.util.ThreadUtils;

import android.content.ContentResolver;
import android.content.Context;
import android.database.Cursor;
import android.os.Bundle;
import android.os.Handler;
import android.os.SystemClock;
import android.util.Log;

class GlobalHistory {
    private static final String LOGTAG = "GeckoGlobalHistory";

    public static final String EVENT_URI_AVAILABLE_IN_HISTORY = "URI_INSERTED_TO_HISTORY";
    public static final String EVENT_PARAM_URI = "uri";

    private static final String TELEMETRY_HISTOGRAM_ADD = "FENNEC_GLOBALHISTORY_ADD_MS";
    private static final String TELEMETRY_HISTOGRAM_UPDATE = "FENNEC_GLOBALHISTORY_UPDATE_MS";
    private static final String TELEMETRY_HISTOGRAM_BUILD_VISITED_LINK = "FENNEC_GLOBALHISTORY_VISITED_BUILD_MS";

    private static final GlobalHistory sInstance = new GlobalHistory();

    static GlobalHistory getInstance() {
        return sInstance;
    }

    // this is the delay between receiving a URI check request and processing it.
    // this allows batching together multiple requests and processing them together,
    // which is more efficient.
    private static final long BATCHING_DELAY_MS = 100;

    private final Handler mHandler;                     // a background thread on which we can process requests

    //  Note: These fields are accessed through the NotificationRunnable inner class.
    final Queue<String> mPendingUris;           // URIs that need to be checked
    SoftReference<Set<String>> mVisitedCache;   // cache of the visited URI list
    boolean mProcessing; // = false             // whether or not the runnable is queued/working

    private class NotifierRunnable implements Runnable {
        private final ContentResolver mContentResolver;
        private final BrowserDB mDB;

        public NotifierRunnable(final Context context) {
            mContentResolver = context.getContentResolver();
            mDB = BrowserDB.from(context);
        }

        @Override
        public void run() {
            Set<String> visitedSet = mVisitedCache.get();
            if (visitedSet == null) {
                // The cache was wiped. Repopulate it.
                Log.w(LOGTAG, "Rebuilding visited link set...");
                final long start = SystemClock.uptimeMillis();
                final Cursor c = mDB.getAllVisitedHistory(mContentResolver);
                if (c == null) {
                    return;
                }

                try {
                    visitedSet = new HashSet<String>();
                    if (c.moveToFirst()) {
                        do {
                            visitedSet.add(c.getString(0));
                        } while (c.moveToNext());
                    }
                    mVisitedCache = new SoftReference<Set<String>>(visitedSet);
                    final long end = SystemClock.uptimeMillis();
                    final long took = end - start;
                    Telemetry.addToHistogram(TELEMETRY_HISTOGRAM_BUILD_VISITED_LINK, (int) Math.min(took, Integer.MAX_VALUE));
                } finally {
                    c.close();
                }
            }

            // This runs on the same handler thread as the checkUriVisited code,
            // so no synchronization is needed.
            while (true) {
                final String uri = mPendingUris.poll();
                if (uri == null) {
                    break;
                }

                if (visitedSet.contains(uri)) {
                    GeckoAppShell.notifyUriVisited(uri);
                }
            }

            mProcessing = false;
        }
    };

    private GlobalHistory() {
        mHandler = ThreadUtils.getBackgroundHandler();
        mPendingUris = new LinkedList<String>();
        mVisitedCache = new SoftReference<Set<String>>(null);
    }

    public void addToGeckoOnly(String uri) {
        Set<String> visitedSet = mVisitedCache.get();
        if (visitedSet != null) {
            visitedSet.add(uri);
        }
        GeckoAppShell.notifyUriVisited(uri);
    }

    public void add(final Context context, final BrowserDB db, String uri) {
        ThreadUtils.assertOnBackgroundThread();
        final long start = SystemClock.uptimeMillis();

        // stripAboutReaderUrl only removes about:reader if present, in all other cases the original string is returned
        final String uriToStore = ReaderModeUtils.stripAboutReaderUrl(uri);

        db.updateVisitedHistory(context.getContentResolver(), uriToStore);

        final long end = SystemClock.uptimeMillis();
        final long took = end - start;
        Telemetry.addToHistogram(TELEMETRY_HISTOGRAM_ADD, (int) Math.min(took, Integer.MAX_VALUE));
        addToGeckoOnly(uriToStore);
        dispatchUriAvailableMessage(uri);
    }

    @SuppressWarnings("static-method")
    public void update(final ContentResolver cr, final BrowserDB db, String uri, String title) {
        ThreadUtils.assertOnBackgroundThread();
        final long start = SystemClock.uptimeMillis();

        final String uriToStore = ReaderModeUtils.stripAboutReaderUrl(uri);

        db.updateHistoryTitle(cr, uriToStore, title);

        final long end = SystemClock.uptimeMillis();
        final long took = end - start;
        Telemetry.addToHistogram(TELEMETRY_HISTOGRAM_UPDATE, (int) Math.min(took, Integer.MAX_VALUE));
    }

    public void checkUriVisited(final String uri) {
        final String storedURI = ReaderModeUtils.stripAboutReaderUrl(uri);

        final NotifierRunnable runnable = new NotifierRunnable(GeckoAppShell.getContext());
        mHandler.post(new Runnable() {
            @Override
            public void run() {
                // this runs on the same handler thread as the processing loop,
                // so no synchronization needed
                mPendingUris.add(storedURI);
                if (mProcessing) {
                    // there's already a runnable queued up or working away, so
                    // no need to post another
                    return;
                }
                mProcessing = true;
                mHandler.postDelayed(runnable, BATCHING_DELAY_MS);
            }
        });
    }

    private void dispatchUriAvailableMessage(String uri) {
        final Bundle message = new Bundle();
        message.putString(EVENT_PARAM_URI, uri);
        EventDispatcher.getInstance().dispatch(EVENT_URI_AVAILABLE_IN_HISTORY, message);
    }
}