0

Google APIを使ったアプリをAndroidマーケットで公開

Androidマーケットでのアプリの公開で書いた方法だけでは、Google Mapsを使っているアプリで地図が表示されなかった。

よく考えたら、MapViewのAPI Keyの指定はアプリのビルド時に使うkeystoreから作ったMD5 finger printから生成されていた。


MD5 fingerprintをAndroidマーケットでのアプリの公開で作ったリリース用の鍵から生成する。

keytool -list -keystore shokai-key


Sign Up for the Android Maps API – Android Maps API – Google Codeに、MD5 fingerprintをコピペして「Generate API Key」を押す


新しくAPI Keyが生成される。


Eclipseで、layout/main.xmlを編集。MapViewにapiKeyを指定
<com.google.android.maps.MapView
    android:id="@+id/mapview"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:enabled="true"
    android:clickable="true"
    android:apiKey="your-api-key"
    />


これでEclipseでパッケージ名を右クリックして[Android Tools]→[Export Signed Application Package]でapkを生成して、Marketで公開する。


shokai.org で検索
android market


既にdebug版アプリが入っていれば入れ替わる
android market


AndroidManifest.xmlに基づいてインストール時に警告が出る
android market

地図を使うアプリの作り方については、以前AndroidでGPSロガーでくわしく書いた。

1

Androidマーケットでのアプリの公開

公開用に、AndroidManifest.xmlにminSdkVersionを指定してからコンパイルする。
Android2.1用なので7を指定。

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
      package="org.shokai.test"
      android:versionCode="1"
      android:versionName="1.0">
    <uses-sdk android:minSdkVersion="7" />
    <application android:icon="@drawable/icon" android:label="@string/app_name" android:debuggable="true">
        <activity android:name=".Test"
                  android:label="@string/app_name">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
</manifest>


公開用apkを作る。
Eclipseでパッケージ名を右クリックして[Android Tools]→[Export Signed Application Package]


1回目は証明書を作る。適当に項目を埋める。
signature key creation


signedなapkができたら、terminalで実機にインストールしてみる。
接続されたデバイス一覧を確認
adb devices
HT04GPL12980にinstall
adb -s HT04GPL12980 install test2.1.apk
既に入っているdebug版を先にアンインストールしないとinstallできない。


http://www.android.com/market/ の下のリンク(http://market.android.com/publish)からAndroid Marketでの公開用にGoogleに年会費25ドル払う。


signedなapkとスクリーンショット等を登録、アップロード。
1分も待たずに即公開されていた。android端末側のマーケットで検索するとすぐ出る。
testapp on android market


Google APIを使ったアプリは専用の証明書か何かが必要らしい。これだけではアプリは起動するけどGoogle Mapが表示されなかった。

0

Android SDKのlogcat

Log.v("button1", "押した");

などするとAndroid実機からEclipseにテキストが流れてくるlogcatだけど
17インチMacbookProでは文字が超小さい。なのにフォント設定ができなかったり、日本語が文字化けしたりする。



terminalで
adb logcat
で起動する方のlogcatならちゃんと日本語出る。

少なくともソースコードUTF-8で、iTermもUTF-8で使ってるので文字化けしない。


adbは複数同時に起動できるので、screenで画面分割して2つ起動。
左側は
adb logcat | grep "^V\/"
でLog.vでの出力だけを表示するようにしてみたりとかすると、Eclipseよりこっちの方が便利な気がしてくる。
adb logcat

2

AndroidでGPSロガー

Android2.1でGoogle Map API、メニュー画面、GPSでの位置の計測を使う方法が理解できたので作った。
GPSとって5秒おきにアップデートし、Google Mapに自分の位置を表示する。
移動に合わせて地図の中心点を動かし、移動のログを地図上に赤線で表示する。

GPS使いまくったら、電車で藤沢と日吉の間を往復する間使い続けただけでHTC Desireの電池が40%ぐらい減った。5秒間隔は狭すぎるか。

バイナリはAndroid2.1以上向け。右クリックで保存し.apkにリネームすればインストールできる

昨日テストした。日吉から藤沢方面に戻るところ。
緯度と経度のListをメモリ上に保存してあるので、線を引いて示す事ができる。
メモリ上に保存してあるだけなのでアプリを終了すると消える。でもAndroidアプリはそもそも終了しないので電源を切るまでは残る。そのうちSDカードに保存するようにしよう
R0014774.JPG


