iPad miniの表示に関する素朴な疑問

もうすぐ発表されるらしいiPad miniに関して最も興味があるのは、従来のiPadアプリがどういう風に表示されるのか? というところ。

iPhone 5を含めて、今までのiPhone/iPadは一貫して等倍表示 (機種ごとにアイコンや文字などの見かけの大きさが変わらない) ことにこだわっているように見えます。

  • iPhone4/第3世代iPadでは、縦横のピクセル密度をちょうど2倍にすることで、従来アプリの見かけを維持している。
  • iPhone5では、ピクセル密度 (ppi) と横幅を旧機種に揃えることで、従来アプリの見かけを維持している。

一方、噂されている7.85インチという画面サイズを前提にすると、従来のiPadの9.7インチを前提としたアプリを等倍で表示するのはおそらく無理でしょう。

思いつく可能性として以下のものがありますが、どちらも一長一短です。

  • 従来のiPadアプリは縮小表示される? → アイコンが小さくなってタップしにくくなるなどの弊害が発生する。また、前述の理由により「Appleらしくない」と感じる。
  • アプリ側で物理的な表示サイズを揃えるように対応? → アプリが対応するまである程度期間がかかりそう。

(SDKレベルではすでに対応しているのかもしれませんが、ちょっとググった限りではその手の情報は見つからず)

Appleが10月23日の発表会を予告 iPad mini登場か - ITmedia NEWS

AndroidHttpClientによるHTTP POST

GCM (Google Cloud Messaging) のRegistration ID登録処理におけるAndroidアプリ側の処理として、サーバアプリに対してHTTP POSTで情報を送信する処理が必要になります。

AndroidでのHTTP POSTは使ったことがなかったので簡単なサンプルを使ってみました。今回はHttpUrlConnectionよりも使い方が簡単そうなAndroidHttpClientを使用しています。

基本的なロジックはAndroidHttpClientによるHTTP GETとほぼ同様ですが、HTTPリクエストにNamedValuePairのリストをセットする点が異なります。

以下のコード例でははしょっていますが、AndroidHttpClientはUIスレッドでは使えないので、適宜AsyncTask等と組み合わせる必要があります。

import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.message.BasicNameValuePair;
import android.net.http.AndroidHttpClient;

class MainActivity exteds Activity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
    ....
        // HTTPリクエストの構築
        HttpPost request = new HttpPost("http://server-app.example.com:3000/devices/register");
        List<NameValuePair> params = new ArrayList<NameValuePair>();
        params.add(new BasicNameValuePair("device_id", "DEVICEID_TEST00"));
        params.add(new BasicNameValuePair("registration_id", "REGID_TEST00"));
        try {
            request.setEntity(new UrlEncodedFormEntity(params, "UTF-8"));
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }

        // HTTPリクエスト発行
        AndroidHttpClient httpClient = AndroidHttpClient.newInstance("Demo AndroidHttpClient");
        HttpResponse response = null;
        try {
            response = httpClient.execute(request);
        } catch (IOException e) {
            e.printStackTrace();
        }
    ....
}

GCM登録用サーバの製作

前回製作したGCM (Google Cloud Messaing) のサンプルでは、スタンドアロンで動くRubyスクリプトからメッセージを送信していました。本来期待されるべき作りとしては、以下のような機能を持つサーバアプリケーションが必要になります。

  • GCMサーバからAndroidアプリに発行されたRegistration IDを受け取り、保存する処理
  • 保存したRegistration IDを使ってGCMサーバにメッセージを投入する処理

今回は前者の方をRailsを使って製作しました (前半の登録部分のみ)。
特に変わったことはしていないと思いますが、Railsあまり慣れていないので手間取りました…

前提としたバージョンは以下の通りです。

サーバアプリが受け取るリクエス

Androidアプリーサーバアプリ間のインタフェースは自由に決められるので、ここは以下のように設計しました。

  • http://ドメイン名/devices/register にHTTP POSTを送信
  • HTTPリクエストボディ (form形式) で、デバイス名 (device_id) とRegistration ID (registration_id) を渡す
  • HTTPレスポンスとして、登録内容がJSON形式で返される

