^Mのせいでシェルスクリプトが動かない!

Linuxの話。
Windowsで作成したシェルスクリプトLinuxで実行した。

問題ないと思っていたのだけど、

#!/bin/sh^Mが見つからないとエラーが出る。

^Mってなんだ!?

心当たりはあったのだけど、調べてみると、
やはり^Mは改行コードみたい。

Windowsの改行コードはCRLFなのに対して、Linuxの改行コードはLFなので、
CRの部分がこうなっちゃってるみたい。

これじゃ動かないので、viエディタで置換することにしたのだけど・・・・。
viエディタを開いても、^Mなんて出てこない。

:%s/^M//
<<|

で置換しようとしたけども、該当する文字列は存在しないと。

どうやら、<span style="color: #ff0000">viを開く時に-bオプションをつければよい</span>らしい。
-bオプションを付けた後、上記の置換操作を行うことで解決できた。

WifiAutoSwitcherをリリースしました

久々にAndroidアプリでも開発しようかということで、
設定した優先順位に従ってWifiのアクセスポイントを自動切り替えするAndroidアプリをリリースしました。

https://play.google.com/store/apps/details?id=net.atlabo.wifisilent.app

WifiAutoSwitcherと名付けました。
命名センスもアプリアイコンのデザインセンスもないという・・・・。

アプリの特長

Wifiアクセスポイントに接続の優先度を設定することができます。
・簡単設定でWifiアクセスポイントを自動的に切り替えることができます。

対象端末は?

Android4.0.3以上の機種であればご利用いただけます。

どんな人にオススメ?

通信手段として、複数のWifi接続先を有している人にオススメです。
逆に、AndroidスマートフォンWifiを使用しない」「Wifiを使用するがアクセスポイントは1箇所しかない」人には効果はありません。

私はdocomoのモバイルルータを持ち歩いていて、
パケットを節約するためにdocomo WIFIやその他の公衆Wifiスポット等に自動的に切り替えを行いたかったため
このアプリを作りました。
同じような状況の人には有用でしょう。

使い方は?

以下に、このアプリの操作説明を記しておきます。

アプリメイン画面

f:id:ats337:20140509022251p:plain:h640:left
アプリを起動した時に表示される画面です。

①サービスステータススイッチ
Wifiの自動切り替えを行うかどうかを切り替えるスイッチです。
ONにしておくと、より優先度の高いアクセスポイントを検知した時に自動的に切り替えます。
OFFにすると、優先度の高いアクセスポイントを検知しても接続先を切り替えません。
※OFFにすると、このアプリの意味はないので、基本的にはONのままにしておいてください。

Wifiステータススイッチ
Wifiの有効/無効を切り替えるスイッチです。

③接続中アクセスポイント
現在接続中のWifiアクセスポイントを表示します。

④優先度設定ボタン
ボタンを押すとアクセスポイント一覧画面に切り替わります。

Wifi設定ボタン
ボタンを押すと端末のWifi設定画面に切り替わります。

⑥履歴ボタン
ボタンを押すと履歴画面に切り替わります。

アクセスポイント一覧画面

f:id:ats337:20140509022255p:plain:h640:right

アクセスポイントの一覧を表示します。
予め端末のWifi設定で登録したアクセスポイント(1度でも接続したことのあるアクセスポイント)のみ表示されます。

①ステータス
アクセスポイントのステータスを色で表示します。
緑:現在接続中
赤:Wifiが有効で接続可能なアクセスポイント
灰:Wifiが無効で接続可能なアクセスポイント
Wifiが無効になっているアクセスポイントを有効にするには、端末のWifi設定で行ってください。

SSID
アクセスポイントの名称

③優先度
アクセスポイントの優先度
数値が高いほど優先度も高くなります。

優先度設定ダイアログ

f:id:ats337:20140509022258p:plain:h640:left

Wifiの接続優先度を設定するためのダイアログです。

①優先度
優先度を編集するための文字入力フィールドです。
設定したい数値に設定してください。

②OKボタン
優先度を確定するためのボタンです。
ボタンを押すと優先度が更新されます。
例えば、通常はモバイルルータを使用しているが、公衆Wifiスポットが使える場合はそちらを優先的に使用したい
という場合は、公衆Wifiスポットの優先度をモバイルルータの優先度よりも高くしておくことで、
公衆Wifiスポットが使える場所に行った時には自動的に切り替えることができます。

