2014年11月25日火曜日

Android : Google Drive 上のファイルの作成・読み込み(一般フォルダ編)


このエントリーをはてなブックマークに追加
Google Drive 上のファイルとして見えるファイルを作成、読み込みするアプリを開発する方法のメモです。
共通する部分は多数ありますが、アプリケーション専用フォルダ(APP_FOLDER)ではありません。
また、ファイルの編集(すでにあるファイルを読み込み、その内容を変えて保存、というのもこの記事では扱っていません。


準備1. サービスの作成、API の有効化

Set up the game in the Google Play Developer Console を参考にゲームサービスを作成し、作成したプロジェクトの Google Developers Console で Drive API を有効にします。


準備2. GoogleApiClient の作成

まずは GoogleApiClient を作成し、connect を呼んでログインしておく必要があります。
ここで必要な API は Drive.API, Scope は Drive.SCOPE_FILE です。
(ちなみにアプリケーション専用フォルダへの保存の場合はスコープに SCOPE_APPFOLDER が必要になります)
また、以降サンプル内で定義されていない変数・定数についてはクラスのメンバ変数などとしてどこかで定義されていると思ってください。
mGoogleApiClient = new GoogleApiClient.Builder(this)
        .addConnectionCallbacks(new GoogleApiClient.ConnectionCallbacks() {
            @Override
            public void onConnected(Bundle bundle) {
                Log.i(LOGTAG, "onConnected");
            }

            @Override
            public void onConnectionSuspended(int i) {
                Log.i(LOGTAG, "onConnectionSuspended:" + i);
            }
        })
        .addOnConnectionFailedListener(new GoogleApiClient.OnConnectionFailedListener() {
            @Override
            public void onConnectionFailed(ConnectionResult connectionResult) {
                Log.i(LOGTAG, "onConnectionFailed:" + connectionResult.toString());
                if(connectionResult.hasResolution()) {
                    try {
                        // 初回起動時などで未認証の場合などはこちらにきます
                        connectionResult.startResolutionForResult(MainActivity.this, GOOGLE_API_CLIENT_RESOLUTION);
                    } catch (Exception e) {
                        Log.e(LOGTAG, e.toString());
                    }
                } else{
                    Log.e(LOGTAG, "hasNoResolution()");
                    int errorCode = connectionResult.getErrorCode();
                    Dialog dialog = GooglePlayServicesUtil.getErrorDialog(errorCode, MainActivity.this, GOOGLE_API_CLIENT_ERROR);
                    if( dialog != null){
                        dialog.show();
                    } else {
                        Log.e(LOGTAG, "no dialog");
                    }
                }
            }
        })
        .addApi(Drive.API).addScope(Drive.SCOPE_FILE)
        .build();
mGoogleApiClient.connect();
startResolutionForResult で OK が返ってきた時に再接続を試みるために onActivityResult に以下のようなコードを書き加えておきます。
※ startResolutionForResult は Activity から startActivity される扱いになるので、Activity の onActivityResult で処理必要があります。Fragment 内から呼んで、Fragment の onActivityResult で処理したい場合は、Activity の onActivityResult からその Fragment の onActivityResult を呼ぶよう明示的にコードを書く必要があります(他の Activity 起動系 API も同様です)。
if(requestCode == GOOGLE_API_CLIENT_RESOLUTION){
    switch (resultCode) {
        case Activity.RESULT_OK:
            mGoogleApiClient.connect();
            break;
        default:
            Log.e(LOGTAG, "" + resultCode);
    }
}


ファイルの作成

DriveApi.newDriveContents に設定したコールバック関数内で、DriveApi.newCreateFileAcitivtyBuilder() にメタデータや初期データを設定、アクティビティを呼ぶ、という流れです。

ResultCallback<DriveApi.DriveContentsResult> newDriveContentsCallback = new
    ResultCallback<DriveApi.DriveContentsResult>() {
        @Override
        public void onResult(DriveApi.DriveContentsResult result) {
            // MimeType は適宜変更する。他にも必要な項目があれば set 関数で set する。
            // setTitle で初期タイトルくらいは決めてあげると良さそう
            MetadataChangeSet metadataChangeSet = new MetadataChangeSet.Builder()
                    .setMimeType("text/plain").build();

            DriveContents contents = result.getDriveContents();
            // OutputStream を取得して初期データを書き込む。例として、適当な文字列を書き込む。
            try {
                OutputStream os = contents.getOutputStream();
                String hello = "Hello World!";
                os.write(hello.getBytes());
                os.close();
            } catch (Exception e){
                Log.e(LOGTAG, e.toString());
            }

            // デフォルトで用意された UI を表示
            IntentSender intentSender = Drive.DriveApi
                    .newCreateFileActivityBuilder()
                    .setInitialMetadata(metadataChangeSet)
                    .setInitialDriveContents(contents)
                    .build(mGoogleApiClient);
            try {
                startIntentSenderForResult(
                        intentSender, REQUEST_CODE_CREATOR, null, 0, 0, 0);
            } catch (IntentSender.SendIntentException e) {
                Log.w(LOGTAG, "Unable to send intent", e);
            }
        }
    };

Drive.DriveApi.newDriveContents(mGoogleApiClient).setResultCallback(newDriveContentsCallback);

onActivityResult に以下のようなコードを書いておくと作成された Google Drive ファイルの DriveId が取得可能です(作りっぱなしでよいなら取得する必要はありません)。
if(requestCode == REQUEST_CODE_CREATOR){
    Log.i(LOGTAG,"REQUEST_CODE_CREATOR:" + resultCode);
    if(resultCode == RESULT_OK) {
        DriveId driveId = data.getParcelableExtra(CreateFileActivityBuilder.EXTRA_RESPONSE_DRIVE_ID);
        Log.i(LOGTAG, "Drive Id:" + driveId.toString());
    }
}


ファイルの読み込み

DriveApi.newOpenFileActivityBuilder() を使用してアクティビティを呼び出し、onActivityResult で DriveId を取得、DriveId からファイルを開いて内容を読み込むという流れです。
まずはファイル選択ダイアログの呼び出し。
// MimeType でフィルタリングされる
IntentSender intentSender = Drive.DriveApi
        .newOpenFileActivityBuilder()
        .setMimeType(new String[] { "text/plain" })
        .build(mGoogleApiClient);
try {
    startIntentSenderForResult(
            intentSender, REQUEST_CODE_OPENER, null, 0, 0, 0);
} catch (IntentSender.SendIntentException e) {
    Log.w(LOGTAG, "Unable to send intent", e);
}

以下のコードは onActivityResult 内という想定です。DriveId からファイル内容を取得します。
if(requestCode == REQUEST_CODE_OPENER){
    if(resultCode == RESULT_OK){
        DriveId driveId = data.getParcelableExtra(
                OpenFileActivityBuilder.EXTRA_RESPONSE_DRIVE_ID);

        // 取得した driveId からファイルの内容を取得。
        // プログレスを取得して何か行う場合は file.open の第三引数にリスナーを設定する。
        DriveFile file = Drive.DriveApi.getFile(mGoogleApiClient, driveId);
        file.open(mGoogleApiClient, DriveFile.MODE_READ_ONLY, null).setResultCallback(new ResultCallback<DriveApi.DriveContentsResult>() {
            @Override
            public void onResult(DriveApi.DriveContentsResult driveContentsResult) {
                if(driveContentsResult.getStatus().isSuccess()){
                    DriveContents driveContents = driveContentsResult.getDriveContents();
                    // DriveContents の getInputStream で内容を取得する。
                    // 以下はファイルの内容がテキストであることを想定した例。
                    BufferedReader reader = new BufferedReader(
                            new InputStreamReader(driveContents.getInputStream()));
                    StringBuilder builder = new StringBuilder();
                    String contents = null;
                    String line;
                    try {
                        while ((line = reader.readLine()) != null) {
                            builder.append(line);
                        }
                        contents = builder.toString();
                    } catch (IOException e) {
                        Log.e(LOGTAG, "IOException while reading from the stream", e);
                    }
                    // 読み込むだけで内容に変更はなし。discard を呼んで閉じておく(意味は無いかも?)
                    driveContents.discard(mGoogleApiClient);

                    Toast.makeText(MainActivity.this, contents, Toast.LENGTH_LONG).show();
                }
            }
        });
    }
}


ちなみに、認証画面をもう一度出したい場合などで、認証を取り消したいときは このリンク先からアプリを 選択してアクセス権を取り消します。

参考
googledrive/android-demos
com.google.android.gms.drive
Google Drive Android API : Create files
Google Drive Android API : Working with File Contents

2014年11月23日日曜日

『Being Mortal』 感想とレビュー


このエントリーをはてなブックマークに追加



Being Mortal: Medicine and What Matters in the End』を読みました。
"Old age is continuous series of losses."
この一文の引用だけで若干憂鬱になりますが、いつかは死ぬ、immortal(不死)ではない人間として、どうやって最後の時を生きて、どうやって死を迎えようか、というちょっと重い本です。

医療施設の話、筆者が関わった患者の話、筆者自身の父親の話などを通して、自分の親だったら、自分自身だったら、といろいろなことを考えさせられました。

まだまだ考えはまとまりませんが、読んでみて良かったです。
あまり上手くまとめられる気がしないので、内容については特に気に留めている点に触れるだけにとどめておきます。本では、他にも、緩和医療、医療コスト、コミュニティなどいろいろな話題に触れています。

安全と長生きが最優先事項か


病院や施設は、お年寄りの安全(と組織としての効率)を第一に考えた運営がなされているが、それが当人達にとって最善なのか、プライバシーも生きる目的も奪われた状態で管理されて長生きすることが幸せなのか、という問題提起。
"It just isn't home"

At home, you decide how you spend your time, how you share your space, and how you manage your possessions. Away from home, you don't.

治療とリスク、トレードオフ


医療技術は大きく発展したが、それでも治療できない、もしくは治療できる可能性が低い病気はある。また、治療行為にはリスクや副作用も存在する。難しいことではあるが、自分(もしくは家族)が何を大事に思うかと照らしあわせて、どのような可能性のために、どのようなリスクなら受け入れられるか、を考える必要がある、という問いかけ。
What are your fears and what are your hopes? What are the trade-offs you are willing to make and not willing to make?

2014年11月21日金曜日

Android : Saved Games を使う


このエントリーをはてなブックマークに追加
Saved Games in Android 参照。
便利だけど、ログイン時の許可リクエスト量が多いのが難点。
  • Google Play Game Services : Getting Started for Android Game Development の Step 2 に沿って Google Play Services の設定(後で Internal Error が起こらないようにこの辺りに注意)
  • 作成したプロジェクトの Google Developers Console で Drive API、Google Play Game Management、Google Play Game Services をオンにする
  • GoogleApiClient を作るときは addApi(Games.API).addScope(Games.SCOPE_GAMES) と addApi(Drive.API).addScope(Drive.SCOPE_APPFOLDER) が必要?(コード後述)
    • 足りないと実行時に "appropriate api was not requested." というエラーでる
    • Games.API を指定するとログイン時に Google+ のプロフィールアクセスのような、ただデータを保存したい、という用途には必要のない許可まで求めてしまうが嫌だけど、削れそうにないか…。
  • Google play Developer Console のゲームサービス、「ゲームの詳細」で「保存済みゲーム」をオンにする(Saved Games の訳が保存済みゲーム…?)
    • オンにしていないと実行時に "Cannot use snapshots without enabling the 'Saved Game' feature in the Play console" というエラーが出る
  • Snapshot 新規作成時に snapshot.readFully() を呼ぶと長さ 0 の byte 配列が返る(null ではないので注意)
  • SnapshotMetadataChange の setCoverImage はしなくても大丈夫(デフォルトのコントローラの画像が表示される)
  • デバッグのためなどで、一回認証を解除して再度ログイン画面を出すには Google+ のアプリ設定 から接続を解除する
    • 他に方法はある?

GoogleApiClient 作成のコード例

mGoogleApiClient = new GoogleApiClient.Builder(this)
        .addConnectionCallbacks(new GoogleApiClient.ConnectionCallbacks() {
            @Override
            public void onConnected(Bundle bundle) {
                Log.i(LOGTAG, "onConnected");
            }

            @Override
            public void onConnectionSuspended(int i) {
                Log.w(LOGTAG, "onConnectionSuspended:" + i);
            }
        })
        .addOnConnectionFailedListener(new GoogleApiClient.OnConnectionFailedListener() {
            @Override
            public void onConnectionFailed(ConnectionResult connectionResult) {
                Log.e(LOGTAG, "onConnectionFailed:" + connectionResult.toString());
                if(connectionResult.hasResolution()) {
                    try {
                        connectionResult.startResolutionForResult(MainActivity.this, REQUEST_CODE_GOOGLE_API_CLIENT_RESOLUTION);
                    } catch (Exception e) {
                        Log.e(LOGTAG, e.toString());
                    }
                } else{
                    Log.e(LOGTAG, "hasNoResolution()");
                    int errorCode = connectionResult.getErrorCode();
                    Dialog dialog = GooglePlayServicesUtil.getErrorDialog(errorCode, MainActivity.this, REQUEST_CODE_GOOGLE_API_CLIENT_ERROR);
                    if( dialog != null){
                        dialog.show();
                    } else {
                        Log.e(LOGTAG, "no dialog");
                    }
                }
            }
        })
        .addApi(Games.API).addScope(Games.SCOPE_GAMES)
        .addApi(Drive.API).addScope(Drive.SCOPE_APPFOLDER)
        .build();
mGoogleApiClient.connect();

Android : GoogleApiClient (Google Play Services) で発生する Internal Error の原因


このエントリーをはてなブックマークに追加
いろいろ調べた挙句、自分の場合は「Google Play Developer Console で設定したパッケージ名とアプリのパッケージ名が違う」でした。せっかくなので、それも含めてありえる原因をまとめておきます。

  • AndroidManifest.xml に指定した meta-data の com.google.android.gms.games.APP_ID が Google Play Developer Console のリンク済みアプリに記載されたアプリID とは異なる
  • Google Developer Console (API のオンオフを設定したりする方)に入力した SHA1 の値が間違っている
    • ビルド時と異なる keystore ファイルから SHA1 を取得していた、など。
    • デバッグ版の場合は、debug.keystore がある場所(Windows の場合は通常 C:\Users\%ユーザ名%\.android )で以下のコマンド。keytool は java の bin 以下にあるのでパスを通しておく。
      keytool -exportcert -alias androiddebugkey -keystore debug.keystore -list -v
  • Google Developer Console で同意画面を作成していない(「API と認証」セクション)
  • GoogleApiClient の setScope で設定するスコープが足りない
  • Google Play Developer Console で設定したパッケージ名と実際のアプリのパッケージ名が異なる
    • AndroidStudio の場合、build.gradle の defaultConfig の applicationId を変える必要あり(ここを見落としていて、src の方のディレクトリ構造とパッケージ名や AndroidManifest.xml のパッケージ名を変更していた場合も Internal Error でした。)

凡ミスには気をつけたいものです。。。

2014年11月15日土曜日

『ZERO to ONE』 感想とレビュー


このエントリーをはてなブックマークに追加



ゼロ・トゥ・ワン 君はゼロから何を生み出せるか』読了しました。

自分なりに本書の主張を箇条書きでまとめると、以下の項目になります。
  • 「リーン」に代表されるような、ユーザの声を聞いて少しずつ改善していくという手法では真に革新的なことはできない。自分なりのビジョンと計画を持って大きく賭けろ
    • 例えば Apple の製品はジョブズのビジョンの結晶である。
  • 競争下では最終的に全体の収益が消失する。他ができないことを行う、という意味での独占は善である
    • 独占しているかしていないか、独自性はあるか、ということを見極める上で、競争範囲の定義は重要である。範囲を狭く定義して独占を主張しても意味が無い。
  • 販売・売り込みは必要。方法には考えが必要。
    • 「あの会社はそんなことせずに成功したじゃないか」と思うのであれば、優れた売り込みに気づいていない。
  • 良い未来は創れるという希望を持ち、自分の頭で考えよう

この辺りの主張については、本書を読み進める中で、十分な説明やエピソードが紹介されており、なるほどと思え納得はしており、それは興味深く読めたのですが、「さてではどうするか」、というのが難しいですね。結局のところ「新しいことを自分の頭で考えよう」の一言に尽きる気はしています。

一人の会社員、社会人としての行動指針に落としこむと

  • 「こうして欲しい」という要望を待たずに「こうすれば良いのは」と提案できるように考える
  • 既存のプロセスを鵜呑みにせず、本質的な問題をより効率よく解決するために他の方法がないか考える
  • (独占的な)強みとなる技術を身につける

といったところでしょうか(なんだかとてもスケールが小さな話になってしまいましたが…)。

Amazon.com、Amazon.co.jp でもランキング上位にあり、結構なレビューの数が付いていることを見ると、起業家(もしくは起業家を目指している人)以外にも多くの人がこの本を読んだのだと思うのですが、そういう人がどのようにこの本を消化して、どのように行動を変えようと思ったのか、という考えを聞いてみたいものです。

また、この本を読んで、以前読んだ『コピーキャット―模倣者こそがイノベーションを起こす』を再読したくなりました。こちらの本はこちらの本で、納得できる内容であり、とても面白かった覚えがあります。


おまけメモ。Amazon で「序文いらない」というレビュー投稿を見たのですが、それにはちょっと同意しました。冒頭で、あなたの書籍の推薦依頼を受けるかどうかのポリシーを語られてもね、という。それ以外の内容は普通で特に気にならないのですが。関係のないこと(特に見方によっては自慢のように思えること)を書くと余計な印象を持たれてしまうかも、という戒めにメモしておきます。

『幻惑の死と使途』 感想 : 「名前」のための殺人の話


このエントリーをはてなブックマークに追加


幻惑の死と使途』読了。Kindle Voyage を買ったついでに、何か読むものを、と思って買った小説でした。森博嗣作品は『すべてがFになる』と『冷たい密室と博士たち』しか読んでませんが、この3つの中では一番良かったです。手品とか好きですし、トリック・動機も納得のいく方でした。

それから、全体的な「名前」というテーマと、犀川教授の
人はアウトプットするときだけ、個たる「人」であり、それ以外は、「人々」でしかない
という言葉がとても印象的でした。

心に残る一言が一つでもあると、単純な娯楽としての読書以上にその小説は読んでよかった、と思えるので、この本もまた読んでよかったです。

2014年11月14日金曜日

Android : string.xml リソースで太字・斜体などのスタイルを指定する


このエントリーをはてなブックマークに追加
リソースファイル string.xml では太字を html の bold として <b> タグで指定。
html として処理するので、改行はいつもの \n ではなく <br/> で。
%1$d は %d の引数番号指定。(引数一つしか無いのでここでは特に意味は無いですが…)

<string name="best_score"><![CDATA[Score : %1$d<br/><b>Best Score !</b>]]></string>

コード上からは、

mScoreTextView.setText(Html.fromHtml(getString(R.string.best_score, score)));

のように、getString で取得した値を Html.fromHtml でフォーマットすることで TextView にスタイルを反映できる Spanned インターフェースを作る。

参考:Is it possible to have multiple styles inside a TextView?