モデル

先ほどのリクエストに登場するdevice_idとregistration_idのほかに、コメントを記述するためのdescriptionを設けています。
gcm_app_server/app/model/device.rb:

class Device < ActiveRecord::Base
  attr_accessible :description, :device_id, :registration_id
end

コントローラ

HTTP POSTのリクエストボディとして受け取ったdevice_id, registration_idからDeviceオブジェクトを作成し、DBに保存します。

ここでのややはまりは、

  • Railsのnewアクションから遷移した場合と直接HTTP POSTを投げた場合でparamsのキーが変わる (前者では、"device_" のようなprefixがつく)
  • render :json => オブジェクト とすることで、HTTPレスポンスをJSON形式で返せる

gcm_app_server/app/controller/DevicesController:

class DevicesController < ApplicationController
  # 登録済みRegistration IDの一覧
  def index
    @devices = Device.all
  end

  # Registration IDの登録
  def register
    reg_id = params[:registration_id]
    dev_id = params[:device_id]
    @device = Device.create(:device_id => dev_id,
                :registration_id => reg_id,
                :description => "")
    render :json => @device
  end
end

ビュー

メインの登録用アクション (register) はビューを持ちませんが、一応一覧表示用アクション (index) のビューを貼っておきます。

gcm_app_server/app/views/devices/index.html.erb:

<h1>デバイス一覧</h1>

<table class="devices">
  <tr>
    <td>#</td>
    <td>Device ID</td>
    <td>Registration ID</td>
    <td>Description</td>
  </tr>
  <% @devices.each do |device| %>
  <tr>
    <td><%= device.id %></td>
    <td><%= device.device_id %></td>
    <td><%= device.registration_id %></td>
    <td><%= device.description %></td>
  </tr>
  <% end %>
</table>

試行錯誤の後がいろいろ含まれていますが、一応indexのスクリーンショット

GCM (Google Cloud Messaging) を試してみる

GCM (Google Cloud Messaging) は、Googleのサーバ (GCMサーバ) を介してAndroid機にキー・バリュー形式のメッセージを送るサービスです。最近出た「Google Androidプログラミング入門 改訂2版」の付録としてGCMが取り上げられていたのもあって、この本のサンプルに沿って (というかほぼ丸写し) GCMによるメッセージ送受信を試してみました。

テスト環境: GCM受信にはGoogle Playアプリが必須 (らしい) なので、Androidエミュレータでは動作しません。

Google Androidプログラミング入門 改訂2版

Google Androidプログラミング入門 改訂2版

Google APIs上でのプロジェクト作成

GCMでメッセージを送るには、事前にGoogle APIs Console上でプロジェクトを作成し、GCMサービスを有効にしておく必要があります。

Google APIs Consoleで新規にプロジェクトを作成し、Servicesを選択すると利用可能なサービス一覧が表示され、ここで "Google Cloud Messaging for Android" をONにします。

さらに、API Accessから "Create new Server key" を選択することで、GCM送信時の認証に必要なAPIキーが発行されます。

ここで表示されるAPIキーと、URLの一部として表示されるプロジェクトID (Sender ID) はあとで使うのでメモっておきます。

クライアントアプリの構成

GCMを受信するAndroidアプリを開発するためには、Android SDK Managerから、"Google Cloud Messaging for Android Library" をダウンロードしておく必要があります。これをダウンロードすると、Android SDKをインストールしたディレクトリの下に extras/google/gcm/gcm-client/dist/gcm.jar というjarファイルができるので、これをEclipseAndroidアプリケーションプロジェクトの libs/ にコピーしておきます (プロジェクトのプロパティとして、外部jarファイルを指定する方法もありますが、今回試した限りではうまく動きませんでした…)。

最小構成としては、クライアントアプリは以下の2つから構成されます。

  • Registration ID (メッセージを送る側から送信先を指定するためのID) をGCMサーバから取得するアクティビティ
  • GCMサーバからのメッセージ受信を処理するサービス

