зеркало из https://github.com/mozilla/gecko-dev.git
326 строки
13 KiB
Java
326 строки
13 KiB
Java
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
|
|
* ***** BEGIN LICENSE BLOCK *****
|
|
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
|
|
*
|
|
* The contents of this file are subject to the Mozilla Public License Version
|
|
* 1.1 (the "License"); you may not use this file except in compliance with
|
|
* the License. You may obtain a copy of the License at
|
|
* http://www.mozilla.org/MPL/
|
|
*
|
|
* Software distributed under the License is distributed on an "AS IS" basis,
|
|
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
|
|
* for the specific language governing rights and limitations under the
|
|
* License.
|
|
*
|
|
* The Original Code is Mozilla Android code.
|
|
*
|
|
* The Initial Developer of the Original Code is Mozilla Foundation.
|
|
* Portions created by the Initial Developer are Copyright (C) 2011-2012
|
|
* the Initial Developer. All Rights Reserved.
|
|
*
|
|
* Contributor(s):
|
|
* Gian-Carlo Pascutto <gpascutto@mozilla.com>
|
|
*
|
|
* Alternatively, the contents of this file may be used under the terms of
|
|
* either the GNU General Public License Version 2 or later (the "GPL"), or
|
|
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
|
|
* in which case the provisions of the GPL or the LGPL are applicable instead
|
|
* of those above. If you wish to allow use of your version of this file only
|
|
* under the terms of either the GPL or the LGPL, and not to allow others to
|
|
* use your version of this file under the terms of the MPL, indicate your
|
|
* decision by deleting the provisions above and replace them with the notice
|
|
* and other provisions required by the GPL or the LGPL. If you do not delete
|
|
* the provisions above, a recipient may use your version of this file under
|
|
* the terms of any one of the MPL, the GPL or the LGPL.
|
|
*
|
|
* ***** END LICENSE BLOCK ***** */
|
|
|
|
package org.mozilla.gecko;
|
|
|
|
import org.mozilla.gecko.db.BrowserDB;
|
|
import org.mozilla.gecko.sqlite.ByteBufferInputStream;
|
|
import org.mozilla.gecko.sqlite.SQLiteBridge;
|
|
import org.mozilla.gecko.sqlite.SQLiteBridgeException;
|
|
|
|
import android.content.ContentResolver;
|
|
import android.database.Cursor;
|
|
import android.database.SQLException;
|
|
import android.graphics.drawable.BitmapDrawable;
|
|
import android.graphics.drawable.Drawable;
|
|
import android.os.AsyncTask;
|
|
import android.provider.Browser;
|
|
import android.util.Log;
|
|
|
|
import java.io.IOException;
|
|
import java.io.File;
|
|
import java.nio.ByteBuffer;
|
|
import java.util.Arrays;
|
|
import java.util.ArrayList;
|
|
import java.util.Collections;
|
|
import java.util.Map;
|
|
import java.util.HashMap;
|
|
import java.util.Date;
|
|
import java.util.List;
|
|
import java.util.Iterator;
|
|
|
|
|
|
public class ProfileMigrator {
|
|
private static final String LOGTAG = "ProfMigr";
|
|
private File mProfileDir;
|
|
private ContentResolver mCr;
|
|
|
|
/*
|
|
These queries are derived from the low-level Places schema
|
|
https://developer.mozilla.org/en/The_Places_database
|
|
*/
|
|
private final String bookmarkQuery = "SELECT places.url AS a_url, "
|
|
+ "places.title AS a_title FROM "
|
|
+ "(moz_places as places JOIN moz_bookmarks as bookmarks ON "
|
|
+ "places.id = bookmarks.fk) WHERE places.hidden <> 1 "
|
|
+ "ORDER BY bookmarks.dateAdded";
|
|
// Result column of relevant data
|
|
private final String bookmarkUrl = "a_url";
|
|
private final String bookmarkTitle = "a_title";
|
|
|
|
/*
|
|
The sort criterion here corresponds to the one used for the
|
|
Awesomebar results. It's an simplification of Frecency.
|
|
We must divide date by 1000 due to the micro (Places)
|
|
vs milli (Android) distiction.
|
|
*/
|
|
private final String historyQuery =
|
|
"SELECT places.url AS a_url, places.title AS a_title,"
|
|
+ "MAX(history.visit_date) AS a_date, COUNT(*) AS a_visits, "
|
|
// see BrowserDB.filterAllSites for this formula
|
|
+ "MAX(1, (((MAX(history.visit_date)/1000) - ?) / 86400000 + 120)) AS a_recent, "
|
|
+ "favicon.data AS a_favicon_data, favicon.mime_type AS a_favicon_mime "
|
|
+ "FROM (moz_historyvisits AS history JOIN moz_places AS places "
|
|
+ "ON places.id = history.place_id "
|
|
// Add favicon data if a favicon is present for this URL.
|
|
+ "LEFT OUTER JOIN moz_favicons AS favicon "
|
|
+ "ON places.favicon_id = favicon.id) "
|
|
+ "WHERE places.hidden <> 1 "
|
|
+ "GROUP BY a_url ORDER BY a_visits * a_recent DESC LIMIT ?";
|
|
private final String historyUrl = "a_url";
|
|
private final String historyTitle = "a_title";
|
|
private final String historyDate = "a_date";
|
|
private final String historyVisits = "a_visits";
|
|
private final String faviconData = "a_favicon_data";
|
|
private final String faviconMime = "a_favicon_mime";
|
|
|
|
public ProfileMigrator(ContentResolver cr, File profileDir) {
|
|
mProfileDir = profileDir;
|
|
mCr = cr;
|
|
}
|
|
|
|
public void launch() {
|
|
new PlacesTask().run();
|
|
}
|
|
|
|
private class PlacesTask implements Runnable {
|
|
// Get a list of the last times an URL was accessed
|
|
protected Map<String, Long> gatherBrowserDBHistory() {
|
|
Map<String, Long> history = new HashMap<String, Long>();
|
|
|
|
Cursor cursor =
|
|
BrowserDB.getRecentHistory(mCr, BrowserDB.getMaxHistoryCount());
|
|
final int urlCol =
|
|
cursor.getColumnIndexOrThrow(BrowserDB.URLColumns.URL);
|
|
final int dateCol =
|
|
cursor.getColumnIndexOrThrow(BrowserDB.URLColumns.DATE_LAST_VISITED);
|
|
|
|
cursor.moveToFirst();
|
|
while (!cursor.isAfterLast()) {
|
|
String url = cursor.getString(urlCol);
|
|
Long date = cursor.getLong(dateCol);
|
|
// getRecentHistory returns newest-to-oldest, which means
|
|
// we remember the most recent access
|
|
if (!history.containsKey(url)) {
|
|
history.put(url, date);
|
|
}
|
|
cursor.moveToNext();
|
|
}
|
|
cursor.close();
|
|
|
|
return history;
|
|
}
|
|
|
|
protected void addHistory(Map<String, Long> browserDBHistory,
|
|
String url, String title, long date, int visits) {
|
|
boolean allowUpdate = false;
|
|
|
|
if (!browserDBHistory.containsKey(url)) {
|
|
// BrowserDB doesn't know the URL, allow it to be
|
|
// inserted with places date.
|
|
allowUpdate = true;
|
|
} else {
|
|
long androidDate = browserDBHistory.get(url);
|
|
if (androidDate < date) {
|
|
// Places URL hit is newer than BrowserDB,
|
|
// allow it to be updated with places date.
|
|
allowUpdate = true;
|
|
}
|
|
}
|
|
|
|
if (allowUpdate) {
|
|
BrowserDB.updateVisitedHistory(mCr, url);
|
|
// The above records one visit. Subtract that one visit here.
|
|
BrowserDB.updateHistoryEntry(mCr, url, title, date, visits - 1);
|
|
}
|
|
}
|
|
|
|
protected void migrateHistory(SQLiteBridge db) {
|
|
Map<String, Long> browserDBHistory = gatherBrowserDBHistory();
|
|
final ArrayList<String> placesHistory = new ArrayList<String>();
|
|
|
|
try {
|
|
final String[] queryParams = new String[] {
|
|
/* current time */
|
|
Long.toString(System.currentTimeMillis()),
|
|
/*
|
|
History entries to return. No point
|
|
in retrieving more than we can store.
|
|
*/
|
|
Integer.toString(BrowserDB.getMaxHistoryCount())
|
|
};
|
|
ArrayList<Object[]> queryResult =
|
|
db.query(historyQuery, queryParams);
|
|
final int urlCol = db.getColumnIndex(historyUrl);
|
|
final int titleCol = db.getColumnIndex(historyTitle);
|
|
final int dateCol = db.getColumnIndex(historyDate);
|
|
final int visitsCol = db.getColumnIndex(historyVisits);
|
|
final int faviconMimeCol = db.getColumnIndex(faviconMime);
|
|
final int faviconDataCol = db.getColumnIndex(faviconData);
|
|
|
|
for (Object[] resultRow: queryResult) {
|
|
String url = (String)resultRow[urlCol];
|
|
String title = (String)resultRow[titleCol];
|
|
long date = Long.parseLong((String)(resultRow[dateCol])) / (long)1000;
|
|
int visits = Integer.parseInt((String)(resultRow[visitsCol]));
|
|
addHistory(browserDBHistory, url, title, date, visits);
|
|
placesHistory.add(url);
|
|
|
|
String mime = (String)resultRow[faviconMimeCol];
|
|
if (mime != null) {
|
|
// Some GIFs can cause us to lock up completely
|
|
// without exceptions or anything. Not cool.
|
|
if (mime.compareTo("image/gif") != 0) {
|
|
ByteBuffer dataBuff =
|
|
(ByteBuffer)resultRow[faviconDataCol];
|
|
addFavicon(url, mime, dataBuff);
|
|
}
|
|
}
|
|
}
|
|
} catch (SQLiteBridgeException e) {
|
|
Log.i(LOGTAG, "Failed to get bookmarks: " + e.getMessage());
|
|
return;
|
|
}
|
|
// GlobalHistory access communicates with Gecko
|
|
// and must run on its thread
|
|
GeckoAppShell.getHandler().post(new Runnable() {
|
|
public void run() {
|
|
for (String url : placesHistory) {
|
|
GlobalHistory.getInstance().addToGeckoOnly(url);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
protected void addBookmark(String url, String title) {
|
|
if (!BrowserDB.isBookmark(mCr, url)) {
|
|
if (title == null) {
|
|
title = url;
|
|
}
|
|
BrowserDB.addBookmark(mCr, title, url);
|
|
}
|
|
}
|
|
|
|
protected void migrateBookmarks(SQLiteBridge db) {
|
|
try {
|
|
ArrayList<Object[]> queryResult = db.query(bookmarkQuery);
|
|
final int urlCol = db.getColumnIndex(bookmarkUrl);
|
|
final int titleCol = db.getColumnIndex(bookmarkTitle);
|
|
|
|
for (Object[] resultRow: queryResult) {
|
|
String url = (String)resultRow[urlCol];
|
|
String title = (String)resultRow[titleCol];
|
|
addBookmark(url, title);
|
|
}
|
|
} catch (SQLiteBridgeException e) {
|
|
Log.i(LOGTAG, "Failed to get bookmarks: " + e.getMessage());
|
|
return;
|
|
}
|
|
}
|
|
|
|
protected void addFavicon(String url, String mime, ByteBuffer data) {
|
|
ByteBufferInputStream byteStream = new ByteBufferInputStream(data);
|
|
BitmapDrawable image = (BitmapDrawable) Drawable.createFromStream(byteStream, "src");
|
|
if (image != null) {
|
|
try {
|
|
BrowserDB.updateFaviconForUrl(mCr, url, image);
|
|
} catch (SQLException e) {
|
|
Log.i(LOGTAG, "Migrating favicon failed: " + mime + " URL: " + url
|
|
+ " error:" + e.getMessage());
|
|
}
|
|
}
|
|
}
|
|
|
|
protected void migratePlaces(File aFile) {
|
|
String dbPath = aFile.getPath() + "/places.sqlite";
|
|
String dbPathWal = aFile.getPath() + "/places.sqlite-wal";
|
|
String dbPathShm = aFile.getPath() + "/places.sqlite-shm";
|
|
Log.i(LOGTAG, "Opening path: " + dbPath);
|
|
|
|
File dbFile = new File(dbPath);
|
|
if (!dbFile.exists()) {
|
|
Log.i(LOGTAG, "No database");
|
|
return;
|
|
}
|
|
File dbFileWal = new File(dbPathWal);
|
|
File dbFileShm = new File(dbPathShm);
|
|
|
|
SQLiteBridge db = null;
|
|
try {
|
|
db = new SQLiteBridge(dbPath);
|
|
migrateBookmarks(db);
|
|
migrateHistory(db);
|
|
db.close();
|
|
|
|
// Clean up
|
|
dbFile.delete();
|
|
dbFileWal.delete();
|
|
dbFileShm.delete();
|
|
|
|
Log.i(LOGTAG, "Profile migration finished");
|
|
} catch (SQLiteBridgeException e) {
|
|
if (db != null) {
|
|
db.close();
|
|
}
|
|
Log.i(LOGTAG, "Error on places database:" + e.getMessage());
|
|
return;
|
|
}
|
|
}
|
|
|
|
protected void cleanupXULLibCache() {
|
|
File cacheFile = GeckoAppShell.getCacheDir();
|
|
File[] files = cacheFile.listFiles();
|
|
if (files != null) {
|
|
Iterator cacheFiles = Arrays.asList(files).iterator();
|
|
while (cacheFiles.hasNext()) {
|
|
File libFile = (File)cacheFiles.next();
|
|
if (libFile.getName().endsWith(".so")) {
|
|
libFile.delete();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void run() {
|
|
migratePlaces(mProfileDir);
|
|
// XXX: Land dependent bugs first
|
|
// cleanupXULLibCache();
|
|
}
|
|
}
|
|
}
|