Legalese.
[bliper.git] / src / pl / blip / divide / bliper / Blip.java
blobd567424f07728dfeed2430ef0d80f65289df7969
1 /*
2 Bliper - a blip.pl client for Android
3 Copyright (C) 2009 RafaƂ Rzepecki
5 This program is free software: you can redistribute it and/or modify
6 it under the terms of the GNU General Public License as published by
7 the Free Software Foundation, either version 3 of the License, or
8 (at your option) any later version.
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 GNU General Public License for more details.
15 You should have received a copy of the GNU General Public License
16 along with this program. If not, see <http://www.gnu.org/licenses/>.
19 package pl.blip.divide.bliper;
21 import java.io.BufferedInputStream;
22 import java.io.BufferedReader;
23 import java.io.DataOutputStream;
24 import java.io.File;
25 import java.io.FileInputStream;
26 import java.io.FileNotFoundException;
27 import java.io.FileOutputStream;
28 import java.io.IOException;
29 import java.io.InputStream;
30 import java.io.InputStreamReader;
31 import java.io.OutputStream;
32 import java.io.StringWriter;
33 import java.net.ContentHandler;
34 import java.net.HttpURLConnection;
35 import java.net.MalformedURLException;
36 import java.net.URL;
37 import java.net.URLConnection;
38 import java.nio.charset.Charset;
39 import java.util.Arrays;
40 import java.util.LinkedList;
41 import java.util.List;
42 import java.util.Set;
43 import java.util.Map.Entry;
44 import java.util.regex.Matcher;
45 import java.util.regex.Pattern;
47 import org.apache.http.NameValuePair;
48 import org.apache.http.client.HttpResponseException;
49 import org.apache.http.client.utils.URLEncodedUtils;
50 import org.json.JSONArray;
51 import org.json.JSONException;
52 import org.json.JSONObject;
54 import android.content.ContentProvider;
55 import android.content.ContentValues;
56 import android.content.Context;
57 import android.content.SharedPreferences;
58 import android.content.SharedPreferences.Editor;
59 import android.database.Cursor;
60 import android.database.MatrixCursor;
61 import android.net.Uri;
62 import android.os.ParcelFileDescriptor;
63 import android.util.Log;
65 public class Blip extends ContentProvider {
66 static public String DIRECTED_MESSAGE_TYPE = "DirectedMessage";
67 static public String PRIVATE_MESSAGE_TYPE = "PrivateMessage";
69 private class BlipCursor extends MatrixCursor {
70 private Uri uri;
71 private String[] projection;
73 public BlipCursor(Uri uri, String[] projection) throws BadCredentials, IOException {
74 super(projection);
75 this.uri = uri;
76 this.projection = projection;
78 query();
81 private void query() throws BadCredentials, IOException {
82 Object result;
83 result = get(uri.getPath());
85 if (result == null)
86 throw new FileNotFoundException(uri.toString());
88 if (result instanceof JSONObject)
89 addRow(projectJSON((JSONObject) result, projection));
90 else {
91 final JSONArray arr = (JSONArray) result;
92 final int size = arr.length();
93 for (int i = 0; i < size; i++)
94 try {
95 addRow(projectJSON(arr.getJSONObject(i), projection));
96 } catch (JSONException e) {
97 log_error("error while building result for querying " + uri.toString(), e);
98 throw new FileNotFoundException(e.getMessage());
104 private static class BlipURLStreamFactory implements URLStreamFactory {
105 private String username, password;
107 public BlipURLStreamFactory(String username, String password) {
108 this.username = username;
109 this.password = password;
112 @Override
113 public InputStream openStream(URL url) throws IOException {
114 Log.d("BlipURLStreamFactory", "Opening " + url);
115 final HttpURLConnection connection = (HttpURLConnection) url.openConnection();
116 connection.addRequestProperty("X-Blip-Api", "0.02");
117 connection.addRequestProperty("Accept", "application/json");
118 connection.addRequestProperty("User-Agent", "Bliper/0.1");
119 if (username != null) {
120 connection.addRequestProperty("Authorization", "Basic " + Base64Coder.encodeString(username + ":" + password));
122 connection.connect();
123 if (connection.getResponseCode() == 401)
124 throw new BadCredentials();
125 return connection.getInputStream();
128 public void post(URL url, String output) throws IOException {
129 Log.d("BlipURLStreamFactory", "POSTing to " + url);
130 final HttpURLConnection connection = (HttpURLConnection) url.openConnection();
131 connection.setRequestMethod("POST");
132 connection.addRequestProperty("X-Blip-Api", "0.02");
133 connection.addRequestProperty("Accept", "application/json");
134 connection.addRequestProperty("User-Agent", "Bliper/0.1");
135 connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
136 connection.setDoInput(true);
137 connection.setDoOutput(true);
138 if (username != null) {
139 connection.addRequestProperty("Authorization", "Basic " + Base64Coder.encodeString(username + ":" + password));
141 connection.connect();
143 OutputStream os = connection.getOutputStream();
144 DataOutputStream dos = new DataOutputStream(os);
145 dos.writeBytes(output);
146 dos.flush();
147 dos.close();
149 int response = connection.getResponseCode();
150 String message = connection.getResponseMessage();
151 connection.disconnect();
152 Log.d("BlipURLStreamFactory", "response code is " + response);
154 if (response >= 400)
155 throw new HttpResponseException(response, message);
159 private static class StandardURLStreamFactory implements URLStreamFactory {
160 @Override
161 public InputStream openStream(URL url) throws IOException {
162 System.err.println("Opening " + url + " using StandardURLStreamFactory");
163 return url.openStream();
167 private interface URLStreamFactory {
168 InputStream openStream(URL url) throws IOException;
171 public class BlipContentHandler extends ContentHandler {
172 @Override
173 public Object getContent(URLConnection conn) throws IOException {
174 return JSONFromStream(conn.getInputStream());
178 public static Object JSONFromStream(final InputStream inputStream) throws IOException {
179 final InputStreamReader isr = new InputStreamReader(inputStream, Charset.forName("utf-8"));
180 final BufferedReader br = new BufferedReader(isr, 1024 /* should be enough */);
181 final StringWriter sw = new StringWriter();
182 String line;
183 while ((line = br.readLine()) != null)
184 sw.write(line);
186 try {
187 final String content = sw.toString();
188 if (content.charAt(0) == '{')
189 return new JSONObject(content);
190 else
191 return new JSONArray(content);
192 } catch (JSONException e) {
193 // TODO Auto-generated catch block
194 e.printStackTrace();
195 return null;
196 } catch (StringIndexOutOfBoundsException e) {
197 // empty object
198 Log.d("Blip", "got empty object");
199 return new JSONArray();
203 public static void log_error(String msg, Throwable e) {
204 Log.e("Blip", msg, e);
207 public static class BadCredentials extends IOException {
208 private static final long serialVersionUID = -5503599181549720716L;
211 public static class Users {
212 public static final Uri CONTENT_URI = Uri.parse("content://blip.pl/users");
214 public static class Updates {
215 public static final Uri CONTENT_URI = Uri.parse("content://blip.pl/updates");
218 public static final Uri CONTENT_URI = Uri.parse("content://blip.pl");
219 public static final Uri DASHBOARD_URI = Uri.parse("content://blip.pl/dashboard");
221 private static final String USERNAME = "username";
222 private static final String PASSWORD = "password";
223 private String login;
224 private String password;
225 private static URLStreamFactory standardStreamFactory = new StandardURLStreamFactory();
226 private BlipURLStreamFactory blipStreamFactory;
228 private File cacheDir;
230 @Override
231 public int delete(Uri uri, String selection, String[] selectionArgs) {
232 // TODO Auto-generated method stub
233 return 0;
236 @Override
237 public String getType(Uri uri) {
238 // TODO Auto-generated method stub
239 return null;
242 @Override
243 public Uri insert(Uri uri, ContentValues values) {
244 try {
245 final String output = formEncode(values);
246 blipStreamFactory.post(new URL("http", "api.blip.pl", -1, uri.getPath()), output);
247 return uri;
248 } catch (IOException e) {
249 Log.e("Blip", "error while inserting into " + uri.toString(), e);
250 return null;
254 private static String formEncode(ContentValues values) {
255 final Set<Entry<String, Object>>valueSet = values.valueSet();
256 List<NameValuePair> parameters = new LinkedList<NameValuePair>();
258 for (final Entry<String, Object> e: valueSet) {
259 parameters.add(new NameValuePair() {
260 @Override
261 public String getName() {
262 return e.getKey();
265 @Override
266 public String getValue() {
267 return e.getValue().toString();
272 return URLEncodedUtils.format(parameters, "UTF-8");
275 @Override
276 public boolean onCreate() {
277 cacheDir = getContext().getCacheDir();
278 loadCredentials();
279 blipStreamFactory = new BlipURLStreamFactory(login, password);
280 return true;
283 private void loadCredentials() {
284 SharedPreferences prefs = getSharedPreferences(getContext());
285 login = prefs.getString(USERNAME, null);
286 password = prefs.getString(PASSWORD, null);
289 @Override
290 public Cursor query(Uri uri, String[] projection, String selection,
291 String[] selectionArgs, String sortOrder) {
292 try {
293 return new BlipCursor(uri, projection);
294 } catch (FileNotFoundException e) {
295 Log.d("Blip", "file not found when querying " + uri.toString());
296 return null;
297 } catch (Exception e) {
298 // TODO Auto-generated catch block
299 log_error("Error while querying", e);
300 return null;
304 private String[] projectJSON(JSONObject object, String[] projection) {
305 String[] result = new String[projection.length];
307 for (int i = 0; i < projection.length; i++) {
308 List<String> path;
309 if (projection[i].equals("_id"))
310 path = Arrays.asList(new String[] {"id"});
311 else
312 path = Arrays.asList(projection[i].split("/"));
313 try {
314 result[i] = extractJSON(object, path);
315 } catch (JSONException e) {
316 // TODO Auto-generated catch block
317 e.printStackTrace();
318 } catch (IOException e) {
319 // TODO Auto-generated catch block
320 e.printStackTrace();
324 return result;
327 private String extractJSON(JSONObject object, List<String> path) throws JSONException, IOException, BadCredentials {
328 if (object == null)
329 return null;
331 if (path.size() == 1)
332 return object.getString(path.get(0));
334 final String root = path.get(0);
336 JSONObject child = null;
337 if (object.has(root))
338 child = object.getJSONObject(root);
339 else if (object.has(root + "_path")) {
340 String childPath = object.getString(root + "_path");
341 // special cases for network request count optimization
342 final Matcher m = Pattern.compile("/users/(.*)$").matcher(childPath);
343 if (m.matches()) {
344 final String next = path.get(1);
345 if (next.equals("login"))
346 // extract login from path
347 return m.group(1);
348 else if (next.equals("avatar")) {
349 // go straight to avatar
350 Object theChild = get("/users/" + m.group(1) + "/avatar");
351 if (theChild instanceof JSONObject) child = (JSONObject) theChild;
352 else return null; // no avatar
353 return extractJSON(child, path.subList(2, path.size()));
356 // end of special cases
357 Object theChild = get(childPath);
358 if (theChild instanceof JSONObject) child = (JSONObject) theChild;
359 else return null; // not expected, better that than TODO
362 return extractJSON(child, path.subList(1, path.size()));
365 private Object get(String path) throws IOException, BadCredentials {
366 return JSONFromStream(new FileInputStream(cachedResource(new URL("http", "api.blip.pl", -1, path), blipStreamFactory)));
369 @Override
370 public ParcelFileDescriptor openFile(Uri uri, String mode)
371 throws FileNotFoundException {
372 URL url;
373 try {
374 url = new URL("http", "blip.pl", -1, uri.getPath());
375 } catch (MalformedURLException e) {
376 e.printStackTrace();
377 throw new FileNotFoundException(uri.toString());
380 return ParcelFileDescriptor.open(cachedResource(url, standardStreamFactory), ParcelFileDescriptor.MODE_READ_ONLY);
383 private File cachedResource(URL url, URLStreamFactory factory) throws FileNotFoundException {
384 final File cachedFile = new File(cacheDir + File.separator + url.getPath() + "/.data"); // the suffix is to avoid file/dir clashes
386 final boolean exists = cachedFile.exists();
387 final boolean isStale = !exists || cacheIsStale(cachedFile);
388 if (!exists || isStale) try {
389 cachedFile.getParentFile().mkdirs();
391 final FileOutputStream fos = new FileOutputStream(cachedFile);
392 final BufferedInputStream bis = new BufferedInputStream(factory.openStream(url), 1024);
393 final int BUFSIZE = 1024;
394 byte buffer[] = new byte[BUFSIZE];
395 int count;
396 while ((count = bis.read(buffer)) != -1) {
397 fos.write(buffer, 0, count);
399 fos.close();
400 } catch (IOException e) {
401 System.err.println("Error while getting " + url.toString());
402 e.printStackTrace();
403 if (!exists) throw new FileNotFoundException(e.getMessage());
406 return cachedFile;
409 private boolean cacheIsStale(File file) {
410 long max_age = 0;
411 final String path = file.getAbsolutePath();
412 if (path.matches(".*/users/.*?/avatar/.*"))
413 max_age = 60 * 60 * 1000; // an hour
414 else if (path.matches(".*/users/.*?/.*"))
415 max_age = 0; // refreshed as often as it's requested
416 else if (path.matches(".*/dashboard/.*"))
417 max_age = 0; // refreshed as often as it's requested
418 else if (path.matches(".*/since/.*"))
419 max_age = 0; // refreshed as often as it's requested
420 else
421 return false; // assume everything else is immutable
423 final long age = System.currentTimeMillis() - file.lastModified();
424 return age > max_age;
427 @Override
428 public int update(Uri uri, ContentValues values, String selection,
429 String[] selectionArgs) {
430 // TODO Auto-generated method stub
431 return 0;
434 public static String getUsername(Context context) {
435 final SharedPreferences prefs = getSharedPreferences(context);
436 return prefs.getString(USERNAME, null);
439 private static SharedPreferences getSharedPreferences(Context context) {
440 return context.getSharedPreferences("blip.pl.credentials", Context.MODE_PRIVATE);
443 public static void setCredentials(Context context,
444 String username, String password) throws BadCredentials, IOException {
445 if (!checkCredentials(username, password))
446 throw new BadCredentials();
448 final SharedPreferences prefs = getSharedPreferences(context);
449 final Editor editor = prefs.edit();
450 editor.putString(USERNAME, username);
451 editor.putString(PASSWORD, password);
452 editor.commit();
455 private static boolean checkCredentials(String username, String password) throws IOException {
456 try {
457 URLStreamFactory factory = new BlipURLStreamFactory(username, password);
458 factory.openStream(new URL("http://api.blip.pl/updates?limit=1"));
459 } catch (BadCredentials e) {
460 return false;
462 return true;