2011年12月20日火曜日

SQLite暗号化OSS「SQLCipher for Android」を使ってみた。

こんにちは@katsummyです。

※Twitterとデベロッパー名が違いますのであしからず^^


Android Advent Calendar 2011 の参加への投稿となります。
Android Advent Calenderについてはこちらを参照下さい。


勢いで参加したものの、先の方々がガチな技術や面白いネタの投稿をされてるのを見て恐縮してます。
。。゛(ノ><)ノ ヒィ
初心者向けな内容です。過度の期待はしないで下さい。


ネタは敷居高そうだし、技術的な投稿の内容を考えておりましたが、
あるところでAndroidの端末内のデータの暗号化について話題が上がったので、
その際に考えてた内容とSQLiteの暗号化について記載したいと思います。



Android 3.x 以降では、ストレージ暗号化機能を使用することができます。
SDカードや内蔵ストレージをすべて暗号化できますので安全性は高くなります。
その端末が専用に使用しているのであれば、有効では無いかと思います。しかし、他の用途でも使用している場合、ひとつのアプリのためだけに、全体を暗号化するのはどうかなと思います。
そもそも、Android 2.3.x 以前の端末では使用できません。


アプリ個別に管理しているファイルを暗号化する。
端末レベルで暗号化するより、比較的導入が容易かと思います。

Androidで、データを管理する方法としては、
・ファイル (.TXT .CSV .XML etc...)
プリファレンス(XML)
・SQLite
これらのファイルを暗号化します。

ファイルの暗号化は、AES/DES/RSA暗号などを用いれば良いと思います。コードサンプルはググれば多く見つかると思うので記載はか割愛します。
プリファレンス(XML)は、それ自信は暗号化されませんので、中に持つ値を暗号化する必要があります。

SQLiteは、暗号化機能を持っていないため、中のレコード自体は暗号化されていません。バイナリー化もされていない(?)ようなので、ダンプをとると容易に中身を参照することができてしまいます。レコードに格納する値を暗号化する方法もありますが、SQLのクエリーやソートが使用できなくなってしまいます。

SQLiteを暗号化するオープンソース・ソフトウェアにSQLCipherがあります。
CやPHP、iPhoneで使用することができてましたが、Android版も11月末に正式版?( v1 FINAL)がリリースされました。





使用するにはgithubからダウンロードしましすが、ソースコードが不要の場合、
https://github.com/guardianproject/android-database-sqlcipher/tree/master/dist/SQLCipherForAndroid-SDK
からダウンロードすれば良いと思います。
必要な


SQLCipherをプロジェクトに設定する手順


1) libs フォルダをプロジェクトにコピーします。
 - commons-codec.jar
 - guava-r09.jar
 - sqlcipher.jar
 + armeabi
  - libdatabase_sqlcipher.so
  - libsqlcipher_android.so
  - libstlport_shared.so


 ※ ターゲットが2.2以下の場合、 assets フォルダもプロジェクトにコピーします。
 + assets
  - icudt44l.zip


2) ソースコードの修正
 a) importするパッケージはSQLiteをSQLCipherに変更します。
  import android.database 
  -->>
  import info.guardianproject.database


 b) 初期処理としてライブラリのロードを行います。
  SQLiteDatabase.loadLibs(context);


 c) データベースをオープンする時にパスワードを指定します。
  SQLiteOpenHelper.getWritableDatabase("thisismysecret"):

  SQLiteOpenHelper.getReadableDatabase("thisismysecret"):


 後は、SQLiteと同じ記述となります。


