2019年3月28日木曜日

Google Play以外で配布したAndroidアプリに半自動アップデート機能をつける

Google Playを使わずに直接apkを端末に配布することがあるかと思いますが、アプリにアップデートがあった場合、Google Playならコンソールでapkをアップロードすれば自動的に各端末でアップデートが行われますが、直接配布した場合はそうはいきません。

新しいapkファイルをWebからダウンロードするかメールで受信するかなど、なんらかの方法で各端末に送って、ファイル管理アプリなどでそのapkを実行して、Androidシステムのインストーラを起動してアップデート、ということを行わなくてはいけません。
台数が多い場合は大変です。かといって各ユーザーにしてもらうのも負担をかけますし、確実にしてもらえるかもわかりません。

楽にするにはどうしたらいいでしょうか。

ざっくり大きく言うと、以下の2つの機能をアプリに入れれば、自動、、、とまではいかないですが、半自動でアップデートすることができます。
(システムのバージョンアップによりいずれできなくなる、あるいは機種によりできない可能性があります。Freetel Priori5 - Android7.1.2で確認しました)

1.新しいapkを端末にダウンロードする
2.IntentにそのapkのURIを入れてインストーラ(正式名称わかりません)を起動する

1は特に問題ないと思います。HTTP,FTPなんでも良いのでサーバーからapkをダウンロードして、端末に保存すればよいです。ただし保存場所は注意しなくてはいけません。ローカル領域(openFileOutput()で取得するパス)だと、インストーラがapkにアクセスできないため、external領域(Environment.getExternalStorageDirectory()で取得するパス)に置く必要があります。

2が肝心なところです。以下のようなコードで実行できます。
Uri uri = FileProvider.getUriForFile(
    context,
    authority,
    file);
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setDataAndType(uri, "application/vnd.android.package-archive");
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_GRANT_READ_URI_PERMISSION);
startActivity(intent);

FileProvider.getUriForFile()を使用してapkのUriを取得するのがポイントだと思います。
第3引数はapkのFileです。1のときexternal領域に保存しているので、パスは「/storage/emulated/0/○○○/myapp.apk」などとなると思います。

FileProviderを使用するには、manifestなどに定義を記述する必要があります。重要なところですが、詳細は本筋と外れる部分ですので省略します。

取得するUriは「content://<authority>/<privider_path>/myapp.apk」などとなると思います。<>で囲った部分はFileProviderに定義した値を入れます。

Uriができたら、あとは上記コードの通り、ACTION_VIEWのインテントに各パラメータを入れてstartActivityをすると、インストーラが起動します。(各パラメータについて、Androidバージョンによって要不要が違ったり書き方が違ったりするようですが、詳細は省略します)

注意点として、インストーラは「Google Play」アプリを使用している、あるいはそのもののようなので、端末のセキュリティソフトでアプリの実行を制限しているような場合は「Google Play」を許可するように設定しておく必要があります。

また、この処理をいつ実行するかですが、アップデートボタンをつける、APIを用意して最新版があるかチェックして必要なときのみ実行するなど、凝ればいくらでもできますが、要件に応じて検討する必要があるかと思います。

最後に、インストーラが起動してインストールが完了するまでの画像を貼って終わります。

手動でapkダウンロードしたときと同じ画面です。「インストール」を選びます。

これは機種によって?出たり出なかったりするかもしれません。「インストールする」を選びます。「OK」が目立つので、そっちが押されそうですね。。。 

「許可」します。 

無事新バージョンがインストールされました。

以上です。





2019年3月27日水曜日

xSpan用のデモアプリ作ってみた

Impinj社が出している xSpan というかっちょいいリーダがあります。

このリーダ、アンテナが内蔵されていてこれだけ設置すれば使えます。それだけなら他にもリーダあるんですが、xSpanは(擬似的に)13枚のアンテナが内蔵されているのです。

アンテナは1列に並んでいる感じで、どのアンテナで読んでるかを見ることでタグが右に行ったとか左に動いたとかがわかるようになっています。スゲー