Menuボタンを押すと
  • GPSのon/off、最後に計測した場所に戻る
  • ズームをレベル18にする(18ぐらいが人間が徒歩で使う地図として調度良い)
  • 衛星写真と地図の切り替え
  • ログ線の表示on/off
  • ができる。
    GPS Tracker


    電車で日吉駅に入る所
    R0014768.JPG


    電車で二俣川あたりを通り過ぎる
    GPS Tracker



    ■GPSで位置情報を取る
    橋本商会 適当なLocationProviderから位置情報を取るに書いたうち、requestLocationUpdatesを使った。


    GPSまわりは、Android2.1と1.6以前でAPIがかなり違う。
    ネットで検索して2009年春以前の記事を見てやってたら痛い目を見た。
    出たばかりのこの本は全体的に薄く広くカバーしていて、とっかかりとして良い。ホームスクリーンウィジェットやbluetoothまで一応載っている。
    レイアウトにXMLを使わず全てJavaで書いているけど。
    Android2.1プログラミングバイブル
    布留川 英一
    ソシム
    売り上げランキング: 847


    ■メニューボタン
    Activityを継承しているクラスならそのまま@OverrideでonCreateOptionsMenuとonInputItemSelectedを実装すれば使える。
    こんな風にクラス内クラスを作っておいて
    private static class MenuId{
        private static final int START_GPS = 1;
        private static final int LAST_LOCATION = 2;
        private static final int SET_ZOOM = 3;
        private static final int SATELLITE_TOGGLE = 4;
        private static final int LOG_TOGGLE = 5;
    }


    ボタンを追加して
    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
          boolean supRetVal = super.onCreateOptionsMenu(menu);
          menu.add(0, MenuId.START_GPS, 0, "Start GPS");
          menu.add(0, MenuId.LAST_LOCATION, 0, "Last Location");
          return supRetVal;
    }


    アイテムが選択された時のイベント内でどのボタンが押されたかを判定する。
    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
            case MenuId.START_GPS:
            // 何か処理
                break;
            case MenuId.LAST_LOCATION:
            // 何か処理
                break;
        }
    }
    イベント内でitem.setTitle()を使えばボタンの文字列を変更できる。


    ■Google Mapを表示する
    mapの表示の前に準備がいる。
    1. Eclipseで[Window]→[Android SDK and AVD Manager]→updateでGoogle APIをインストールする
    2. projectのPropertiesからbuild targetを変更。Android 2.1からGoogle APIのAndroid 2.1にする
    3. 最初に作ったActivityをcom.google.maps.MapActivityの継承に変更
    4. Google Maps API Keyを取得


    Google Maps API Keyの取得に必要なMD5 fingerprintは、Macなら
    keytool -list -keystore ~/.android/debug.keystore
    して適当なパスワードを入れたら生成された。~/.androidディレクトリごと生成された。


    AndroidManifest.xmlにandroid.permission.INTERNETとcom.google.android.mapsを追加する
    <?xml version="1.0" encoding="utf-8"?>
    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="org.shokai.gpstracker"
          android:versionCode="1"
          android:versionName="1.0">
        <application android:icon="@drawable/icon" android:label="@string/app_name">
            <activity android:name=".GpsTracker"
                      android:label="@string/app_name"
                      android:screenOrientation="portrait">
                <intent-filter>
                    <action android:name="android.intent.action.MAIN" />
                    <category android:name="android.intent.category.LAUNCHER" />
                </intent-filter>
            </activity>
    <uses-library android:name="com.google.android.maps" />
        </application>

    <uses-permission android:name="android.permission.INTERNET"/>
        <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
        <uses-permission android:name="android.permission.ACCESS_MOCK_LOCATION"/>    

    </manifest>

    res/layout/main.xmlにAPI key取得時にでてきたサンプルコードを貼り付けると、MapViewが表示される。
    <com.google.android.maps.MapView
    android:id="@+id/mapview"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:enabled="true"
    android:clickable="true"
    android:apiKey="your-api-key"
    />
    clickableやenabledをtrueにした。地図を指でつまんで動かせるようになる。


    main.xmlを書き換えたら、R.javaにいつのまにかMapViewのidが入っていた。
    import com.google.android.maps.*;
    MapView map = (MapView)findViewById(R.id.mapview);
    という風にcontrollerから扱える。


    これでとりあえず地図は表示できる。


    ■地図を操作する
    onLocationChangedでLocationオブジェクトが取れるので、そこから緯度経度が取れる。
    MapViewからMapControllerを取得して操作する。
    double lat = location.getLatitude(); // 緯度
    double lon = location.getLongitude(); // 軽度
    MapController mc = map.getController();
    GeoPoint p = new GeoPoint((int)(lat*1E6), (int)(lon*1E6));
    mc.setCenter(p);
    mc.setZoom(18);



    ■Google Map上にOverlayを表示する
    自分の位置を表示するだけなら、地図/位置情報/GPSを使うAndroidアプリを作るには (1/3) – @ITのようにMyLocationOverlayを使うといい。

    ただこの通りやるとGPSの取得間隔や精度を詳細に指定できるrequestLocationUpdateの機能とぶつかるのでそのままは使わず、onLocationChangedの中で位置を指定するようにした。


    とりあえずMyLoactionOverlayを作ってmapに配置しておくが、表示はしない
    MapView map = (MapView)findViewById(R.id.mapview);
    MyLocationOverlay myOverlay = new MyLocationOverlay(getApplicationContext(), map);
    myOverlay.onProviderEnabled(LocationManager.GPS_PROVIDER);
    map.getOverlays().add(myOverlay);

    自分アイコンを地図に表示
    public void onLocationChanged(Location location) {
        myOverlay.getMyLocation();
    }



    ログの線の描画は、Overlayを継承したLogOverlayクラスを自分で作り、draw関数の中を実装してやると作れる。drawはどうやらMapViewが描画しなおす毎に実行されるっぽい。MapView.invalidate()を呼ぶとすぐ再描画できる。
    Google MapにDrawableを配置する – Android Wiki*に東京と大阪の間を緯度経度で指定して線で結ぶ例があったので参考にした。

    LogOverlay.java
    package org.shokai.gpstracker;

    import java.util.*;
    import android.graphics.*;
    import android.graphics.Paint.*;
    import com.google.android.maps.*;

    public class LogOverlay extends Overlay {

    private Paint linePaint;
    private List<GeoPoint> points;

    public LogOverlay() {
    this.points = new ArrayList<GeoPoint>();
    this.linePaint = new Paint();
    linePaint.setARGB(255, 255, 0, 0);
    linePaint.setStrokeWidth(2);
    linePaint.setDither(true);
    linePaint.setStyle(Style.FILL);
    linePaint.setAntiAlias(true);
    linePaint.setStrokeJoin(Paint.Join.ROUND);
    linePaint.setStrokeCap(Paint.Cap.ROUND);
    }

    public void add(GeoPoint p){
    points.add(p);
    }

    @Override
    public void draw(Canvas canvas, MapView view, boolean shadow){
    if(points.size() < 2) return;
    Point p_a = new Point();
    Point p_b = new Point();
    for(int i = 0; i < points.size()-1; i++){
    view.getProjection().toPixels(points.get(i), p_a);
    view.getProjection().toPixels(points.get(i+1), p_b);
    canvas.drawLine(p_a.x, p_a.y, p_b.x, p_b.y, linePaint);
    }
    }

    public int size(){
    return points.size();
    }

    public List<GeoPoint> getPoints(){
    return this.points;
    }

    }



    ■画面を回転させない
    Androidは画面を回転させると、onDestroyイベント→onCreateイベントの順にイベントが発生して画面がまるごと描画しなおされてしまう。
    なので、画面を回転させるだけでTextViewは中身が消滅するし、Google MapにOverlayとして描画していた画像も消える。
    値や描画はいいんだけど、requestLocationUpdateの取り消しができなくなって、アプリを強制終了するまでGPSマークが点きっぱなしになったりするのがまずい。


    しょうがないので画面を回転させないようにした。


    回転させない方法としては八角研究所 : Android で再開する Java プログラミング(12) – Android ライフサイクルを考慮-手書きメモを作り込む
    に書いてある様にAndroidManifest.xmlの manifest/application/activityのattributeとして
    android:screenOrientation="portrait"
    を書いておくと縦画面専用になり、回転しなくなる。
    landscapeだと横になる。



    理想的な方法としては、回転した後に値を復元するのがある。
    画面回転時の挙動 – isherの日記が参考になる。
    onSaveInstanceState と onRestoreInstanceStateを@Overrideで実装してBundleにputString/getStringして復元する。
        @Override
        protected void onSaveInstanceState(Bundle bundle) {
            super.onSaveInstanceState(bundle);        
            bundle.putString("textViewMessage", this.textViewMessage.getText().toString() );
        }

        @Override
        protected void onRestoreInstanceState(Bundle bundle) {
            super.onRestoreInstanceState(bundle);
            this.textViewMessage.setText(bundle.getString("textViewMessage"));
        }
    key-valueでbundleに保存して復元できる。


    値の保存と復元ならこれでいいんだけど、GPSがどうしょうもない。センサーまわりのAPIの癖をつかまないと画面回転ができないアプリになっちゃって困る。


    GpsTracker.java
    package org.shokai.gpstracker;

    import android.os.Bundle;
    import com.google.android.maps.*;

    import android.content.Context;
    import android.location.*;
    import android.view.*;
    import android.widget.*;
    import java.util.*;

    public class GpsTracker extends MapActivity implements LocationListener{

    private MapView map;
    private TextView textViewMessage;
    private LocationManager lm;
    private MyLocationOverlay myOverlay;
    private final int zoom_default = 18;
    private boolean location_enalbed, log_enabled; // GPSとコンパスを動かしているかどうか、logを表示しているかどうか
    private LogOverlay logOverlay;

    private static class MenuId{
         private static final int START_GPS = 1;
         private static final int LAST_LOCATION = 2;
         private static final int SET_ZOOM = 3;
         private static final int SATELLITE_TOGGLE = 4;
         private static final int LOG_TOGGLE = 5;
    }

    public GpsTracker(){
            this.location_enalbed = false;
    }

        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.main);
            this.textViewMessage = (TextView)findViewById(R.id.textViewMessage);
            lm = (LocationManager)this.getSystemService(Context.LOCATION_SERVICE);
            map = (MapView)findViewById(R.id.mapview);
            myOverlay = new MyLocationOverlay(getApplicationContext(), map);
            myOverlay.onProviderEnabled(LocationManager.GPS_PROVIDER);
            map.getOverlays().add(myOverlay);
    logOverlay = new LogOverlay();
        }

        @Override
        public boolean onCreateOptionsMenu(Menu menu) {
          boolean supRetVal = super.onCreateOptionsMenu(menu);
          menu.add(0, MenuId.START_GPS, 0, "Start GPS");
          menu.add(0, MenuId.LAST_LOCATION, 0, "Last Location");
          menu.add(0, MenuId.SET_ZOOM, 0, "Zoom");
          menu.add(0, MenuId.SATELLITE_TOGGLE, 0, "Satellite/Map");
          menu.add(0, MenuId.LOG_TOGGLE, 0, "Show Logs");
          return supRetVal;
        }
        
        @Override
        public boolean onOptionsItemSelected(MenuItem item) {
         switch (item.getItemId()) {
    case MenuId.START_GPS:
    if(!this.location_enalbed){
    lm.requestLocationUpdates(LocationManager.GPS_PROVIDER, 5000, 10, this); // 5(sec), 10(meter)
            myOverlay.enableMyLocation();
            myOverlay.enableCompass();
            message("Start GPS");
            this.location_enalbed = true;
            item.setTitle("Stop GPS");
    }
    else{
    lm.removeUpdates(this);
    myOverlay.disableCompass();
    myOverlay.disableMyLocation();
    message("Stop GPS");
    this.location_enalbed = false;
    item.setTitle("Start GPS");
    }
    break;
    case MenuId.LAST_LOCATION:
    Location loc = lm.getLastKnownLocation(LocationManager.GPS_PROVIDER);
    double lat = loc.getLatitude();
    double lon = loc.getLongitude();
    message("last lat:"+Double.toString(lat) + ", lon:" + Double.toString(lon));
    this.setPosition(lat, lon, this.zoom_default);
    break;
    case MenuId.SET_ZOOM:
    MapController mc = map.getController();
    mc.setZoom(this.zoom_default);
    map.getOverlays().add(logOverlay);
    break;
    case MenuId.SATELLITE_TOGGLE:
    if(map.isSatellite()){
    map.setSatellite(false);
    }
    else{
    map.setSatellite(true);
    }
    break;
    case MenuId.LOG_TOGGLE:
    if(this.log_enabled != true){
    map.getOverlays().add(logOverlay);
    item.setTitle("Hide Logs");
    message("logs: "+Integer.toString(logOverlay.size()));
    log_enabled = true;
    }
    else{
    map.getOverlays().remove(logOverlay);
    item.setTitle("Show Logs");
    log_enabled = false;
    }
    map.invalidate(); // すぐ再描画
    break;
         }
         return true;
        }
        
        public void setPosition(double lat, double lon, int zoom){
         MapController mc = map.getController();
         GeoPoint p = new GeoPoint((int)(lat*1E6), (int)(lon*1E6));
         logOverlay.add(p);
         mc.setCenter(p);
         mc.setZoom(zoom);
         this.myOverlay.getMyLocation();
        }

        // zoomは変更せずに地図だけ動かす
        public void setPosition(double lat, double lon){
         this.setPosition(lat, lon, map.getZoomLevel());
        }
        
    public void message(String mes){
    this.textViewMessage.setText(mes);
    }

    public void onLocationChanged(Location location) {
            double lat = location.getLatitude();
            double lon = location.getLongitude();
    message("lat:"+Double.toString(lat)+", lon:"+Double.toString(lon));
    this.setPosition(lat, lon);
    }

    public void onProviderDisabled(String provider) {
    }

    public void onProviderEnabled(String provider) {
    }

    public void onStatusChanged(String provider, int status, Bundle extras) {
    }

    @Override
    protected boolean isRouteDisplayed() {
    return false;
    }

        @Override
        protected void onSaveInstanceState(Bundle bundle) {
            super.onSaveInstanceState(bundle);        
            bundle.putString("textViewMessage", this.textViewMessage.getText().toString() );
        }

        @Override
        protected void onRestoreInstanceState(Bundle bundle) {
            super.onRestoreInstanceState(bundle);
            this.textViewMessage.setText(bundle.getString("textViewMessage"));
        }
        
    }



    res/layout/main.xml
    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:orientation="vertical"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        >

    <TextView  
        android:layout_width="fill_parent" 
        android:layout_height="wrap_content" 
        android:id="@+id/textViewMessage" android:text="@string/message"/>
            
    <com.google.android.maps.MapView
    android:id="@+id/mapview"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:enabled="true"
    android:clickable="true"
    android:apiKey="0y0rb2n5OYga60X7toG-HzSYQh2ICnOT27xI4LA"
    />


    </LinearLayout>


    AndroidManifest.xml

    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="org.shokai.gpstracker"
          android:versionCode="1"
          android:versionName="1.0">
        <application android:icon="@drawable/icon" android:label="@string/app_name">
            <activity android:name=".GpsTracker"
                      android:label="@string/app_name"
                      android:screenOrientation="portrait">
                <intent-filter>
                    <action android:name="android.intent.action.MAIN" />
                    <category android:name="android.intent.category.LAUNCHER" />
                </intent-filter>
            </activity>
    <uses-library android:name="com.google.android.maps" />
        </application>

    <uses-permission android:name="android.permission.INTERNET"/>
        <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
        <uses-permission android:name="android.permission.ACCESS_MOCK_LOCATION"/>    

    </manifest>


