1 /* 2 * Copyright (C) 2010 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 18 package com.android.browser; 19 20 import android.content.ContentResolver; 21 import android.content.ContentUris; 22 import android.content.ContentValues; 23 import android.content.Context; 24 import android.database.Cursor; 25 import android.database.sqlite.SQLiteException; 26 import android.graphics.Bitmap; 27 import android.net.Uri; 28 import android.os.Handler; 29 import android.os.Message; 30 import android.provider.BrowserContract; 31 import android.provider.BrowserContract.History; 32 import android.util.Log; 33 34 import com.android.browser.provider.BrowserProvider2.Thumbnails; 35 36 import java.nio.ByteBuffer; 37 import java.util.concurrent.BlockingQueue; 38 import java.util.concurrent.LinkedBlockingQueue; 39 40 public class DataController { 41 private static final String LOGTAG = "DataController"; 42 // Message IDs 43 private static final int HISTORY_UPDATE_VISITED = 100; 44 private static final int HISTORY_UPDATE_TITLE = 101; 45 private static final int QUERY_URL_IS_BOOKMARK = 200; 46 private static final int TAB_LOAD_THUMBNAIL = 201; 47 private static final int TAB_SAVE_THUMBNAIL = 202; 48 private static final int TAB_DELETE_THUMBNAIL = 203; 49 private static DataController sInstance; 50 51 private Context mContext; 52 private DataControllerHandler mDataHandler; 53 private Handler mCbHandler; // To respond on the UI thread 54 private ByteBuffer mBuffer; // to capture thumbnails 55 56 /* package */ static interface OnQueryUrlIsBookmark { onQueryUrlIsBookmark(String url, boolean isBookmark)57 void onQueryUrlIsBookmark(String url, boolean isBookmark); 58 } 59 private static class CallbackContainer { 60 Object replyTo; 61 Object[] args; 62 } 63 64 private static class DCMessage { 65 int what; 66 Object obj; 67 Object replyTo; DCMessage(int w, Object o)68 DCMessage(int w, Object o) { 69 what = w; 70 obj = o; 71 } 72 } 73 getInstance(Context c)74 /* package */ static DataController getInstance(Context c) { 75 if (sInstance == null) { 76 sInstance = new DataController(c); 77 } 78 return sInstance; 79 } 80 DataController(Context c)81 private DataController(Context c) { 82 mContext = c.getApplicationContext(); 83 mDataHandler = new DataControllerHandler(); 84 mDataHandler.start(); 85 mCbHandler = new Handler() { 86 @Override 87 public void handleMessage(Message msg) { 88 CallbackContainer cc = (CallbackContainer) msg.obj; 89 switch (msg.what) { 90 case QUERY_URL_IS_BOOKMARK: { 91 OnQueryUrlIsBookmark cb = (OnQueryUrlIsBookmark) cc.replyTo; 92 String url = (String) cc.args[0]; 93 boolean isBookmark = (Boolean) cc.args[1]; 94 cb.onQueryUrlIsBookmark(url, isBookmark); 95 break; 96 } 97 } 98 } 99 }; 100 } 101 updateVisitedHistory(String url)102 public void updateVisitedHistory(String url) { 103 mDataHandler.sendMessage(HISTORY_UPDATE_VISITED, url); 104 } 105 updateHistoryTitle(String url, String title)106 public void updateHistoryTitle(String url, String title) { 107 mDataHandler.sendMessage(HISTORY_UPDATE_TITLE, new String[] { url, title }); 108 } 109 queryBookmarkStatus(String url, OnQueryUrlIsBookmark replyTo)110 public void queryBookmarkStatus(String url, OnQueryUrlIsBookmark replyTo) { 111 if (url == null || url.trim().length() == 0) { 112 // null or empty url is never a bookmark 113 replyTo.onQueryUrlIsBookmark(url, false); 114 return; 115 } 116 mDataHandler.sendMessage(QUERY_URL_IS_BOOKMARK, url.trim(), replyTo); 117 } 118 loadThumbnail(Tab tab)119 public void loadThumbnail(Tab tab) { 120 mDataHandler.sendMessage(TAB_LOAD_THUMBNAIL, tab); 121 } 122 deleteThumbnail(Tab tab)123 public void deleteThumbnail(Tab tab) { 124 mDataHandler.sendMessage(TAB_DELETE_THUMBNAIL, tab.getId()); 125 } 126 saveThumbnail(Tab tab)127 public void saveThumbnail(Tab tab) { 128 mDataHandler.sendMessage(TAB_SAVE_THUMBNAIL, tab); 129 } 130 131 // The standard Handler and Message classes don't allow the queue manipulation 132 // we want (such as peeking). So we use our own queue. 133 class DataControllerHandler extends Thread { 134 private BlockingQueue<DCMessage> mMessageQueue 135 = new LinkedBlockingQueue<DCMessage>(); 136 DataControllerHandler()137 public DataControllerHandler() { 138 super("DataControllerHandler"); 139 } 140 141 @Override run()142 public void run() { 143 setPriority(Thread.MIN_PRIORITY); 144 while (true) { 145 try { 146 handleMessage(mMessageQueue.take()); 147 } catch (InterruptedException ex) { 148 break; 149 } 150 } 151 } 152 sendMessage(int what, Object obj)153 void sendMessage(int what, Object obj) { 154 DCMessage m = new DCMessage(what, obj); 155 mMessageQueue.add(m); 156 } 157 sendMessage(int what, Object obj, Object replyTo)158 void sendMessage(int what, Object obj, Object replyTo) { 159 DCMessage m = new DCMessage(what, obj); 160 m.replyTo = replyTo; 161 mMessageQueue.add(m); 162 } 163 handleMessage(DCMessage msg)164 private void handleMessage(DCMessage msg) { 165 switch (msg.what) { 166 case HISTORY_UPDATE_VISITED: 167 doUpdateVisitedHistory((String) msg.obj); 168 break; 169 case HISTORY_UPDATE_TITLE: 170 String[] args = (String[]) msg.obj; 171 doUpdateHistoryTitle(args[0], args[1]); 172 break; 173 case QUERY_URL_IS_BOOKMARK: 174 // TODO: Look for identical messages in the queue and remove them 175 // TODO: Also, look for partial matches and merge them (such as 176 // multiple callbacks querying the same URL) 177 doQueryBookmarkStatus((String) msg.obj, msg.replyTo); 178 break; 179 case TAB_LOAD_THUMBNAIL: 180 doLoadThumbnail((Tab) msg.obj); 181 break; 182 case TAB_DELETE_THUMBNAIL: 183 ContentResolver cr = mContext.getContentResolver(); 184 try { 185 cr.delete(ContentUris.withAppendedId( 186 Thumbnails.CONTENT_URI, (Long)msg.obj), 187 null, null); 188 } catch (Throwable t) {} 189 break; 190 case TAB_SAVE_THUMBNAIL: 191 doSaveThumbnail((Tab)msg.obj); 192 break; 193 } 194 } 195 getCaptureBlob(Tab tab)196 private byte[] getCaptureBlob(Tab tab) { 197 synchronized (tab) { 198 Bitmap capture = tab.getScreenshot(); 199 if (capture == null) { 200 return null; 201 } 202 if (mBuffer == null || mBuffer.limit() < capture.getByteCount()) { 203 mBuffer = ByteBuffer.allocate(capture.getByteCount()); 204 } 205 capture.copyPixelsToBuffer(mBuffer); 206 mBuffer.rewind(); 207 return mBuffer.array(); 208 } 209 } 210 doSaveThumbnail(Tab tab)211 private void doSaveThumbnail(Tab tab) { 212 byte[] blob = getCaptureBlob(tab); 213 if (blob == null) { 214 return; 215 } 216 ContentResolver cr = mContext.getContentResolver(); 217 ContentValues values = new ContentValues(); 218 values.put(Thumbnails._ID, tab.getId()); 219 values.put(Thumbnails.THUMBNAIL, blob); 220 cr.insert(Thumbnails.CONTENT_URI, values); 221 } 222 doLoadThumbnail(Tab tab)223 private void doLoadThumbnail(Tab tab) { 224 ContentResolver cr = mContext.getContentResolver(); 225 Cursor c = null; 226 try { 227 Uri uri = ContentUris.withAppendedId(Thumbnails.CONTENT_URI, tab.getId()); 228 c = cr.query(uri, new String[] {Thumbnails._ID, 229 Thumbnails.THUMBNAIL}, null, null, null); 230 if (c.moveToFirst()) { 231 byte[] data = c.getBlob(1); 232 if (data != null && data.length > 0) { 233 tab.updateCaptureFromBlob(data); 234 } 235 } 236 } finally { 237 if (c != null) { 238 c.close(); 239 } 240 } 241 } 242 doUpdateVisitedHistory(String url)243 private void doUpdateVisitedHistory(String url) { 244 ContentResolver cr = mContext.getContentResolver(); 245 Cursor c = null; 246 try { 247 c = cr.query(History.CONTENT_URI, new String[] { History._ID, History.VISITS }, 248 History.URL + "=?", new String[] { url }, null); 249 if (c.moveToFirst()) { 250 ContentValues values = new ContentValues(); 251 values.put(History.VISITS, c.getInt(1) + 1); 252 values.put(History.DATE_LAST_VISITED, System.currentTimeMillis()); 253 cr.update(ContentUris.withAppendedId(History.CONTENT_URI, c.getLong(0)), 254 values, null, null); 255 } else { 256 android.provider.Browser.truncateHistory(cr); 257 ContentValues values = new ContentValues(); 258 values.put(History.URL, url); 259 values.put(History.VISITS, 1); 260 values.put(History.DATE_LAST_VISITED, System.currentTimeMillis()); 261 values.put(History.TITLE, url); 262 values.put(History.DATE_CREATED, 0); 263 values.put(History.USER_ENTERED, 0); 264 cr.insert(History.CONTENT_URI, values); 265 } 266 } finally { 267 if (c != null) c.close(); 268 } 269 } 270 doQueryBookmarkStatus(String url, Object replyTo)271 private void doQueryBookmarkStatus(String url, Object replyTo) { 272 // Check to see if the site is bookmarked 273 Cursor cursor = null; 274 boolean isBookmark = false; 275 try { 276 cursor = mContext.getContentResolver().query( 277 BookmarkUtils.getBookmarksUri(mContext), 278 new String[] { BrowserContract.Bookmarks.URL }, 279 BrowserContract.Bookmarks.URL + " == ?", 280 new String[] { url }, 281 null); 282 isBookmark = cursor.moveToFirst(); 283 } catch (SQLiteException e) { 284 Log.e(LOGTAG, "Error checking for bookmark: " + e); 285 } finally { 286 if (cursor != null) cursor.close(); 287 } 288 CallbackContainer cc = new CallbackContainer(); 289 cc.replyTo = replyTo; 290 cc.args = new Object[] { url, isBookmark }; 291 mCbHandler.obtainMessage(QUERY_URL_IS_BOOKMARK, cc).sendToTarget(); 292 } 293 doUpdateHistoryTitle(String url, String title)294 private void doUpdateHistoryTitle(String url, String title) { 295 ContentResolver cr = mContext.getContentResolver(); 296 ContentValues values = new ContentValues(); 297 values.put(History.TITLE, title); 298 cr.update(History.CONTENT_URI, values, History.URL + "=?", 299 new String[] { url }); 300 } 301 } 302 } 303