これらのアクティビティとサービス、およびGCM受信に必要な各種のパーミッションをAndroidManifest.xmlに記述する必要があります。

AndroidManifest

GCM受信のために必要になる一連のパーミッションを記述します。その中に、<アプリケーションのパッケージ名>.permission.C2D_MESSAGE というユーザ定義パーミッションが含まれるので、その定義も記述する必要があります。

<manifest ...>
    ....
    <uses-permission android:name="android.permission.INTERNET"/>
    <uses-permission android:name="android.permission.GET_ACCOUNTS"/>
    <uses-permission android:name="android.permission.WAKE_LOCK"/>
    <uses-permission android:name="com.google.android.c2dm.permission.RECEIVE"/>
    <permission android:name="com.example.gcmdemo.permission.C2D_MESSAGE" />
    <uses-permission android:name="com.example.gcmdemo.permission.C2D_MESSAGE"/>
    ....

さらに、GCMのメッセージ受信時に発行されるブロードキャストインテントを受信するBroadcastReceiverと、メッセージ受信を処理するサービスのクラス名を記述する必要があります。

<manifest ...>
    ...
    <application ...>
        // Registration IDを取得するアクティビティ
        <activity android:name=".GCMDemo" ... />

        // GCM受信時のブロードキャストを処理するレシーバ
        <receiver
            android:name="com.google.android.gcm.GCMBroadcastReceiver"
            android:permission="com.google.android.c2dm.permission.SEND" >
            <intent-filter>
                <action android:name="com.google.android.c2dm.intent.RECEIVE" />
                <action android:name="com.google.android.c2dm.intent.REGISTRATION" />
                <category android:name="com.example.gcmdemo" />
            </intent-filter>
        </receiver>

        // GCMサーバからのメッセージ受信を処理するサービス 
        <service android:name=".GCMIntentService" />
        ...

Registration IDの取得

アクティビティ (GCMDemo) では、Register/Unregisterの2つのボタンにonClick()を定義して、GCMサーバへの登録/登録解除を行います。

ここで主に使うのがgcm.jarに含まれる com.google.android.gcm.GCMRegistrar というクラスで、実際の登録/登録解除処理はこのクラスが一手に引き受けています。

    GCMRegistrar.register(this, SENDER_ID); // 登録

    GCMRegistrar.unregister(this); // 登録解除

ここで、SENDER_IDには、Google APIs Consoleで取得したSender IDの値を指定します。

登録に成功するとGCMサーバからRegistration IDが発行されて、この値をメッセージ送信時に指定することになりますが、Registration IDの取得は後述の「受信メッセージの処理」として記述することになります。

受信メッセージの処理

com.google.android.gcm.GCMBaseIntentService のサブクラスとして、GCM関連で発生するイベントの処理を記述します (ここでは GCMIntentService というクラス名)。

  • onRegistered(): GCMサーバへの登録が完了したときに発生 (前述のGCMRegistrar.register() に対応)
  • onUnregistered(): GCMサーバへの登録解除が完了したときに発生 (前述のGCMRegistrar.unregister() に対応)
  • onMessage(): メッセージ受信時に発生
  • onError(): 何らかのエラーが発生したときに発生

onRegistered() では、発行されたRegistration IDをアプリケーションサーバに通知する処理が期待されていますが、今回はこの処理ははしょって、LogCatに表示したRegistration IDをコピペして使うことにします。

onMessage() では、NotificationManagerを使用してメッセージの到着を表示します。

public class GCMIntentService extends GCMBaseIntentService {
    ....
    private static String TAG = "gcm_demo_service";

    // GCMサーバへの登録完了時の処理
    protected void onRegistered(Context context, String registrationId) {
        // 取得したRegistration IDの値をログ出力
        Log.d(TAG, "GCMIntentService.onRegistered registrationId=" + registrationId);
        sendRegistrationIdToAppServer(registrationId);
    }
    static void sendRegistrationIdToAppServer(String registrationId) {
        // アプリケーションサーバにregistration IDを送る処理。今回は何もしない。
    }

