1
2/*
3 * Copyright (C) 2011 The Android Open Source Project
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 *      http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17package com.android.browser.homepages;
18
19import android.content.Context;
20import android.content.UriMatcher;
21import android.content.res.Resources;
22import android.database.Cursor;
23import android.database.MergeCursor;
24import android.net.Uri;
25import android.provider.BrowserContract.Bookmarks;
26import android.provider.BrowserContract.History;
27import android.text.TextUtils;
28import android.util.Base64;
29import android.util.Log;
30
31import com.android.browser.R;
32import com.android.browser.homepages.Template.ListEntityIterator;
33
34import java.io.File;
35import java.io.IOException;
36import java.io.InputStream;
37import java.io.OutputStream;
38import java.text.DateFormat;
39import java.text.DecimalFormat;
40import java.util.Arrays;
41import java.util.Comparator;
42import java.util.regex.Matcher;
43import java.util.regex.Pattern;
44
45public class RequestHandler extends Thread {
46
47    private static final String TAG = "RequestHandler";
48    private static final int INDEX = 1;
49    private static final int RESOURCE = 2;
50    private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
51
52    Uri mUri;
53    Context mContext;
54    OutputStream mOutput;
55
56    static {
57        sUriMatcher.addURI(HomeProvider.AUTHORITY, "/", INDEX);
58        sUriMatcher.addURI(HomeProvider.AUTHORITY, "res/*/*", RESOURCE);
59    }
60
61    public RequestHandler(Context context, Uri uri, OutputStream out) {
62        mUri = uri;
63        mContext = context.getApplicationContext();
64        mOutput = out;
65    }
66
67    @Override
68    public void run() {
69        super.run();
70        try {
71            doHandleRequest();
72        } catch (Exception e) {
73            Log.e(TAG, "Failed to handle request: " + mUri, e);
74        } finally {
75            cleanup();
76        }
77    }
78
79    void doHandleRequest() throws IOException {
80        if ("file".equals(mUri.getScheme())) {
81            writeFolderIndex();
82            return;
83        }
84        int match = sUriMatcher.match(mUri);
85        switch (match) {
86        case INDEX:
87            writeTemplatedIndex();
88            break;
89        case RESOURCE:
90            writeResource(getUriResourcePath());
91            break;
92        }
93    }
94
95    byte[] htmlEncode(String s) {
96        return TextUtils.htmlEncode(s).getBytes();
97    }
98
99    // We can reuse this for both History and Bookmarks queries because the
100    // columns defined actually belong to the CommonColumn and ImageColumn
101    // interfaces that both History and Bookmarks implement
102    private static final String[] PROJECTION = new String[] {
103        History.URL,
104        History.TITLE,
105        History.THUMBNAIL
106    };
107    private static final String SELECTION = History.URL
108            + " NOT LIKE 'content:%' AND " + History.THUMBNAIL + " IS NOT NULL";
109    void writeTemplatedIndex() throws IOException {
110        Template t = Template.getCachedTemplate(mContext, R.raw.most_visited);
111        Cursor historyResults = mContext.getContentResolver().query(
112                History.CONTENT_URI, PROJECTION, SELECTION,
113                null, History.VISITS + " DESC LIMIT 12");
114        Cursor cursor = historyResults;
115        try {
116            if (cursor.getCount() < 12) {
117                Cursor bookmarkResults = mContext.getContentResolver().query(
118                        Bookmarks.CONTENT_URI, PROJECTION, SELECTION,
119                        null, Bookmarks.DATE_CREATED + " DESC LIMIT 12");
120                cursor = new MergeCursor(new Cursor[] { historyResults, bookmarkResults }) {
121                    @Override
122                    public int getCount() {
123                        return Math.min(12, super.getCount());
124                    }
125                };
126            }
127            t.assignLoop("most_visited", new Template.CursorListEntityWrapper(cursor) {
128                @Override
129                public void writeValue(OutputStream stream, String key) throws IOException {
130                    Cursor cursor = getCursor();
131                    if (key.equals("url")) {
132                        stream.write(htmlEncode(cursor.getString(0)));
133                    } else if (key.equals("title")) {
134                        stream.write(htmlEncode(cursor.getString(1)));
135                    } else if (key.equals("thumbnail")) {
136                        stream.write("data:image/png;base64,".getBytes());
137                        byte[] thumb = cursor.getBlob(2);
138                        stream.write(Base64.encode(thumb, Base64.DEFAULT));
139                    }
140                }
141            });
142            t.write(mOutput);
143        } finally {
144            cursor.close();
145        }
146    }
147
148    private static final Comparator<File> sFileComparator = new Comparator<File>() {
149        @Override
150        public int compare(File lhs, File rhs) {
151            if (lhs.isDirectory() != rhs.isDirectory()) {
152                return lhs.isDirectory() ? -1 : 1;
153            }
154            return lhs.getName().compareTo(rhs.getName());
155        }
156    };
157
158    void writeFolderIndex() throws IOException {
159        File f = new File(mUri.getPath());
160        final File[] files = f.listFiles();
161        Arrays.sort(files, sFileComparator);
162        Template t = Template.getCachedTemplate(mContext, R.raw.folder_view);
163        t.assign("path", mUri.getPath());
164        t.assign("parent_url", f.getParent() != null ? f.getParent() : f.getPath());
165        t.assignLoop("files", new ListEntityIterator() {
166            int index = -1;
167
168            @Override
169            public void writeValue(OutputStream stream, String key) throws IOException {
170                File f = files[index];
171                if ("name".equals(key)) {
172                    stream.write(f.getName().getBytes());
173                }
174                if ("url".equals(key)) {
175                    stream.write(("file://" + f.getAbsolutePath()).getBytes());
176                }
177                if ("type".equals(key)) {
178                    stream.write((f.isDirectory() ? "dir" : "file").getBytes());
179                }
180                if ("size".equals(key)) {
181                    if (f.isFile()) {
182                        stream.write(readableFileSize(f.length()).getBytes());
183                    }
184                }
185                if ("last_modified".equals(key)) {
186                    String date = DateFormat.getDateTimeInstance(
187                            DateFormat.SHORT, DateFormat.SHORT)
188                            .format(f.lastModified());
189                    stream.write(date.getBytes());
190                }
191                if ("alt".equals(key)) {
192                    if (index % 2 == 0) {
193                        stream.write("alt".getBytes());
194                    }
195                }
196            }
197
198            @Override
199            public ListEntityIterator getListIterator(String key) {
200                return null;
201            }
202
203            @Override
204            public void reset() {
205                index = -1;
206            }
207
208            @Override
209            public boolean moveToNext() {
210                return (++index) < files.length;
211            }
212        });
213        t.write(mOutput);
214    }
215
216    static String readableFileSize(long size) {
217        if(size <= 0) return "0";
218        final String[] units = new String[] { "B", "KB", "MB", "GB", "TB" };
219        int digitGroups = (int) (Math.log10(size) / Math.log10(1024));
220        return new DecimalFormat("#,##0.#").format(
221                size / Math.pow(1024, digitGroups)) + " " + units[digitGroups];
222    }
223
224    String getUriResourcePath() {
225        final Pattern pattern = Pattern.compile("/?res/([\\w/]+)");
226        Matcher m = pattern.matcher(mUri.getPath());
227        if (m.matches()) {
228            return m.group(1);
229        } else {
230            return mUri.getPath();
231        }
232    }
233
234    void writeResource(String fileName) throws IOException {
235        Resources res = mContext.getResources();
236        String packageName = R.class.getPackage().getName();
237        int id = res.getIdentifier(fileName, null, packageName);
238        if (id != 0) {
239            InputStream in = res.openRawResource(id);
240            byte[] buf = new byte[4096];
241            int read;
242            while ((read = in.read(buf)) > 0) {
243                mOutput.write(buf, 0, read);
244            }
245        }
246    }
247
248    void writeString(String str) throws IOException {
249        mOutput.write(str.getBytes());
250    }
251
252    void writeString(String str, int offset, int count) throws IOException {
253        mOutput.write(str.getBytes(), offset, count);
254    }
255
256    void cleanup() {
257        try {
258            mOutput.close();
259        } catch (Exception e) {
260            Log.e(TAG, "Failed to close pipe!", e);
261        }
262    }
263
264}
265