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
18package com.android.browser;
19
20import android.content.ContentResolver;
21import android.content.ContentUris;
22import android.content.ContentValues;
23import android.content.Context;
24import android.database.Cursor;
25import android.database.sqlite.SQLiteException;
26import android.graphics.Bitmap;
27import android.net.Uri;
28import android.os.Handler;
29import android.os.Message;
30import android.provider.BrowserContract;
31import android.provider.BrowserContract.History;
32import android.util.Log;
33
34import com.android.browser.provider.BrowserProvider2.Thumbnails;
35
36import java.nio.ByteBuffer;
37import java.util.concurrent.BlockingQueue;
38import java.util.concurrent.LinkedBlockingQueue;
39
40public 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 {
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;
68        DCMessage(int w, Object o) {
69            what = w;
70            obj = o;
71        }
72    }
73
74    /* package */ static DataController getInstance(Context c) {
75        if (sInstance == null) {
76            sInstance = new DataController(c);
77        }
78        return sInstance;
79    }
80
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
102    public void updateVisitedHistory(String url) {
103        mDataHandler.sendMessage(HISTORY_UPDATE_VISITED, url);
104    }
105
106    public void updateHistoryTitle(String url, String title) {
107        mDataHandler.sendMessage(HISTORY_UPDATE_TITLE, new String[] { url, title });
108    }
109
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
119    public void loadThumbnail(Tab tab) {
120        mDataHandler.sendMessage(TAB_LOAD_THUMBNAIL, tab);
121    }
122
123    public void deleteThumbnail(Tab tab) {
124        mDataHandler.sendMessage(TAB_DELETE_THUMBNAIL, tab.getId());
125    }
126
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
137        public DataControllerHandler() {
138            super("DataControllerHandler");
139        }
140
141        @Override
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
153        void sendMessage(int what, Object obj) {
154            DCMessage m = new DCMessage(what, obj);
155            mMessageQueue.add(m);
156        }
157
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
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
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
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
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
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
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
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