Tag Archives: OAuth

[Android Dev] Gmail OAuth (2)

这一部分主要介绍如何使用已获得的Access Token去获取Gmail以及计算未读邮件的数量。

1. 从上一篇博客里获得的AccessToken以及Secret,生成OAuthConsumer,非常简单。

CommonsHttpOAuthConsumer consumer =
  new CommonsHttpOAuthConsumer("anonymous", "anonymous");
consumer.setTokenWithSecret(token, secret);

2. Gmail提供了RSS Feed,只需要使用token取得RSS Feed,就拿到了Gmail的内容。其中:
https://mail.google.com/mail/feed/atom/ 对应Inbox里的未读邮件
https://mail.google.com/mail/feed/atom/unread/ 对应所有的未读邮件
https://mail.google.com/mail/feed/atom/labelname/ 对应某个label的未读邮件
我只需要取得用户的Inbox的未读邮件,因此code是这样的:

HttpGet request = new HttpGet("https://mail.google.com/mail/feed/atom/");
// sign the request
try {
  consumer.sign(request);
} catch (Exception e) {
  e.printStackTrace();
}
// send the request
HttpClient httpClient = new DefaultHttpClient();
org.apache.http.HttpResponse response;
try {
  response = httpClient.execute(request);
  feedString = read(response.getEntity().getContent());
} catch (Exception e) {
  e.printStackTrace();
}

feedString里就保存了Google返回的feed的内容。而函数read是为了把response里的内容读出来生成String. 这个是从网上抄来的…

private static String read(InputStream in) throws IOException {
  StringBuilder sb = new StringBuilder();
  BufferedReader r = new BufferedReader(new InputStreamReader(in), 1000);
  for (String line = r.readLine(); line != null; line = r.readLine()) {
    sb.append(line);
  }
  in.close();
  return sb.toString();
}

3. 有了feed,就剩下计算unread count的工作了。这分成两部分。
i) 从String生成XML Document,其实是一个DOM tree;
ii) 在XML Document里parse数据,取得unread count.
关于i) 也是网上有相应的code:

private static Document XMLfromString(String xml){
  Document doc = null;
  DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
  try {
    DocumentBuilder db = dbf.newDocumentBuilder();
    InputSource is = new InputSource();
    is.setCharacterStream(new StringReader(xml));
    doc = db.parse(is);
  } catch (ParserConfigurationException e) {
  // XML parse error
  return null;
  } catch (SAXException e) {
  // Wrong XML file structure
  return null;
  } catch (IOException e) {
  //I/O exeption
  return null;
  }
  return doc;
}

关于ii) Gmail的一个个邮件是放在一个个entry里的,所以只需要parse entry就行了。

NodeList nodes = feedDoc.getElementsByTagName("entry");
unread_count = nodes.getLength();

这就搞定了未读邮件数量的计算。

4. 虽然正常情况下都没问题,但是假如用户改了密码,token就无效了,应该检测这种情况,然后让用户重新进行authenticate。虽然这不常见,但也得处理。
假如Token变得无效,Gmail会返回一个显示Unauthorized的网页。因此最简单的方法是通过字符串判断. 注意如果是正常的feed,string是以<?xml为开头的,而如果是unauthorized的网页,就以<HTML>开头。

private static boolean verifyValid(String doc) {
  return !(doc.startsWith("<HTML>") && doc.contains("<TITLE>Unauthorized"));
}

这样就解决了。

Q.E.D.

Share

[Android Dev] Gmail OAuth

自从几个月前Gmail更新之后,第三方app就不能再通过ContentResolver去获取gmail的内容和状态了。因此很多Gmail Unread/Reminder就没办法用以前的方式工作了。我的app里Gmail Reminder的功能也因此暂时取消。
最近终于有时间继续写code,google了一番之后发现目前最可靠的方法就是用OAuth来访问Gmail。