③キャンセルボタン
ダイアログを閉じるためのボタンです。
優先度を編集していても編集した数値は無効となります。

履歴画面

f:id:ats337:20140509022302p:plain:h640:right

WifiのON/OFFやアクセスポイントに接続した日時を表示する画面です。
履歴は最大100件まで表示することができます。

①日時
イベントが発生した日時を表示します。

②メッセージ
イベント内容を表示します。

③履歴削除ボタン
押すと履歴を全削除することができます。

最後に

どこにでも転がっているようなアプリで、今更感はありますが、不具合や要望がありましたらコメントください。

音声認識APIを利用する

音声認識を実装してみる。
実装方法は以下の2通りを説明する。

音声認識を実装するにあたり、AndroidManifest.xmlに以下のパーミッションを追加しておく必要がある。

    <uses-permission android:name="android.permission.RECORD_AUDIO"/>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>

下2つは機種によっては不要かも?

Androidで用意された音声認識用のセットを用いる

予め使いやすいように音声認識用のセットを用意していくれているので、実装は簡単。
画面上のボタンを押下することで、以下のような画面を起動する。

f:id:ats337:20140417011205p:plain

この画面が出ている間に、聞き取った音声を取得する。

Activityを継承したクラスに以下のように実装する。

    // ボタンが押された時に呼び出されるメソッド
    public void onRecordClick(View view) {
        Intent intent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
        intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL,
                RecognizerIntent.LANGUAGE_MODEL_FREE_FORM);
        intent.putExtra(RecognizerIntent.EXTRA_PROMPT, "Please Speech!!");
        startActivityForResult(intent, 0);
    }

    // startActivityForResultで呼び出したActivityから応答される結果を受け取るコールバックメソッド
    public void onActivityResult(int requestCode, int resultCode, Intent data) {
        if (requestCode == 0 && resultCode == RESULT_OK) {
            // 結果文字列リスト
            ArrayList<String> results = data
                    .getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS);

            // TODO ここに取得した文字列に対する処理を記述

        }
    }

音声認識の画面はAndroid自体が用意しており、RecognizerIntent.ACTION_RECOGNIZE_SPEECHを設定したIntentインスタンスを生成し、
startActivityForResultを呼び出すことで簡単に実装することができる。

音声を認識したら、onActivityResultが呼び出されるので、
取得処理はここに実装する。
認識した音声はdata.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS)で取得することができる。
ここで取得できるのはArrayListインスタンスであるが、中身となるString文字列には、
認識した文字列の候補が返却される。
聞き取った単語がArrayListの1要素として応答されるわけではないので注意。

音声認識処理を独自実装する

独自実装する場合はSpeechRecognizerクラスを使用する。

        // インスタンス生成
        SpeechRecognizer recognizer = SpeechRecognizer.createSpeechRecognizer(this);
        // リスナを設定
        recognizer.setRecognitionListener(new RecognitionListenerImpl());
        // リッスンスタート
        recognizer.startListening(intent);

上記で音声認識を開始できる。
終了する時はrecognizer.stopListening()で停止することができる。

音声認識のコールバックを受け付けるためにRecognitionListenerインターフェースを実装したクラスを作成して、setRecognitionListenerで設定している。

RecognitionListenerImplクラスは以下

public class RecognitionListenerImpl implements RecognitionListener {
    // 音声認識を開始する準備が整った時に呼び出される
    @Override
    public void onReadyForSpeech(Bundle bundle) {
        Log.v(RecognitionListenerImpl.this.getClass().getSimpleName(), "onReadyForSpeech");
    }

    // 音声の聞き取りを開始した時に呼び出される
    @Override
    public void onBeginningOfSpeech() {
        Log.v(RecognitionListenerImpl.this.getClass().getSimpleName(), "onBeginningOfSpeech");
    }

    // 音量(?)が変わった時に呼び出される
    @Override
    public void onRmsChanged(float v) {
    }

    // ちょっとよくわからない。呼び出されなかった。
    @Override
    public void onBufferReceived(byte[] bytes) {
        Log.v(RecognitionListenerImpl.this.getClass().getSimpleName(), "onBufferReceived");
    }

    // 音声の聞き取りを終了した時に呼び出される
    @Override
    public void onEndOfSpeech() {
        Log.v(RecognitionListenerImpl.this.getClass().getSimpleName(), "onEndOfSpeech");
    }

