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
;
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
;
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
;
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
{
71 private String
[] projection
;
73 public BlipCursor(Uri uri
, String
[] projection
) throws BadCredentials
, IOException
{
76 this.projection
= projection
;
81 private void query() throws BadCredentials
, IOException
{
83 result
= get(uri
.getPath());
86 throw new FileNotFoundException(uri
.toString());
88 if (result
instanceof JSONObject
)
89 addRow(projectJSON((JSONObject
) result
, projection
));
91 final JSONArray arr
= (JSONArray
) result
;
92 final int size
= arr
.length();
93 for (int i
= 0; i
< size
; i
++)
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
;
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
);
149 int response
= connection
.getResponseCode();
150 String message
= connection
.getResponseMessage();
151 connection
.disconnect();
152 Log
.d("BlipURLStreamFactory", "response code is " + response
);
155 throw new HttpResponseException(response
, message
);
159 private static class StandardURLStreamFactory
implements URLStreamFactory
{
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
{
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();
183 while ((line
= br
.readLine()) != null)
187 final String content
= sw
.toString();
188 if (content
.charAt(0) == '{')
189 return new JSONObject(content
);
191 return new JSONArray(content
);
192 } catch (JSONException e
) {
193 // TODO Auto-generated catch block
196 } catch (StringIndexOutOfBoundsException e
) {
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
;
231 public int delete(Uri uri
, String selection
, String
[] selectionArgs
) {
232 // TODO Auto-generated method stub
237 public String
getType(Uri uri
) {
238 // TODO Auto-generated method stub
243 public Uri
insert(Uri uri
, ContentValues values
) {
245 final String output
= formEncode(values
);
246 blipStreamFactory
.post(new URL("http", "api.blip.pl", -1, uri
.getPath()), output
);
248 } catch (IOException e
) {
249 Log
.e("Blip", "error while inserting into " + uri
.toString(), e
);
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() {
261 public String
getName() {
266 public String
getValue() {
267 return e
.getValue().toString();
272 return URLEncodedUtils
.format(parameters
, "UTF-8");
276 public boolean onCreate() {
277 cacheDir
= getContext().getCacheDir();
279 blipStreamFactory
= new BlipURLStreamFactory(login
, password
);
283 private void loadCredentials() {
284 SharedPreferences prefs
= getSharedPreferences(getContext());
285 login
= prefs
.getString(USERNAME
, null);
286 password
= prefs
.getString(PASSWORD
, null);
290 public Cursor
query(Uri uri
, String
[] projection
, String selection
,
291 String
[] selectionArgs
, String sortOrder
) {
293 return new BlipCursor(uri
, projection
);
294 } catch (FileNotFoundException e
) {
295 Log
.d("Blip", "file not found when querying " + uri
.toString());
297 } catch (Exception e
) {
298 // TODO Auto-generated catch block
299 log_error("Error while querying", e
);
304 private String
[] projectJSON(JSONObject object
, String
[] projection
) {
305 String
[] result
= new String
[projection
.length
];
307 for (int i
= 0; i
< projection
.length
; i
++) {
309 if (projection
[i
].equals("_id"))
310 path
= Arrays
.asList(new String
[] {"id"});
312 path
= Arrays
.asList(projection
[i
].split("/"));
314 result
[i
] = extractJSON(object
, path
);
315 } catch (JSONException e
) {
316 // TODO Auto-generated catch block
318 } catch (IOException e
) {
319 // TODO Auto-generated catch block
327 private String
extractJSON(JSONObject object
, List
<String
> path
) throws JSONException
, IOException
, BadCredentials
{
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
);
344 final String next
= path
.get(1);
345 if (next
.equals("login"))
346 // extract login from path
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
)));
370 public ParcelFileDescriptor
openFile(Uri uri
, String mode
)
371 throws FileNotFoundException
{
374 url
= new URL("http", "blip.pl", -1, uri
.getPath());
375 } catch (MalformedURLException e
) {
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
];
396 while ((count
= bis
.read(buffer
)) != -1) {
397 fos
.write(buffer
, 0, count
);
400 } catch (IOException e
) {
401 System
.err
.println("Error while getting " + url
.toString());
403 if (!exists
) throw new FileNotFoundException(e
.getMessage());
409 private boolean cacheIsStale(File file
) {
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
421 return false; // assume everything else is immutable
423 final long age
= System
.currentTimeMillis() - file
.lastModified();
424 return age
> max_age
;
428 public int update(Uri uri
, ContentValues values
, String selection
,
429 String
[] selectionArgs
) {
430 // TODO Auto-generated method stub
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
);
455 private static boolean checkCredentials(String username
, String password
) throws IOException
{
457 URLStreamFactory factory
= new BlipURLStreamFactory(username
, password
);
458 factory
.openStream(new URL("http://api.blip.pl/updates?limit=1"));
459 } catch (BadCredentials e
) {