    // メッセージ到着時の処理
    protected void onMessage(Context context, Intent intent) {
        Bundle messageBundle = intent.getExtras(); // Intent#getExtras() で受信したメッセージの内容を取得

        // キー・バリュー形式のメッセージにBundleとしてアクセス
        Iterator<String> iterator = messageBundle.keySet().iterator();
        while (iterator.hasNext()) {
            String key = iterator.next();
            // 取得したキー・バリューの対をログ出力
            Log.d(TAG, key + " = " + messageBundle.getString(key));
        }
        try {
            showNotification(context,
                    URLDecoder.decode(messageBundle.getString("message"), "UTF-8"),
                    URLDecoder.decode(messageBundle.getString("detail"), "UTF-8"));
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
    }
    private void showNotification(Context context, String message, String detail) {
        // NotificationManagerを使って、受信したメッセージの内容を表示
    }
    ...

PCとAndroid機をUSBKケーブルで接続した状態で、このアプリケーションを起動してRegisterボタンを押すと、LogCatにRegistration IDが出力されていることを確認できます。

ログ中に "internal error: retry receiver class not set yet" なるエラーが出ているのが気になりますが、調べてみた限りでは無視して良さそうなので気にしないことにします。
retry receiver class not set yet ? - Google Groups

# 今回テキストとして使った「Google Androidプログラミング入門」のサンプルプログラムでは、Notification周りのコードにdeprecatedなAPIが使われているようなので、warningが出ないように書き換えたいところ

メッセージの送信

Androidアプリ側は一応完成ということで、メッセージを送るサーバ側のプログラムを作ります。ちょっとしたフォームでメッセージの内容を入力したいところですが、今回は最小限の動作を確認するということで、決め打ちのメッセージを送ってみます。
言語は重要ではありませんが、勝手が分かっているRubyを選択。

require 'json'
require 'net/https'

# GCMサーバの接続先 (https://android.googleapis.com/gcm/send)
GCM_HOST = "android.googleapis.com"
GCM_PATH = "/gcm/send"

REG_ID = # Registration ID (Androidアプリ実行時にGCMサーバから発行されたもの)
API_KEY = # APIキー (Google APIs Consoleで発行されたもの)

// 送信するメッセージの内容
message = {
  "registration_ids" => [REG_ID],
  "collapse_key" => "collapse_key",
  "delay_while_idle" => false,
  "time_to_live" => 60,
  "data" => { "message" => "GCM Demo",
              "detail" => "Hello world"}
}

// HTTPS POST実行
http = Net::HTTP.new(GCM_HOST, 443);
http.use_ssl = true
http.start{|w|
  response = w.post(GCM_PATH,
    message.to_json + "\n",
    {"Content-Type" => "application/json",
     "Authorization" => "key=#{API_KEY}"})
  puts "response code = #{response.code}"
  puts "response body = #{response.body}"
}

PC上でこれを実行すると、Android機側では以下のような通知が現れます。

ついでにLogCatを見ると、以下のようにメッセージの内容が取得できていることを確認できます。

HTTPクライアント (HttpURLConnection編)

前回のあらすじ AndroidHttpClientを用いたデータ取得 - m-kawato@hatena_diary
↑ではAndroidHttpClientを使いましたが、↓この辺の記事を参考に、HttpURLConnectionを使って書き直してみました。
Y.A.M の 雑記帳: Android Apache HTTP Client と HttpURLConnection どっちを使うべき?

Android API ReferenceのHttpURLConnectionの項:
http://developer.android.com/reference/java/net/HttpURLConnection.html
割と親切なコード例が載っているので特に悩むところはありません。

基本的な構造はAndroidHttpClientを使ったサンプルから変えていませんが、インタフェースが異なるのに合わせてAsyncTaskのサブクラスのメソッドの型を変えています。あくまでAPIの使い方を確かめるのが目的なので、エラー処理はばっさり省略しています。

HttpDemo2.java

import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;

import android.os.AsyncTask;
import android.widget.TextView;

import org.apache.commons.io.IOUtils;
...
public class HttpDemo2 extends Activity {
    TextView textView;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.http_demo2);
        this.textView = (TextView) findViewById(R.id.text);

        // 接続先のURLを指定してHTTP GET実行
        URL url = null;
        try {
            url = new URL("http://www.example.net:8080/");
        } catch (MalformedURLException e) {
            e.printStackTrace();
            return;
        }
        new HttpGetTask().execute(url);
    }