    // エラーが発生した時に呼び出される
    @Override
    public void onError(int i) {
        Log.v(RecognitionListenerImpl.this.getClass().getSimpleName(), "onError : " + i);
    }

    // 結果を返却するときに呼び出される
    @Override
    public void onResults(Bundle bundle) {
        List<String> list = (List) bundle.get(SpeechRecognizer.RESULTS_RECOGNITION);
        Log.v(RecognitionListenerImpl.this.getClass().getSimpleName(), "onResults");
        for (String str : list) {
            Log.v(RecognitionListenerImpl.this.getClass().getSimpleName(), str);
        }
    }

    // 部分的に結果を返却する時に呼び出される。
    @Override
    public void onPartialResults(Bundle bundle) {
        List<String> list = (List) bundle.get(SpeechRecognizer.RESULTS_RECOGNITION);
        Log.v(RecognitionListenerImpl.this.getClass().getSimpleName(), "onPartialResults");
        for (String str : list) {
            Log.v(RecognitionListenerImpl.this.getClass().getSimpleName(), str);
        }
    }

    // 呼び出されたことない。よくわからない。
    @Override
    public void onEvent(int i, Bundle bundle) {
        Log.v(RecognitionListenerImpl.this.getClass().getSimpleName(), "onEvent : " + i);
    }

上記のように、コールバックメソッドを実装していくことになる。

ちなみに、startListeningを呼び出した後、10秒くらい無音だと音声認識が終了してしまう。
また、音声を認識させている時でも、一定時間無音の状態だと終了してしまうようだ。

intent.putExtra(RecognizerIntent.EXTRA_SPEECH_INPUT_MINIMUM_LENGTH_MILLIS, 3600000);
   intent.putExtra(RecognizerIntent.EXTRA_SPEECH_INPUT_COMPLETE_SILENCE_LENGTH_MILLIS, 3600000);
   intent.putExtra(RecognizerIntent.EXTRA_SPEECH_INPUT_POSSIBLY_COMPLETE_SILENCE_LENGTH_MILLIS, 3600000);

Intentに上記の設定をすれば、1時間起動しっぱなしになるんじゃないかと思って試したけど、結果は変わらなかった。

EditTextのスタイルを変更

Android3.0以降のデフォルトのEditTextのデザインがイマイチだなぁと思うので、
テーマを変更してみることにした。

以下のログインID、パスワードの右側にあるようなスタイルに変更してみた。

f:id:ats337:20140414012555p:plain


どうもEditTextの枠線を設定するパラメータが見当たらなかったので、
layer-listを使い、2つの四角形を重ねてやり、枠線を表現した。

以下のedit_text.xmlファイルをdrawableフォルダに配置する。

<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    <!-- グレーの四角形を配置 -->
    <item>
        <shape>
            <solid android:color="#c2c2c2"/>
        </shape>
    </item>
    <!-- 白い四角形を上記の四角形に重ねて配置。ただし、2dpだけ内側にずらす -->
    <item
        android:bottom="2dp"
        android:left="2dp"
        android:right="2dp"
        android:top="2dp">
        <shape>
            <solid android:color="#ffffff"/>
        </shape>
    </item>
</layer-list>

次はstyle.xmlファイルに上記の設定を適用する。

<resources>
    <!-- アプリのテーマ -->
    <style name="AppTheme" parent="android:Theme.Holo.Light.DarkActionBar">
        <item name="android:editTextStyle">@style/EditText</item>
    </style>