ちなみにどんな風にアンテナが並んでるかは、
https://support.impinj.com/hc/en-us/articles/206246824-xSpan-Documentation
こちらの「xArray/xSpan Orientation Guide」というのに記載されています。

とはいえなんせ電波なもんで見えないし、実際にはどんな風に読んでるのさ?というのがわかるといいなーと思ってデモアプリを作ってみました。

じゃーん


超シンプル。読み取ったタグのEPCと、横に読み取ったアンテナのところにRSSIが入るというそれだけです。これがなかなか遊び甲斐があります。

というわけで動画

わかりづらくてすいません。でも何となくおわかりいただけるのではないかと。画面の左下に一枚タグを置いてるんですが、これが1行目のデータで、2行目と3行目は動かしているタグのデータです。どうでしょう。わかります?わかるよね?

デモ用なんですが、リーダ設置の際とか読取りテストに使えるかなーと思ってます。

2019年3月14日木曜日

リーダの制御をうまいことする

今回の内容は RFID とはあんまり関係ないんですが、Android から RFID のリーダを制御するときのあるある的なネタです。

リーダを制御する場合、Bluetooth でつないだり 直接接続(シリアル的な)でつないだりとリーダごとに違うんですが、問題は通信先のリーダは1つということで、スマホにおいてはカメラやマイクなどハードを使うときと同じように1つのアプリで専有しちゃうとまずいということなんですね。

つまりアプリの起動時にリーダに接続(専有)して、終了時に切断(開放)する必要があるということです。さらにサスペンドしてるときとかアプリが裏に回ったときなんかも細かく切断する必要があります。

じゃぁ、Android の Activity のライフサイクルにおいて onResumeで接続して、onPauseで切断すればいいじゃん的な流れに当然なりますよね。しかし複数の画面があるアプリで画面を切り替えると onPause とか呼ばれちゃうので画面を切り替えるたびに切断、接続を繰り返すのでちょっとそれはなーってなります。

ようするに自分のアプリが画面に出ているときは接続してて、画面からいなくなったら(それが終了であれ裏に回ったであれサスペンドであれ)切断するというのをやりたいのですが簡単そうでなかなかできません。

そこでこちらの記事を参考にしてみました。
https://qiita.com/wasnot/items/e7e4699de4b21f150c63

Applicationを継承して、Activity のライフサイクルじゃなくて Application のライフサイクルを利用しようというわけです。

public class MyApplication extends Application {

    private static MyReaderController mController;

    @Override
    public void onCreate() {
        super.onCreate();
        registerActivityLifecycleCallbacks(new MyLifecycleHandler());

        mController = ....
    }

    @Override
    public void onTerminate() {
        super.onTerminate();
    }

    private class MyLifecycleHandlerimplements ActivityLifecycleCallbacks {
        private int activities = 0;

        @Override
        public void onActivityCreated(Activity activity, Bundle bundle) { }

        @Override
        public void onActivityStarted(Activity activity) {
            activities++;
            if (activities == 1) {
                mController.start();
            }
        }

        @Override
        public void onActivityResumed(Activity activity) { }

        @Override
        public void onActivityPaused(Activity activity) { }

        @Override
        public void onActivityStopped(Activity activity) {
            activities--;
            if (activities <= 0) {
                mController.stop();
            }
        }

        @Override
        public void onActivitySaveInstanceState(Activity activity, Bundle bundle) { }

        @Override
        public void onActivityDestroyed(Activity activity) { }
    }
}


やり方は簡単で、Application を継承したクラスを作って、AndroidManifest.xml の application の name属性 に作成したクラス名を指定するだけです。

この onActivityStartyed と onActivityStopped がそれぞれアプリ内で Activity がスタートしたときとストップしたときに呼ばれますので、呼ばれた数をカウントして1個目だったら画面に表示されはじめた、0個になったら画面から居なくなったと判断できるわけですね。

これが正解なのかよくわかりませんが、実装は楽でした。