    // AsyncTaskのサブクラスとして、バックグラウンドでHTTP GETしてTextViewに表示するタスクを定義
    class HttpGetTask extends AsyncTask<URL, Void, String> {
        // HttpURLConnectionを使ったデータ取得 (バックグラウンド)
        @Override
        protected String doInBackground(URL... url) {
            String result = "";
            HttpURLConnection urlConnection = null;
            try {
                urlConnection = (HttpURLConnection) url[0].openConnection();
                result = IOUtils.toString(urlConnection.getInputStream());
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                if (urlConnection != null) {
                    urlConnection.disconnect();
                }
            }
            return result;
        }

        // データ取得結果のTextViewへの表示 (UIスレッド)
        @Override
        protected void onPostExecute(String response) {
            HttpDemo2.this.textView.setText(response);
	}
    }
}

AndroidHttpClientを用いたデータ取得

Androidで、HTTPを用いてWebサーバ上のデータを取得する手段はいくつかあるようですが、今回はAndroidHttpClientを使って、HTTPサーバから取得した文字列をTextView上に表示するというサンプルを作ってみました。

主なポイントは以下の2点です。

  • AndroidManifest.xmlandroid.permission.INTERNETのパーミッションが必要がある。
  • AndroidHttpClientはUIスレッドで動かすことはできないので、別途スレッドを作って動かす必要がある。

スレッドを作る手段も複数ありますが、今回は比較的お手軽と思われるAsyncTaskを使いました。

また、AndroidHttpClientではHTTPレスポンスボディはInputStreamとして得られますが、これをStringに変換する処理は若干面倒なので、今回はApache Commons IOに含まれるIOUtilsを利用しました。

テスト環境は以下の通りです。

AndroidManifest.xmlの記述

ここは特に問題ないと思いますが、AndroidManifest.xml中のmanifest要素の子要素としてINTERNETパーミッションを追加します。

<manifest xmlns:android="http://schemas.android.com/apk/res/android" ... >
    <uses-sdk
        android:minSdkVersion="9"
        android:targetSdkVersion="15" />
    <uses-permission android:name="android.permission.INTERNET"/>

レイアウトの記述

本題ではありませんが、一応今回使った res/layout/ 以下のレイアウトファイルの内容を挙げておきます。

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" ...>
   <TextView
        android:id="@+id/text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerHorizontal="true"
        android:layout_centerVertical="true"
        android:textSize="30dp"
   />
</RelativeLayout>

AndroidHttpClientの使用

HTTPリクエストを発行してレスポンスを受け取るという処理自体には特別なところはありません。
こんな感じ:

import android.net.http.AndroidHttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.HttpResponse;
    ...
    AndroidHttpClient httpClient = AndroidHttpClient.newInstance("User Agent");
    HttpGet request = new HttpGet("http://www.example.net:8080/");
    HttpResponse response = null;
    try {
        response = httpClient.execute(request);
    } catch (IOException e) {
        e.printStackTrace();
    }

ただし、前述した通りUIスレッドとは別スレッドで実行する必要があることと、HTTPレスポンスからのボディの取り出しのために若干の手を加える必要があります。

AsyncTaskによるバックグラウンド実行

HTTP GETを別スレッドで実行し、実行結果をUIスレッドで画面に表示するという動作を実現するために、このような動作のために用意されているAsyncTaskを利用します。AsyncTaskはジェネリクスを全面的に使っているので最初戸惑いましたが、おそらく慣れたらそれほど難しいものではないと思います。

import android.os.AsyncTask;
import android.net.http.AndroidHttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.HttpResponse;
...
public class HttpDemo extends Activity {
    TextView textView;