    <!-- EditTextのスタイル -->
    <style name="EditText" parent="android:Widget.EditText">
        <item name="android:background">@drawable/edit_text</item>
        <item name="android:paddingLeft">8dp</item>
        <item name="android:paddingRight">8dp</item>
    </style>

</resources>

Widget.EditTextを継承したスタイルをEditTextという定義名で作成し、
先ほど定義したedit_textをbackgroundに設定
paddingLeftとpaddingRightは、文字を入力した時に枠に重なってしまうため、若干余白を空けた。

更に、EditTextをアプリのテーマAppThemeのeditTextStyleに定義。

もちろんAppThemeはAndroidManifest.xmlでデフォルトのテーマとして定義している。
これで完了。

Android4.4(KitKat)のWebViewでJavaScript実行できない!

今まで動いていたWebViewのアプリがNexus7(4.4)で動かなくなった。

4.4からWebView実装がChromiumに変わったらしい。
それに伴い、これまで、WebView#loadUrlメソッドで呼び出していたJavaScriptの呼び出し方が変わった。
4.4からはWebView#evaluteJavascriptで呼び出せばいいみたい。
2つ目の引数にはコールバック用のクラスを設定する。

それ以前は、従来通りWebView#loadUrlを使うことになるので、少々厄介・・・・。

以下を参考にさせていただいた。
http://stackoverflow.com/questions/21224321/webview-on-android-4-4-changing-font-using-javascript

greenDAOの使い方

今回は、AndroidのO/RマッパーであるgreenDAOに手を出してみる。

公式ページはこちら
http://greendao-orm.com/

greenDAOでは、Daoクラスの生成を、greendao-generatorというライブラリを用いて行う。

Daoを生成するために必要なライブラリは以下の3つ。
freemarker
greendao
greendao-generator


試しに作ってみた。

package net.atlabo.greendao.generator;

import de.greenrobot.daogenerator.DaoGenerator;
import de.greenrobot.daogenerator.Entity;
import de.greenrobot.daogenerator.Schema;

public class GreenDaoGenerator {
	public static void main(String[] args) throws Exception {
		Schema schema = new Schema(1, "net.atlabo.webserviceapp.app.dao");
		addUserInfoEntity(schema);
		addHistorySummaryInfoEntity(schema);
		addHistoryInfoEntity(schema);
		addNewDetailInfoEntity(schema);
		new DaoGenerator().generateAll(schema, "./");
	}
	
	public static void addUserInfoEntity(Schema schema) {
		Entity entity = schema.addEntity("UserInfo");
		entity.addIdProperty().autoincrement();
		entity.addStringProperty("loginId").notNull();
		entity.addStringProperty("password").notNull();
	}
	
	public static void addNewDetailInfoEntity(Schema schema) {
		Entity entity = schema.addEntity("NewDetailInfo");
		entity.addIdProperty().autoincrement();
		entity.addDateProperty("useDate"); // 利用日
		entity.addStringProperty("user"); // 利用者
		entity.addStringProperty("contents"); // 利用内容
		entity.addStringProperty("kind"); // 利用区分
		entity.addLongProperty("useAmount"); // 利用額
		entity.addLongProperty("requestAmount"); // 請求額
		entity.addStringProperty("requestMonth"); // 請求月
	}
	
	public static void addHistorySummaryInfoEntity(Schema schema) {
		Entity entity = schema.addEntity("HistorySummaryInfo");
		entity.addIdProperty().autoincrement();
		entity.addDateProperty("depositDate").notNull().unique(); // お支払い日
		entity.addLongProperty("totalRequestAmount").notNull(); // 請求合計金額
	}
	
	public static void addHistoryInfoEntity(Schema schema) {
		Entity entity = schema.addEntity("HistoryDetailInfo");
		entity.addIdProperty().autoincrement();
		entity.addDateProperty("depositDate").notNull(); // お支払い日
		entity.addDateProperty("useDate"); // 利用日
		entity.addStringProperty("user"); // 利用者
		entity.addStringProperty("contents"); // 利用内容
		entity.addStringProperty("kind"); // 利用区分
		entity.addLongProperty("newAmount"); // 新規利用額
		entity.addLongProperty("requestAmount"); // 今回請求額
	}
}

上記コードを実行することで、
net/atlabo/webserviceapp/app/daoにDaoクラスとEntityクラスが出来上がる。
これをAndroidソースにコピーして使用する。

Androidソース

Androidの方では以下のようにして使用する。

package net.atlabo.webserviceapp.app;

import android.database.sqlite.SQLiteDatabase;
import android.os.Bundle;
import android.view.View;
import android.widget.EditText;
import android.widget.Toast;

import net.atlabo.webserviceapp.app.dao.DaoMaster;
import net.atlabo.webserviceapp.app.dao.DaoSession;
import net.atlabo.webserviceapp.app.dao.UserInfo;
import net.atlabo.webserviceapp.app.dao.UserInfoDao;

import roboguice.activity.RoboActivity;
import roboguice.inject.ContentView;
import roboguice.inject.InjectView;

/**
 * Created by ats337 on 2014/03/15.
 */
@ContentView(R.layout.setting)
public class SettingActivity extends RoboActivity {
    private DaoMaster.DevOpenHelper helper;

    @InjectView(R.id.loginid_text)
    private EditText loginIdText;

