2013 年 4 月 20 日

アンドロイドアプリ自身でアプリをアップデートする方法

アンドロイドアプリ自身でアプリをアップデートする方法

なつかしの1.6のドーナッツアイコン

iPhoneと比べて、アンドロイドでは自由にアプリをインストールできます。たとえGoogle Playのようなマーケットに登録していなくてもインストールは可能です。しかし、アプリを更新しても、自動的にアップデートすることはできません。

弊社では企業向けタブレットアプリ開発がメインなのですが、このようなアプリは不特定多数がダウンロード可能なGoolge Playに登録するわけにもいきません。

登録してもよい場合でも、ネットワークがインターネットに接続されていないVPNであったりしますので、アプリの更新はアプリで自力で行うことになります。

プライベート・アプリマーケット

企業向けということになると、端末が数百台なんということもありますし、また端末も全国の各地の支店に配置されていたりしますから、アプリのアップデートを行うのは容易ではありません。初期キッティングでさえ、非常にコストも時間もかかります。

プライベートアプリマーケットが社内に簡単に構築できて、App StoreやGoogle Playのような専用アプリが利用できればいいなと思うのですが、一つの企業でそんなに多くのアプリを保有するわけではないので、ちょっとおおげさに思えます。

mixiが運営しているDeployGateは、アプリを配布するプラットフォームですが、開発者向けサービスとなっていますので、ちょっと用途が異なります。
私たちは、一般利用者への配布を考慮したPaads(Private Android App Distribution Service)というプロジェクトで、調査開発しています。
SDK形式なら開発でも利用してもらえるかもしれませんし、ECサイト(たとえばEC-CUBEとかMagentoとか)と連動して、ダウンロード課金が可能になれば、ニーズも広がるかもしれません。

しかし、大規模でなければ、アプリの方で自動アップデートの仕組みを実装するのが手っ取り早いと思います。

では、どうやって実現すればいいでしょうか?ということですが、我々がよく使う方法は、サーバサイドに新しいアプリがあれば、アンドロイドアプリ側でダウンロードして、インストーラーを起動するという仕組みです。

ブラウザでダウンロードして、インストールできるよって思う方もいるかもしれませんが、エンドユーザーにはそういったことが受け入れていただけない場合もあります。

我々がいままで試行錯誤した方法をまとめてみますので、ぜひ参考にしてみてください。また、もっといい方法があるよという方がいたら、ぜひ教えていただきたいです。

参考に、一部ソースコードを掲載していますが、抜粋ですので、このままでは動作しません。ご了承ください。また、このソースコードは弊社の調査研究開発のもので、お客様の開発案件で利用したものではありません。

【PR】

こういう自動アップデート機能を持ったアンドロイドアプリやアプリ配布方法を検討されている方、またはキッティングでお困りな方は、ぜひ弊社にもご相談ください。

アプリから自身の最新アプリをダウンロード

アプリのダウンロードはHTTPを使えば簡単ですが、企業向けの場合は制限があって利用できない場合もあります。しかし、なんらかのファイル転送プロトコルが有効であるということが前提になります。

弊社では単純なHTTPのファイル転送ならAsyncHttpClientを使うことが多いです。GUIスレッドではHTTP通信はできませんが、これを使えば簡単に実現できます。

AsyncHttpClient client = new AsyncHttpClient();
client.get(mApp.getApkUrl(), new DownloadHandler(mApp, btn, mProgress));        

最近、実装したアプリでは、HTTPが使えず、FTPによる転送で実装したことがあります。FTPだとApache Commons Netなんかが便利です(2013/06/12に、3.2から3.3へアップデートされています。3.2のバグでftpアップロードが異常に遅いというバグも改善されていますので、3.2をお使いの方は、3.3へアップデートをおすすめします)。こちらはandroid用ってことではないので、非同期処理をラップしないと使えないです。

これで最新のアプリがダウンロードできるようになりましたが、これが最新かどうかというのはどうすればわかるか?という疑問がわきます。しかも、本体をダウンロードしてみないと最新かどうかわからなければ、通信リソースを消費することになります。

Google Cloud Messaging

アンドロイドにはGCM(Google Cloud Messaging)というサービスがあります。最新のアプリがあれば、端末にプッシュで通知する仕組みです。これが使える環境であれば、一番スマートかもしれませんが、古いタブレットandroidのバージョンが古いだと使えなかったり、Google Playへのアカウント登録が必須であったりと、これまた企業向けタブレットのアプリでは、あまり使えなくて残念です※。

※補足:正確にはAPI Level8(android2.2)以上で、Google Play Storeがインストール(com.google.android.gsfがインストールされているということだろう)されている端末で動作します(Googleアカウント登録は、API Level 15以上なら不必要)。

関連記事:アンドロイドアプリからGoogle Cloud Messagingを使う方法(第1回)

独自APIをポーリング

次に、サーバサイドにアプリ情報を取得できるAPIを用意しておき、これをポーリングするという手段があります。この場合、JSONを返すようなAPIを用意してやり、それをパースしてバージョン番号を得るようにして、今インストールしているアプリのバージョンと比較して、新しければ、ダウンロードするということができるでしょう。

サーバサイドでは、apkをアップロードするだけで、apkを解凍して難読化されたmanifestを戻して、バージョンやアイコンが登録できると便利だと思います。この方法はまた別記事(apktoolでアンドロイドアプリの.apkファイルの中身を調べる)で書きたいと思います。

タイムスタンプを調べる

それもちょっと大げさということであれば、インストールしているアプリのタイムスタンプを記憶しておいて、サーバサイドのアプリファイルのタイムスタンプだけを読んで、タイムスタンプが新しくなっていれば、本体をダウンロードするということでもよいでしょう。