    @Override
    public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.http_demo);
    this.textView = (TextView) findViewById(R.id.text);

    HttpGet request = new HttpGet("http://www.example.net:8080/");
    new HttpGetTask().execute(request); // AsyncTaskを使って定義したタスクを呼び出す

    // AsyncTaskのサブクラスとして、バックグラウンド処理用のタスクを記述
    class HttpGetTask extends AsyncTask<HttpUriRequest, Void, HttpResponse> {
        // doInBackground() に、バックグラウンド処理の内容を記述する。
        // ここではAndroidHttpClientによるHTTP GET実行
        protected HttpResponse doInBackground(HttpUriRequest... request) {
            AndroidHttpClient httpClient = AndroidHttpClient.newInstance("Demo AndroidHttpClient");
            HttpResponse response = null;
	    try {
                response = httpClient.execute(request[0]);
            } catch (IOException e) {
                e.printStackTrace();
            }
            return response;
        }

        // onPostExecute() に、バックグラウンド処理完了時の処理を記述する。
        // ここでは、HTTPレスポンスボディとして取得した文字列のTextViewへの貼り付け
        protected void onPostExecute(HttpResponse response) {
          .... // この部分の処理は後述
        }
    }
}

Commons IOを用いたHTTPレスポンスから文字列への変換

AndroidHttpClientでHTTP GETの結果として得られるHttpResponseオブジェクトからレスポンスボディを取り出すには、response.getEntity().getContent() で行けます。しかしながら、この結果として得られるのはInputStreamオブジェクトであり、文字列として取り出したい場合には変換に一手間かかります。

InputStream→String の変換は、Apache Commons IOライブラリで実現できるようなので、今回は安直にこれを使いました。Commons IOの配布に含まれるcommons-io-2.4.jarを、Androidのプロジェクト内の libs/ ディレクトリに置くと使えます。

Commons IOを使ってHTTPレスポンスボディ中の文字列を抽出し、その結果をTextViewに貼り付ける部分のコードは以下のようになります。

import org.apache.commons.io.IOUtils;
....
public class HttpDemo extends Activity {
    ....
    class HttpGetTask extends AsyncTask<HttpUriRequest, Void, HttpResponse> {
        ....
        protected void onPostExecute(HttpResponse response) {
            String message = "";
            try {
                InputStream content = response.getEntity().getContent();
                message = IOUtils.toString(content); // Commons IOを用いてInputStream→String変換
            } catch (IOException e) {
                e.printStackTrace();
            }
            textView.setText(message);
        }
    }
}

実行

今回は、以下のような簡単なHTTPサーバをnode.jsで作成して、作成したAndroidアプリから読む込んでみました。もちろんApacheなどを使っても良いと思います。

var http = require('http');
var server = http.createServer(function(req, res) {
   res.writeHead(200, {'Content-Type': 'text/plain'});
   res.write('Hello Internet\n');
   res.end();
});
server.listen(8080);

この状態で、作成したサンプルプログラムを動かすと、無事HTTPで取得した文字列が表示されました。

AsyncTaskメモ

AsyncTask

AsyncTaskの3つの仮型引数の参照箇所

  • Params: AsyncTask#doInBackGround の引数の型
  • Progress; AsyncTask#onProgressUpdate, AsyncTask#publishProgress の引数の型
  • Result: AsyncTask#onPostExecute の引数の型、AsyncTask#doInBackground() の戻り値の型

AsyncTaskの主要メソッド

  • AsyncTask execute(Params... params)
  • Result doInBackground(Params... params): バックグラウンドで実行するタスクへの入力をParams型で受け取り、タスクの実行結果をResult型で出力する。
  • void publishProcess(Progress... Values): タスクの実行状態をProgress型で受け取る。
  • void onProgressUpdate(Progress... values): publishProcessに渡されたタスクの実行状態をProgress型で受け取る。このメソッドはUIスレッドで実行される。
  • void onPostExecute(Result result): doInBackgroundによる実行結果をResult型で受け取る。このメソッドはUIスレッドで実行される。