    @InjectView(R.id.password_text)
    private EditText passwordText;

    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        helper = new DaoMaster.DevOpenHelper(this, "webservice-db", null);

        // ユーザレコードを取得
        SQLiteDatabase db = helper.getReadableDatabase();
        DaoMaster master = new DaoMaster(db);
        DaoSession session = master.newSession();
        UserInfoDao userInfoDao = session.getUserInfoDao();
        long count = userInfoDao.count();
        if (count > 0) {
            // レコードが存在すればEditTextに値を設定
            UserInfo userInfo = userInfoDao.loadAll().get(0);
            loginIdText.setText(userInfo.getLoginId());
            passwordText.setText(userInfo.getPassword());
        }
    }

    /**
     * 設定完了ボタン押下時
     * @param view
     */
    public void onFinishSettingButton(View view) {
        // ログインID
        String loginId = loginIdText.getText().toString();
        // パスワード
        String password = passwordText.getText().toString();
        // 入力チェック
        if (loginId == null || "".equals(loginId)) {
            // TODO エラー
            return;
        }
        if (password == null || "".equals(password)) {
            // TODO エラー
            return;
        }

        UserInfo userInfo = new UserInfo();
        userInfo.setLoginId(loginId);
        userInfo.setPassword(password);

        SQLiteDatabase db = helper.getWritableDatabase();
        DaoMaster master = new DaoMaster(db);
        DaoSession session = master.newSession();
        UserInfoDao userInfoDao = session.getUserInfoDao();

        // 件数を取得
        long count = userInfoDao.count();
        if (count == 0) {
            // レコードがなければInsert
            userInfoDao.insert(userInfo);
        } else {
            // レコードがあればUpdate
            userInfo.setId(userInfoDao.loadAll().get(0).getId());
            userInfoDao.update(userInfo);
        }

        // トースト出力
        Toast.makeText(this, "ユーザ登録を完了しました。", Toast.LENGTH_LONG).show();
        // Acitivityを終了
        finish();
    }
}

ユーザレコードを登録、更新、表示するプログラム。
登録は1件を前提とした作りになっているので、DBを使う必要はないのだけれど、サンプルということで。

ヘルパーを作って、SQLiteDatabase作ってDaoMaster、DaoSessionを作ってDaoを取得して・・・・となかなかに煩わしい。
とりあえず初回ということでこんなもんでしょうか。

Google Maps Android API v2登録方法

いつのまにかGoogleMapAPIもv2になっていた。
旧版のAPI Keyは使用できないということで、新たにAPI Keyを取得してみた。


まずはkeytoolでフィンガープリントを取得しないといけない。
keystoreはユーザディレクトリ配下の.androidフォルダにあるdebug.keystoreを使用する。

以下のようにしてフィンガープリントを表示する。

keytool -list -v -keystore debug.keystore -alias androiddebugkey -storepass android -keypass android

出力された中のSHA-1を使用する。

次にGoogle Developer Console(https://console.developers.google.com/project)で、登録を行う。

任意のプロジェクトを選択し、APIsを開いてGoogle Maps Android API v2をONにしておくこと。

f:id:ats337:20140313182131p:plain

更にCredentialsを選択して、

f:id:ats337:20140313182209p:plain

CREATE NEW KEYを選択。

f:id:ats337:20140313182304p:plain

Android keyを選択

f:id:ats337:20140313182337p:plain

SHA-1のフィンガープリントを入力する。
フィンガープリントに「;」を続けて、アプリのパッケージ名を入力する。

こんな感じ

3F:34:04:9D:2A:B6:FC:34:F6:96:4C:A8:81:CA:97:E8:AD:72:DC:DD;net.atlabo.sample

これでAPI Keyが作成できる。
作成したAPI KeyはAndroidManifest.xmlに追記する。

AndroidManifest.xmlの定義情報にも変更があるようだ。


まず、パーミッション
インターネットに加えて、以下の2つが必要になっている。

    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
    <uses-permission android:name="com.google.android.providers.gsf.permission.READ_GSERVICES"/>

また、applicationタグの子要素として、meta-dataを設定する。

    <meta-data android:name="com.google.android.gms.version" android:value="@integer/google_play_services_version"/>
    <meta-data android:name="com.google.android.maps.v2.API_KEY" android:value="@string/map_key"/>

@string/map_keyに、先ほど取得したAPI Keyを設定すればOK。

ちょっと手間だったけど、これで何とかGoogleMapが使えるようになった。