不过在Android实现OAuth还是有一点tricky的,在这边写下来,作为开发笔记,说不定也能帮到一些人 🙂
p.s. 得感谢Google Search和Stackoverflow,从里面抄了不少code.

计划分成2部分: 1) OAuth认证; 2) 通过Token访问Gmail并计算unread count
具体的细节可以直接访问开源项目 https://code.google.com/p/minemessagevibrator/

一)OAuth: 主要使用signpost库实现。(https://code.google.com/p/oauth-signpost/)

1. 是一个处理OAuth以及callback的Activity. 这个Activity负责打开oauth的网站,让用户认证完成之后得到callback,并得到和保存token.
说明:
i) android:scheme和android:host是为了回调的时候响应自定义的URI: “mine-activity://mine-vibration/”
ii) singleTask是为了回调的时候能得到onNewIntent()消息

<activity android:name=".oauth.MineOAuthAccessActivity" android:launchMode="singleTask">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="mine-activity" android:host="mine-vibration"/>
</intent-filter>
</activity>

2. 准备signpost的OAuthProvider, 其中consumerKey和consumerSecret是应用的key和secret,熟悉Twitter OAuth的人肯定知道。在Google上可以用”anonymous”

mConsumer = new CommonsHttpOAuthConsumer(consumerKey, consumerSecret);
mProvider = new CommonsHttpOAuthProvider(
"https://www.google.com/accounts/OAuthGetRequestToken?scope="
+ URLEncoder.encode("https://mail.google.com/", "utf-8"),
"https://www.google.com/accounts/OAuthGetAccessToken",
"https://www.google.com/accounts/OAuthAuthorizeToken?hd=default");
mCallbackUrl = "mine-activity://mine-vibration/";

3. 获得request token的URL,很简单。但是注意这一步需要网络连接。

String authUrl = mProvider.retrieveRequestToken(mConsumer, mCallbackUrl);

4. 打开浏览器让用户进行认证,也很简单。

startActivity(new Intent("android.intent.action.VIEW", Uri.parse(ret)).
setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP |
Intent.FLAG_ACTIVITY_NO_HISTORY |
Intent.FLAG_FROM_BACKGROUND));

5. 当用户认证完,网站会重定向到callback URL,就是 “mine-activity://mine-vibration/”,在Android里,会启动MineOAuthAccessActivity并调用onNewIntent(),我们要做的事情是从回调的参数里获得verifier,并获取access token & secret。这样OAuth的认证过程就结束了。之后app可以通过access token去访问gmail。

Uri uri = intent.getData();
String verifier = uri.getQueryParameter("oauth_verifier");
mProvider.retrieveAccessToken(mConsumer, verifier);
mConsumer.getToken();    // AccessToken
mConsumer.getTokenSecret();    // Token Secret

6. 在小内存的机器上(比如说俺的Milestone),启动浏览器之后基本上原来的Activity就会被kill掉,而且process也很可能被杀掉,这样当callback回来的时候,会发现原来的变量都没了。所以我们必须保存OAuth相关的变量,这样可以在启动浏览器之前保存,在重启Activity之后恢复。可以通过java的Serialize object来实现。
保存:

FileOutputStream fos = context.openFileOutput("xxx", // saved file name
Context.MODE_PRIVATE);
ObjectOutputStream os = new ObjectOutputStream(fos);
os.writeObject(xxx);    // the object to save
os.close();

读取:

FileInputStream fis;
fis = context.openFileInput("xxx");  // the file name
ObjectInputStream is = new ObjectInputStream(fis);
xxx= (xxx) is.readObject();    // the object to load
is.close();

但是signpost的类虽然是Serializable的,但是其中有个不知道算不算bug的问题,其中CommonsHttpOAuthProvider里的httpClient被声明成transient的,所以这个变量是不能被serialize的。因此得额外加一句:

mProvider.setHttpClient(new DefaultHttpClient());

Done!

btw, 通过Token访问Gmail并计算unread count会有另外一篇blog来介绍。

Share