アプリのバージョンが簡単にわかるように、ファイル名にバージョン番号を付加することもあります。たとえば、hoge-v-1.apkという具合です。このバージョン番号をみて判断ということでもよいでしょう。

次にapkファイルはどこへ保存すべきかということも疑問がわくかもしれません。我々もアプリ領域にダウンロードしたり、エクスターナルストレージを利用したりしています。
あまり、みせたくないファイルでもあるので、アプリ領域へと思っても、MODE_WORLD_READABLEにしないとインストーラーが起動できませんでした。

以下のようにアプリ領域にファイルを作成するときのモードはMODE_WORLD_READABLEにしてやらないと、インストーラーが読めないようです。

String fn = mInfo.getApkFileName();
FileOutputStream fos = mContext.openFileOutput(fn, Context.MODE_WORLD_READABLE);  
fos.write(fileData);
fos.close();

しかし、MODE_WORLD_READABLEはAPI Level 17でdeprecatedとなっていて、今後はこのモードは使うことができないようです。確かにできれば、使いたくないモードです。世界中から読めちゃうモードですからね…(笑)

しかも、openFileOutputでオープンできるのは、アプリ領域の直下のみで、新たにディレクトリを作成しても、そこにこのモードのファイルを作成することもできませんので、使い勝手が悪いです。通常のJavaのファイルオープンではこのモードを指定することができません。
ということで、最近はエクスターナルストレージへ保存することにしています。

パーミッションがないとインストーラーが「パッケージの解析中にエラーが発生しました。」とエラーを表示します。またこのエラーは、異なる署名をもつ同一アプリをインストールしようとしたときも発生しますので、複数の開発者がビルドしている環境ではご注意ください(2013/06/25追記)。

アプリからインストーラーを起動

今起動しているアプリから自分自身を書き換えるって、可能なの?って思ったりしますが、インストーラーを呼び出せば、自身のアプリを一旦終了させて、インストール後、開くかと聞いてくれるので、大丈夫です。

インストーラーの呼び出しは以下のように行います。

static final String APK_MIME_TYPE = "application/vnd.android.package-archive";

String fn = mApp.getApkFileName();
fn = mContext.getFileStreamPath(fn).getAbsolutePath();
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setDataAndType(Uri.fromFile(new File(fn)), APK_MIME_TYPE);
mContext.startActivity(intent);

ちなみに、アンインストールは以下のように行います。

Uri uri = Uri.fromParts("package", mApp.getPackageName(), null);
Intent intent=new Intent(Intent.ACTION_DELETE,uri);
mContext.startActivity(intent);        

現在のアプリのバージョン番号を取得

アプリのバージョンが知りたければ、PackageMangerからパッケージ名で検索して、戻り値のgetVersionCodeを参照します。
versionNameは比較的自由に設定できる値ですが、versionCodeは整数値で、新しいものほど大きな値となるので、こちらを利用しています。

PackageInfo packageInfo info = ctx.getPackageManager().getPackageInfo(packageName, PackageManager.GET_RECEIVERS);
int versionCode = packageInfo.versionCode;
String versionName = packageInfo.versionName;

アプリのインストール・アンインストールを検知

これはあまり必要ないかもしれませんが、アプリがインストールされたり、アンインストールされたことは、BroadcastReceiverを使って検知することができます。たとえば、インストールされているアプリを一覧していて、新たにアプリがインストールされたとき、一覧を更新したい場合があるでしょう。そんなときはこのBroadcastReceiverを登録しておくとよいです。
以下はActionBarSherlockのサンプルに含まれているものです。com.actionbarsherlock.sample.fragments.LoaderCustomSupportクラスで実装されています。

    /**
     * Helper class to look for interesting changes to the installed apps
     * so that the loader can be updated.
     */
    public static class PackageIntentReceiver extends BroadcastReceiver {
        final AppListLoader mLoader;

        public PackageIntentReceiver(AppListLoader loader) {
            mLoader = loader;
            IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED);
            filter.addAction(Intent.ACTION_PACKAGE_REMOVED);
            filter.addAction(Intent.ACTION_PACKAGE_CHANGED);
            filter.addDataScheme("package");
            mLoader.getContext().registerReceiver(this, filter);
            // Register for events related to sdcard installation.
            IntentFilter sdFilter = new IntentFilter();
            sdFilter.addAction(IntentCompat.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE);
            sdFilter.addAction(IntentCompat.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE);
            mLoader.getContext().registerReceiver(this, sdFilter);
        }

        @Override public void onReceive(Context context, Intent intent) {
            // Tell the loader about the change.
            mLoader.onContentChanged();
        }
    }

このインスタンスを生成すると、レシーバーが登録されます。

                mPackageObserver = new PackageIntentReceiver(this);

監視が必要なくなれば、登録を解除します。サンプルではAppListLoaderクラスのonResetハンドラーで行っています。詳細はサンプルコードを確認頂く方がよいでしょう。

        /**
         * Handles a request to completely reset the Loader.
         */
        @Override protected void onReset() {
                 super.onReset();
                 getContext().unregisterReceiver(mPackageObserver);
        }

アプリ自身でアプリをアップデートする手順は以上です。他にもやらなければいけない細かなことはありますが、それほど大変というわけではありませんので、是非ご自身でも実装してみてください。この機能があれば、バグがあっても、後ほどなんとか修正できるというメリットがあります(笑)。

【PR】

アンドロイドアプリの開発・キッティング作業でお困りの方、ぜひ弊社へご相談ください。
弊社ではアンドロイドアプリ、ウェブアプリの開発だけではなく、キッティング作業も請け負っています。