サンプルソース
  1. import android.database.Cursor;  
  2. import info.guardianproject.database.sqlcipher.SQLiteDatabase;  
  3. import android.app.Activity;  
  4. import android.content.ContentValues;  
  5. import android.os.Bundle;  
  6. import android.util.Log;  
  7.   
  8. public class SQLDemoActivity extends Activity {  
  9.  EventDataSQLHelper mSQLHelper;  
  10.   
  11.  @Override  
  12.  public void onCreate(Bundle savedInstanceState) {  
  13.   super.onCreate(savedInstanceState);  
  14.   
  15.   // SQLCipherライブラリのイニシャルロード  
  16.   SQLiteDatabase.loadLibs(this);  
  17.   
  18.   String password = "foo123";  
  19.   
  20.   mSQLHelper = new EventDataSQLHelper(this);  
  21.   
  22.   // パスワードを指定して書き込み可能なデータベースを開く  
  23.   SQLiteDatabase db = mSQLHelper.getWritableDatabase(password);  
  24.   
  25.   // レコードのインサート  
  26.   for (int i = 1; i < 10; i++) {  
  27.    addEvent("こんにちは Android イベント: " + i, db);  
  28.   }  
  29.   db.close();  
  30.   
  31.   // パスワードを指定して読み込み可能なデータベースを開く  
  32.   db = mSQLHelper.getReadableDatabase(password);  
  33.   
  34.   // クエリー  
  35.   Cursor cursor = db.query(EventDataSQLHelper.TABLE, nullnullnullnullnullnull);  
  36.   startManagingCursor(cursor);  
  37.   // レコードの読み込み  
  38.   StringBuilder ret = new StringBuilder("Saved Events:\n\n");  
  39.   while (cursor.moveToNext()) {  
  40.    long id = cursor.getLong(0);  
  41.    long time = cursor.getLong(1);  
  42.    String title = cursor.getString(2);  
  43.    ret.append(id + ": " + time + ": " + title + "\n");  
  44.   }  
  45.   Log.i("sqldemo", ret.toString());  
  46.   
  47.   db.close();  
  48.  }  
  49.   
  50.  @Override  
  51.  public void onDestroy() {  
  52.   super.onDestroy();  
  53.   mSQLHelper.close();  
  54.  }  
  55.   
  56.  private void addEvent(String title, SQLiteDatabase db) {  
  57.   ContentValues values = new ContentValues();  
  58.   values.put(EventDataSQLHelper.TIME, System.currentTimeMillis());  
  59.   values.put(EventDataSQLHelper.TITLE, title);  
  60.   db.insert(EventDataSQLHelper.TABLE, null, values);  
  61.  }  
  62.   
  63. }  

  1. import info.guardianproject.database.sqlcipher.SQLiteDatabase;  
  2. import info.guardianproject.database.sqlcipher.SQLiteOpenHelper;  
  3. import android.content.Context;  
  4. import android.provider.BaseColumns;  
  5. import android.util.Log;  
  6.   
  7. // SQLiteOpenHelper  
  8. public class EventDataSQLHelper extends SQLiteOpenHelper {  
  9.  // データベス名  
  10.  private static final String DATABASE_NAME = "events.db";  
  11.  private static final int DATABASE_VERSION = 1;  
  12.  // テーブル名  
  13.  public static final String TABLE = "events";  
  14.  // カラム  
  15.  public static final String TIME = "time";  
  16.  public static final String TITLE = "title";  
  17.   
  18.  // コンストラクタ  
  19.  public EventDataSQLHelper(Context context) {  
  20.   super(context, DATABASE_NAME, null, DATABASE_VERSION);  
  21.  }  
  22.   
  23.  @Override  
  24.  public void onCreate(SQLiteDatabase db) {  
  25.   String sql = "create table " + TABLE + "( "  
  26.     + BaseColumns._ID + " integer primary key autoincrement, "  
  27.     + TIME + " integer, "  
  28.     + TITLE + " text not null);";  
  29.   Log.d("EventsData""onCreate: " + sql);  
  30.   db.execSQL(sql);  
  31.  }  
  32.   
  33.  @Override  
  34.  public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {  
  35.  }  
  36.   
  37. }  



ダンプ
$ hexdump -C events.db
  • SQLite