0

適当なLocationProviderから位置情報を取る

AndroidはGPS以外にもWiFiのESSIDやIPアドレスとかで位置情報がとれるので(機種によるし今後増減すると思うが)、bestなLocationProvierから緯度経度を取るのはこんな感じ

Android2.1向け。

LocationManager lm = (LocationManager)this.getSystemService(Context.LOCATION_SERVICE);

Criteria crit = new Criteria();
crit.setAccuracy(Criteria.ACCURACY_FINE);
String provider = lm.getBestProvider(crit, true);
Location loc = lm.getLastKnownLocation(provider);

double lat = loc.getLatitude();
double lon = loc.getLongitude();
参考:Using Location API – Marakana




ふつうにGPSを使って緯度経度を取るのは、衛星キャッチして計測するまで少し間が空くものなのでonLocationChangedイベントを登録して待とう


自らLocationListenerをimplementsして
public class GpsTracker extends Activity implements LocationListener{
 // code
}


requestLocationUpdatesを呼ぶ。画面上の方のメニューバーにGPS測位中アイコンが出るはず
LocationManager lm = (LocationManager)this.getSystemService(Context.LOCATION_SERVICE);
lm.requestLocationUpdates(LocationManager.GPS_PROVIDER, 5000, 10, this); // 5秒間隔、10メートル


位置がupdateされる毎にこの関数が呼ばれる
public void onLocationChanged(Location location) {
double lat = location.getLatitude();
double lon = location.getLongitude();
}

onLocationChangedが呼ばれるタイミングは、この場合5000ミリ秒経過した&&10メートル以上移動したという事でいいのかな?
よくわからない。0メートルにしたらほぼ5秒間隔で呼ばれた。


ちなみに複数回requestLocationUpdatesすると、リスナが多重に登録されてしまう。数を数えておくか、登録する前にあらかじめremoveしておくとかすれば良さそう
lm.removeUpdates(this);
removeUpdatesを呼ぶか、アプリ自体を終了させるまでGPSのアイコンは点きっぱなしになっていた。
ユーザが止めれるようにするか、数分で自動的に止めるべき。


あと当たり前だけどLocationManagerを初期化しているgetSystemService()はonCreateイベント内かそれより後で呼ばないとアプリごと強制終了する。コンストラクタで呼んだら堕ちた。