00000000  53 51 4c 69 74 65 20 66  6f 72 6d 61 74 20 33 00  |SQLite format 3.|
...
00000e30  00 00 00 00 00 31 09 04  00 05 5b 01 34 42 1d 75  |.....1....[.4B.u|
00000e40  20 e3 81 93 e3 82 93 e3  81 ab e3 81 a1 e3 81 af  | ...............|
00000e50  20 41 6e 64 72 6f 69 64  20 e3 82 a4 e3 83 99 e3  | Android .......|
00000e60  83 b3 e3 83 88 3a 20 39  31 08 04 00 05 5b 01 34  |.....: 91....[.4|
00000e70  42 1d 74 ec e3 81 93 e3  82 93 e3 81 ab e3 81 a1  |B.t.............|
00000e80  e3 81 af 20 41 6e 64 72  6f 69 64 20 e3 82 a4 e3  |... Android ....|
00000e90  83 99 e3 83 b3 e3 83 88  3a 20 38 31 07 04 00 05  |........: 81....|
00000ea0  5b 01 34 42 1d 74 d4 e3  81 93 e3 82 93 e3 81 ab  |[.4B.t..........|
00000eb0  e3 81 a1 e3 81 af 20 41  6e 64 72 6f 69 64 20 e3  |...... Android .|
…
  • SQLCipher
00000000  5a f7 b6 fa 3e 78 f2 9b  10 71 bc cd 64 e8 f4 01  |Z...>x...q..d...|
00000010  11 7c 48 1b 3f d9 15 e3  70 01 68 c0 d0 7c be 0b  |.|H.?...p.h..|..|
00000020  af 55 b5 95 7b a3 d2 83  90 61 31 0a 83 ce 52 13  |.U..{....a1...R.|
00000030  0a ac 02 a3 39 ad ec e2  92 65 3f 01 c6 d6 5d 97  |....9....e?...].|
00000040  b0 d0 f3 a3 5a c2 3b 1d  58 b4 d2 41 2a d0 bf 39  |....Z.;.X..A*..9|
00000050  3c 96 88 8b 66 ff d1 de  73 c5 c8 2e d6 52 6b 7d  |<...f...s....Rk}|
00000060  6c 7e f9 41 bb 9e 00 bb  3e d6 2e 9a dc 07 da 0f  |l~.A....>.......|
00000070  fe 0e 14 38 63 34 01 3e  e7 70 c4 4c 20 da c5 d5  |...8c4.>.p.L ...|
…
暗号化されているのが分かると思います。

なお、サンプルコードは、パスワードをソースコードに記述してますが、
端末を識別する情報やGoogleアカウント情報など一意となる情報から、
生成した方が良いと思います。

※ IMEI(端末識別番号)、IMSI(加入者識別番号)、ICCID(SIMカード固有番号)、Android IDの文字列を組み合わせて、ハッシュ化するとか〜。


SQLiteのデータベースは通常/data/data/package/databasesに作成されるためアプリ以外のユーザ権限で取得することはできませんがルートユーザでは可能になります。
また、データベースの保管先は、SDCardに指定することも可能です。


Rooted端末対策として、保管をSDCard指定する際には、SQLCipherを利用してみてはどうかなと思います。



ライセンス
同梱されているLICENSEファイルには、Apache License Version 2.0とされていました。
別にSQLCIPHER_LICENSEファイルにも再配布や免責事項に付いて記載がありますので確認しておいた方がよいと思います。






でわでわ。




2011年10月22日土曜日

Conversion to Dalvik format failed with error 1

ADT14 にアップデートしたら「Conversion to Dalvik format failed with error 1」でまくったので、その時の対応メモ。


原因は参照ライブラリでした。

プロジェクトツリーに、参照しているライブラリのソースパスが追加されてました。 

プロジェクトのプロパティでLibraryに追加しています。
これは問題ありません。

Java  Build Path に参照ライブラリのパスが追加されていました。
こいつが悪さしてるっぽいので削除します。

プロジェクトツリーに、参照しているライブラリのソースパスが残っているので、
これも削除してしまいました。


これでエラーが出なくなりました。


ActionBarSherlock の Issues にも対応方法が上がってました。
#79: not compatible with Android SDK Tools 14 - Issues - JakeWharton/ActionBarSherlock - GitHub https://github.com/JakeWharton/ActionBarSherlock/issues/79#issuecomment-2464701

2011年10月13日木曜日

List of libraries for android

[覚書] Android用各種ライブラリ一覧
  • Back
    • Twitter4J 
      • A Java library for the Twitter API
    • Android ROME Feed Reader
      • ROME is an open source Java tool for parsing, generating, and publishing RSS and Atom feeds. Android ROME Feed Reader is a repackaging of ROME so that it works on Android devices.
    • Spring Android
      • A Rest Client for Android
      • Auth support for accessing secure APIs
  • Graph
    • AChartEngine
      • AChartEngine is a charting library for Android applications. It currently supports the following chart types:
        • line chart / area chart / scatter chart / time chart / bar chart / pie chart / bubble chart / doughnut chart / range (high-low) bar chart / dial chart / gauge / combined (any combination of line, cubic line, scatter, bar, range bar, bubble) chart / cubic line chart
    • AndroidPlot
      • AndroidPlot is a pure Java API for creating dynamic and static charts within your Android application. AndroidPlot currently supports the following types of charts:
        • Line charts / Scatter charts / Bar charts / Step charts
    • ChartDroid
      • ChartDroid is an Intent-based "library application" for static chart and graph generation on Android. It can graph/plot/display numerical data in many representations.
  • Game
    • AndEngine
      • Free Android 2D OpenGL Game Engine
    • libgdx
      • Android/desktop game development framework



2011年9月28日水曜日

Android からApp EngineのフォームにPOSTする

「Google アカウントを使用して Google App Engine の認証を行う」の続きです。

Google App Engine のスタートガイドで作成しているゲストブックのフォームにポストするサンプルです。



以前のソースはActivityクラスにコードを記載してましたが、もう少し汎用的なクラスを作成しました。

GoogleServiceAuthenticator
public class GoogleServiceAuthenticator {
public static final String ACCOUNT_TYPE = "com.google";
public enum GOOGLE_ACCOUNT_TYPE {
APPENGINE { public String toString() { return "ah"; } },
}
public interface PostExecuteCallback {
void run(String acsid);
}
private Context context;
private AccountManager accountManager;
private String hostname;
private String appPath;
private PostExecuteCallback postExecuteCallback;
private DefaultHttpClient httpClient = new DefaultHttpClient();
public GoogleServiceAuthenticator(Context context) {
this.context = context;
}
public Account[] getGoogleAccounts() {
if (accountManager == null) {
accountManager = AccountManager.get(context);
}
return accountManager.getAccountsByType(ACCOUNT_TYPE);
}
public void execute(Account account, GOOGLE_ACCOUNT_TYPE type, PostExecuteCallback postExecuteCallback) throws Exception {
if (hostname == null) {
throw new Exception("hostname must not be null");
}
if (appPath == null) {
throw new Exception("appPath must not be null");
}
this.postExecuteCallback = postExecuteCallback;
// Gets an auth token of the specified type.
accountManager.getAuthToken(
account, // The account to fetch an auth token for
type.toString(), // The auth token type, an authenticator-dependent string token, must not be null
false, // True to add a notification to prompt the user for a password if necessary, false to leave that to the caller
new GetAuthTokenCallback(), // Callback to invoke when the request completes, null for no callback
null // Handler identifying the callback thread, null for the main thread
);
}
public void setHostname(String hostname) {
this.hostname = hostname;
}
public void setAppPath(String appPath) {
this.appPath = appPath;
}
private String getLoginUrl(String hostname, String appPath, String authToken) {
return "https://" + hostname + "/_ah/login?continue=" + appPath + "&auth=" + authToken;
}
private class GetAuthTokenCallback implements AccountManagerCallback<Bundle> {
@Override
public void run(AccountManagerFuture<Bundle> result) {
Bundle bundle;
try {
bundle = result.getResult();
Intent intent = (Intent)bundle.get(AccountManager.KEY_INTENT);
if(intent != null) {
// User input required
context.startActivity(intent);
} else {
String acsid = getAuthToken(bundle);
// If authentication succeeds and gets the SACSID/ACSID, the post-process is called.
if (acsid != null) {
if (postExecuteCallback != null) {
postExecuteCallback.run(acsid);
}
}
}
} catch (OperationCanceledException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (AuthenticatorException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
};
private String getAuthToken(Bundle bundle) {
boolean validated = false;
int count = 3;
try {
while (!validated) {
String authToken = bundle.getString(AccountManager.KEY_AUTHTOKEN);
httpClient.getParams().setBooleanParameter(ClientPNames.HANDLE_REDIRECTS, false);
String uri = getLoginUrl(hostname, appPath, authToken);
HttpGet httpGet = new HttpGet(uri);
HttpResponse httpResponse = httpClient.execute(httpGet);
int status = httpResponse.getStatusLine().getStatusCode();
if (status == HttpStatus.SC_INTERNAL_SERVER_ERROR) {
// Authenticate error (500)
try {
StringBuilder buf = new StringBuilder();
buf.append(String.format("Status:%d ", status));
InputStream in = httpResponse.getEntity().getContent();
BufferedReader reader = new BufferedReader(new InputStreamReader(in));
String l = null;
while ((l = reader.readLine()) != null) {
buf.append(l + "\n");
}
Log.w(TAG, buf.toString());
} catch (Exception e) {
}
// Removes an auth token from the AccountManager's cache.
String accountType = bundle.getString(AccountManager.KEY_ACCOUNT_TYPE);
accountManager.invalidateAuthToken(accountType, authToken);
} else {
// Authenticate success
validated = true;
break;
}
// retry count down
if (0 < count--) {
break;
}
}
// If authentication succeeds, get the SACSID/ACSID from the Cookie
if (validated) {
for (Cookie cookie : httpClient.getCookieStore().getCookies()) {
if ("SACSID".equals(cookie.getName()) || "ACSID".equals(cookie.getName())) {
return cookie.getName() + "=" + cookie.getValue();
}
}
}
} catch (ClientProtocolException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return null;
}
}


  • コンストラクタにはContextを指定してます。
  • getGoogleAccounts() で、アカウントリストを取得できます。
  • execute()で、認証を実行してます。
    • 引数 accountには、リストから選ばられた一意のアカウントを指定します。typeは、Googleのアカウントタイプ(AppEngineの場合は"ah")。postExecuteCallbackには、認証終了後にコールしたいクラスを指定します。
    • AccountManagerのgetAuthTokenを呼び出します。
      • GetAuthTokenCallbackをcallbackに指定してます。
      • GetAuthTokenCallbackのrun()の認証処理の実態のgetAuthToken()を呼び出してます。
    • getAuthToken()では、認証処理を失敗時のリトライのためにループさせてます。
      • getLoginUrl()を呼び出して、アプリのUrlを作成。
        • https://yourappl.appspot.com/_ah/login?continue=http://localhost/&auth=[authToken]
        • authTokenは、AccountManagerから取得した文字列を指定してます。
      • urlを読んで、レスポンスを取得。
      • 取得したレスポンスが500の場合、AccountManagerのキャッシュからauthTokenを削除して、リトライをさせてます。
      • 認証に成功したら、Cookieから、ASCID または、SACSIDを取得します。
        • httpsで認証した場合は、SACSID、httpの場合は、ASCIDが取得できます。
    • その後、postExecuteCallbackを実行させてます。
      • postExecuteCallback内で、App Engineにアクセスするときに、Http Headerの CookieにASCID/SACSIDをセットする必要があります。


Activityの実装例

GoogleServiceAuthExampleActivity
public class GoogleServiceAuthExampleActivity extends ListActivity {
private static final String TAG = GoogleServiceAuthExampleActivity.class.getName();
GoogleServiceAuthenticator authenticator;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
authenticator = new GoogleServiceAuthenticator(this);
Account[] accounts = authenticator.getGoogleAccounts();
this.setListAdapter(new ArrayAdapter<Account>(this, android.R.layout.simple_list_item_1, accounts));
}
@Override
protected void onListItemClick(ListView l, View v, int position, long id) {
Account account = (Account)getListView().getItemAtPosition(position);
authenticator.setHostname("yourappl.appspot.com");
authenticator.setAppPath("http://localhost/");
try {
authenticator.execute(account, GOOGLE_ACCOUNT_TYPE.APPENGINE,
new GoogleServiceAuthenticator.PostExecuteCallback() {
@Override
public void run(String acsid) {
DefaultHttpClient httpClient = new DefaultHttpClient();
HttpPost httpPost = new HttpPost("http://yourappl.appspot.com/sign");
HttpResponse httpResponse = null;
try {
List<BasicNameValuePair> parms = new ArrayList<BasicNameValuePair>();
parms.add(new BasicNameValuePair("content", "InputData=" + new SimpleDateFormat().format(new Date()) ));
httpPost.setEntity(new UrlEncodedFormEntity(parms, HTTP.UTF_8));
httpPost.setHeader("Cookie", acsid);
httpResponse = httpClient.execute(httpPost);
} catch (UnsupportedEncodingException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (ClientProtocolException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
if (httpResponse != null) {
int status = httpResponse.getStatusLine().getStatusCode();
StringBuilder buf = new StringBuilder();
buf.append(String.format("status:%d", status));
try {
InputStream in = httpResponse.getEntity().getContent();
BufferedReader reader = new BufferedReader(new InputStreamReader(in));
String l = null;
while((l = reader.readLine()) != null) {
buf.append(l + "\n");
}
if (status != HttpStatus.SC_OK) {
Log.e(TAG, buf.toString());
}
} catch(Exception e) {
e.printStackTrace();
}
(Toast.makeText(
GoogleServiceAuthExampleActivity.this,
buf.toString(),
Toast.LENGTH_LONG)).show();
Log.d(TAG, buf.toString());
}
}
});
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}


フルプロジェクトはgithubに上げました。
http://github.com/granoeste/GoogleServiceAuthExample

Google アカウントを使用して Google App Engine の認証を行う

AccountManager から、端末に登録されているGoogleアカウントを取得して、
そのアカウントでGoogle App Engineの認証を行う簡単なサンプルです。


AndroidManifest.xmlは、次のパーミッションを指定します。

  • android.permission.GET_ACCOUNTS
  • android.permission.MANAGE_ACCOUNTS
  • android.permission.USE_CREDENTIALS

  1. <!--xml version="1.0" encoding="utf-8"?-->  
  2. <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="net.granoeste.creador.AuthenticatorTest" android:versioncode="1" android:versionname="1.0">  
  3.   <uses-sdk android:minsdkversion="7">  
  4.   
  5.   <uses-permission android:name="android.permission.GET_ACCOUNTS"></uses-permission>  
  6.   <uses-permission android:name="android.permission.MANAGE_ACCOUNTS"></uses-permission>  
  7.   <uses-permission android:name="android.permission.USE_CREDENTIALS"></uses-permission>  
  8.   
  9.   <application android:icon="@drawable/icon" android:label="@string/app_name">  
  10.     <activity android:label="@string/app_name" android:name="AccountList">  
  11.       <intent-filter>  
  12.         <action android:name="android.intent.action.MAIN">  
  13.         <category android:name="android.intent.category.LAUNCHER">  
  14.       </category></action></intent-filter>  
  15.     </activity>  
  16.   
  17.   </application>  
  18. </uses-sdk></manifest>  



Activity


AccountManagerから、AccountsByTypeに"com.google"を指定して、Googleアカウントの一覧を取得します。
それをリスト表示してます。
※リストは簡易的に標準のレイアウトを使用してます。

  1. public class AccountList extends ListActivity {  
  2.     protected AccountManager accountManager;  
  3.     protected Intent intent;  
  4.   
  5.     /** Called when the activity is first created. */  
  6.     @Override  
  7.     public void onCreate(Bundle savedInstanceState) {  
  8.         super.onCreate(savedInstanceState);  
  9.         accountManager = AccountManager.get(getApplicationContext());  
  10.         Account[] accounts = accountManager.getAccountsByType("com.google");  
  11.         this.setListAdapter(new ArrayAdapter<account>(  
  12.                          this, android.R.layout.simple_list_item_1, accounts));  
  13.     }  
  14.   
  15.     @Override  
  16.     protected void onListItemClick(ListView l, View v, int position, long id) {  
  17.         Account account = (Account)getListView().getItemAtPosition(position);  
  18.         AccountManagerFuture<bundle> accountManagerFuture =   
  19.             accountManager.getAuthToken(account, "ah"nullthisnullnull);  
  20.         Bundle authTokenBundle;  
  21.         try {  
  22.             authTokenBundle = accountManagerFuture.getResult();  
  23.             String authToken = authTokenBundle.get(  
  24.                                    AccountManager.KEY_AUTHTOKEN).toString();  
  25.             Log.i(TAG,"authToken:"+authToken);  
  26.   
  27.         } catch (OperationCanceledException e) {  
  28.             // TODO Auto-generated catch block  
  29.             e.printStackTrace();  
  30.         } catch (AuthenticatorException e) {  
  31.             // TODO Auto-generated catch block  
  32.             e.printStackTrace();  
  33.         } catch (IOException e) {  
  34.             // TODO Auto-generated catch block  
  35.             e.printStackTrace();  
  36.         }  
  37.     }  
  38. }  
  39. </bundle></account>  

リストで選んだAccountでGoogle App Engineの認証を行ってます。
AccountManager#getAuthToken() の 第2引数に AuthTokenTypeを指定します。
"ah"がGoogle App EngineのAuthTokenTypeとなってます。

AuthTokenTypeについては、次のサイトが参考になると思います。
  Account Managerについて - adsaria mood
  Google Data APIs Frequently Asked Questions - Google Base Data API - Google Code

認証画面が表示されます。

ユーザがAllowを選択するとauthTokenを取得することができます。


2011年9月19日月曜日

外部メディア (SDカード) へのインストール不可にする


Android 2.2.x (Froyo) から、アプリケーションは内蔵ストレージ(携帯端末)だけでなく、外部メディア(SDカード) にインストールさせることが可能になってます。

端末によっては、内蔵ストレージは、通常数百MBとサイズが小さく、プリインストールのアプリケーションなどで、ユーザが使用できるサイズ100MB以下の場合があります。
その場合、ゲームなどサイズの大きいアプリケーションは、2GB-32GBとサイズの大きい外部メディアにインストールさせることで、内蔵ストレージの消費を抑えることができます。

しかし、常に常駐するタイプのものや、ホームに配置するウィジェットの場合、外部メディアにインストールされてしまうと問題が発生します。

外部メディアは、端末から取り外したり、USBでパソコンと接続した場合にアンマウントされてしまいます。アンマウントされてしまうと、端末から外部メディアが認識できなくなり、その上で動作していたアプリケーションも停止することなります。

この問題を防ぐために、アプリケーションの開発時に外部メディアに移動できないように設定することが可能です。


AndroidManifest.xmlの<manifest>タグに、android:installLocation属性を定義します。
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="net.granoeste.creador.InternalOnlyAppWidget"
android:versionCode="1"
android:versionName="1.0"
android:installLocation="internalOnly"
>
</manifest>
"internalOnly"と定義することで、外部メディアへの移動が出来なくなります。

android:installLocation属性の詳細はDev Guideを参照してください。
<manifest> | Android Developers

android:installLocation属性には、他に"auto"と"preferExternal"が設定することが可能です。
auto - アプリケーションは、外部ストレージにインストールすることができますが、システムはデフォルトで内部ストレージにアプリケーションをインストールします。内部ストレージがいっぱいになっている場合、システムは外部ストレージにインストールします。
preferExternal - アプリケーションは、外部ストレージ(SDカード)にインストールされることを優先します。


Android 2.1.x (Eclar) までは、外部メディアへのインストールができないため、android:installLocation属性はありませんので、通常のインストール先は内蔵ストレージとなります。
Android 2.2.x (Froyo)でも、android:installLocation属性を設定していない場合、通常のインストール先は内蔵ストレージとなります。

通常の端末の状態では、問題はありませんが、アプリケーションの通常のインストール先を内蔵ストレージから外部ストレージに変更することが出来てしまいます。

USB接続して、次のコマンドを実行します。

adb shell pm setInstallLocation 2
コマンドを実行することで、アプリケーションの通常のインストール先を外部メディアに変更します。

このような場合、android:installLocation属性に"internalOnly"を指定してない場合、外部メディアにインストールされてしまうことになります。
ウィジェットの場合、外部メディアにインストールされると、ホーム画面でウィジェットの一覧に表示されなくなります。


なので、
ウィジェットや常駐アプリは android:installLocation属性に "internalOnly" を指定しましょう!!

Android 2.1 以下を対象とするアプリケーションの場合、プロジェクトのビルドターゲットにAndroid 2.2 以上を指定して、AndroidManifest.xmlにuses-sdkタグでターゲットバージョンを指定します。

2011年8月22日月曜日

[AndroidFace Ver.1.3.2] リリース

[AndroidFace Lite Ver.1.3.2] をリリースしました。
* 通知(ステータスバー)にバッテリーの数値を表示する機能の追加
* 電波強度の強弱の閾値の変更値の修正


[AndroidFace Lite Ver.1.3.1] もリリースしてました。
* au CDMA2000(EVO)対応
* 動作条件 2.1 に引き上げ
* 電波強度をウィジェットに表示するオプションを追加
* 電波強度の強弱の閾値を変更できる機能を追加
* 機能のリファイン
* いくつかのバグ修正

2011年8月7日日曜日

日本の伝統色 (ライブ壁紙) リリース

日本の伝統色 (ライブ壁紙) リリースしました。

伝統的な日本の色 のライブ壁紙
30分ごとか画面操作時に、ランダムで壁紙の色が変わります。

色の種類は、500色近くあります。
通常のアプリとして起動することもできます。
画面をタッチすることで、次の色に変えることもできます。
また、メニューから一覧をすることも可能です。


マーケットリンク
http://t.co/oQ3yQsb

BurgerTimer リリース

ちょっと変わったキッチンタイマーアプリです。
ちょっとファジーなのはご愛嬌。

1) タイマー設定画面で、針をまわしてタイマーを設定します。
2) スタートすると、設定した時間で、ハンバーガーが作成されていきます。
 途中、変な具材が混ざることがあります。そのときは、端末を振ると捨てることができます。
3) 時間になるとハンバーガーが完成し、アラームが鳴ります。


2011/07/23 に開催されました「Androidアプリケーション、デザイナとプログラマのハッカソン」にて、
私が所属した「チームめんたいこ」で作成したアプリです。

スタッフ:
 デザイナー:kawacocoさん Matsu11さん _erisan_さん
 プログラマ:openkakuさん katsummy(私)

1日で、なんとか仕上げることができ、参加者投票で優勝を頂けました。
せっかくなので、ブラッシュアップして、リリースさせて頂きました。

http://atnd.org/events/17260
#dpthon

ちょっとした小ネタアプリですがダウンロードしてみて下さい。

アプリリンク
http://t.co/2D2Tdrt

ハッカソンの発表資料

2011年6月28日火曜日

Portable Wi-Fi Monitor Ver 1.3 アップデート

Portable Wi-Fi Monitor Ver 1.3 にアップデート
http://ow.ly/5r2ar

コメントで頂いていた要望に対応しました。
設定画面に以下の項目が追加されています。

・ステータスバー表示/非表示オプション
 通知エリアのアイコンを表示しない設定です。
 但し、バッテリー残量が閾値を下回ったときの通知は表示されます。
・バッテリーアイコン表示オプション
 バッテリー型のアイコンを表示する設定です。
 設定しない場合は、Portable Wi-Fiのアイコンが表示されます。
・バッテリーレベル表示オプション
 バッテリーレベルを表示する設定です。
 100, 90, 80, , , , 30, 20, 10, 5 単位に数値を表示します。
 正確な値はルーター画面かウィジェットで確認して下さい。
・ウィジェットで、バッテリーレベルが 20%以下の場合、赤字で数値を表示するようにしました。
・アプリアイコンをちょっと変えました。

2011年6月25日土曜日

AlarmManagerで定期的に繰り返しサービスを実行する

一定時間に処理を実行したり、定期的に処理を繰り返すにはAlarmManagerを使用します。
今回は、AlarmManagerで定期的にServiceを実行するメモです。


AlarmManager でサービスを登録
  1. ServiceのIntentを作成します。
  2. Serviceを開始するPendingIntentを取得します。
  3. AlarmManager#setInexactRepeatingでアラームをスケジュールします。
Intent serviceIntent = new Intent(this, MyService.class);
PendingIntent pendingIntent
= PendingIntent.getService(this, 0, serviceIntent, PendingIntent.FLAG_UPDATE_CURRENT);
AlarmManager am = (AlarmManager) getSystemService(ALARM_SERVICE);
am.setInexactRepeating (
AlarmManager.RTC,
System.currentTimeMillis(),
//AlarmManager.INTERVAL_HOUR,
AlarmManager.INTERVAL_FIFTEEN_MINUTES,
pendingIntent);

AlarmManager.RTC - wall-clock timew in UTC 要は現実世界での経過時間
AlarmManager.INTERVAL_FIFTEEN_MINUTES - 15分(900000秒) Intervalの定数


AlarmManager でサービスを解除
Intent serviceIntent = new Intent(this, MyService.class);
PendingIntent pendingIntent
= PendingIntent.getService(this, 0, serviceIntent, PendingIntent.FLAG_UPDATE_CURRENT);
AlarmManager am = (AlarmManager) getSystemService(ALARM_SERVICE);
am.cancel(pendingIntent);


adb コマンドで、AlarmManagerの登録状態を確認
# adb shell dumpsys alarm
Current Alarm Manager state:
Realtime wakeup (now=1308812542789):
RTC #2: Alarm{487c9590 type 1 net.granoeste.example.repeatedlyrunning}
type=1 when=1308813378896 repeatInterval=900000 count=1
operation=PendingIntent{487f4ac0: PendingIntentRecord{487bb340 net.granoeste.example.repeatedlyrunning startService}}
Elapsed realtime wakeup (now=620467383):
Broadcast ref count: 0
Alarm Stats:
net.granoeste.example.repeatedlyrunning
44ms running, 0 wakeups
1 alarms: flg=0x4 cmp=net.granoeste.example.repeatedlyrunning/.MyService
view raw dumpsys alarm hosted with ❤ by GitHub

2011年6月18日土曜日

HandlerThreadで、LooperとHandlerを使用する方法

メッセージキューの仕組みを実装しようとして、LooperやThread、Handlerを調べていたら、
HandlerThreadという便利なクラスがあったのでメモ

HandlerThread の概要
Handy class for starting a new thread that has a looper. The looper can then be used to create handler classes. Note that start() must still be called.

ルーパーを持つ新しいスレッドを開始するための便利なクラス。ルーパーは、ハンドラクラスを作成するために使用することができます。 start()を呼び出す必要があることに注意してください。

HandlerThreadはThreadを継承していて、内部にLooperを保持しています。
そのLooperを使って、Handlerのインスタンスを作成します。

android.app.IntentService のソースを参考にコードを記述してみました。

// With reference to the android.app.IntentService of the Android framework
// Member variable
private volatile Looper mServiceLooper;
private volatile ServiceHandler mServiceHandler;
// Handler Class
private final class ServiceHandler extends Handler {
public ServiceHandler(Looper looper) {
super(looper);
}
@Override
public void handleMessage(Message msg) {
// TODO
// Process the received message.
}
}
// Create the HandlerThread, and start
// Get the looper from the HandlerThread to the constructor of the Handler it.
public void onYourCreateMethod() {
HandlerThread thread = new HandlerThread("<Thread Name>");
thread.start();
mServiceLooper = thread.getLooper();
mServiceHandler = new ServiceHandler(mServiceLooper);
}
// Send a message using the Handler was created.
public void onYourMethod(Intent intent) {
Message msg = mServiceHandler.obtainMessage();
msg.what = <What>;
msg.obj = <Object>;
mServiceHandler.sendMessage(msg);
}
// To terminate the Handler
public void onYourDestroyMethod(Intent intent) {
mServiceLooper.quit();
}
view raw gistfile1.java hosted with ❤ by GitHub



参考サイト

2011年6月16日木曜日

2011年1月23日日曜日

[AndroidFace Ver.1.3] リリース

 [AndroidFace Lite Ver.1.3] をリリースしました。
 * 追加ファイル対応 (Faceパターンをダウンロードして追加できます。)
 表情のプレビュー機能追加
 shibainuの表情が、電波強度とは逆に反応していたのを修正



追加のFaceパターンファイルは、設定画面の"Faceパターンを取得する"で、検索することができます。
もしくは、マーケットで "AndroidFace"を検索します。

今回のリリースでは、以下の追加ファイルが公開されています。
- AndroidFace FriendlyFire
- AndroidFace ウサギ   * 兎年ですから!!