【Swift・Android】MobileBackendを利用したアプリの強制アップデート


#動作概要
iOSアプリ、Androidアプリどちらにも対応出来るようにする。
アプリ起動時に自身のバージョンと強制更新バージョン比較
⇒強制更新バージョン以下の場合は更新ダイアログを出す。

同時にメンテナンスモードフラグを確認して、サーバのメンテナンス中は操作不可にしたい。
とりあえず保留

・Androidはバージョン名よりバージョンコードを使ったほうが整数値でやりやすいですけど、iOS版でどうせバージョン名比較するのでわかりやすい方で実装します。

#システム構成
![Kobito.mDb9l3.png](https://qiita-image-store.s3.amazonaws.com/0/69919/a7facb1f-5a2a-a1af-30b2-e7646d796e78.png "Kobito.mDb9l3.png")




#サーバ側構成
自前のサーバに構築してもいいのですが、今回はmobilebackendのデータストアを利用します。
http://mb.cloud.nifty.com/function.htm#datastore

利用したいのはデータをブラウザから編集してアプリがそれを確認出来たらいいだけなので、パッと実装するには楽なサービスです。
初期設定等は公式のクイックスタートを見てみて下さい。
http://mb.cloud.nifty.com/doc/current/
不満はフィールドの入れ替えが出来ないところ。途中に新規フィールドが挿入出来ないので修正のために全部フィールド削除したりしなきゃいけないです。
なんとかしてください。フィールド名の変更ができればまだいいんですけど・・・

##構成例
systemテーブルを作成して、フィールドを追加、とりあえず値を入れた状態です。

![Kobito.XYC4Gk.png](https://qiita-image-store.s3.amazonaws.com/0/69919/7742918b-04bc-0f82-32c5-a497df499b31.png "Kobito.XYC4Gk.png")

・ObjectId　自動生成
・os ObjectIdで取得してもいいんですけど、こっちのほうがわかりやすいかな
・forceUpdateVersion 強制アップデートをするバージョン

サーバ側の準備はこれで完了。
あとはテストするときにブラウザからforceUpdateVersionをいじってテストするだけです。

#Android自動更新処理
###サーバとの通信部分
```java:MobileBackend.java
package com.maruno_ph.homeapp.common;

import android.app.Activity;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.net.Uri;
import android.util.Log;

import com.maruno_ph.homeapp.R;
import com.maruno_ph.homeapp.init.LaunchScreenActivity;
import com.nifty.cloud.mb.FindCallback;
import com.nifty.cloud.mb.NCMB;
import com.nifty.cloud.mb.NCMBException;
import com.nifty.cloud.mb.NCMBInstallation;
import com.nifty.cloud.mb.NCMBObject;
import com.nifty.cloud.mb.NCMBPush;
import com.nifty.cloud.mb.NCMBQuery;
import com.nifty.cloud.mb.RegistrationCallback;

import java.util.ArrayList;
import java.util.List;

public class MobileBackend {

    ArrayList columns = new ArrayList<String>();
    Activity callFromActivity;
    
    /* production
    final String API_KEY = "";
    final String CL_KEY = "";
     */

    /* dev production
    final String API_KEY = "";
    final String CL_KEY = "";
    */

    /* dev devlopment    */
    final String API_KEY = "yourAppKey";
    final String CL_KEY = "yourAppClKey";


    public MobileBackend(Activity callFromActivity){
        this.callFromActivity = callFromActivity;
        NCMB.initialize(callFromActivity,API_KEY,CL_KEY);
    }

	
    /**
     * 強制更新チェック
     * @param versionName format[x.x.x]
     */
    public void checkForceUpdate(final String versionName){
        NCMBQuery<NCMBObject> query = NCMBQuery.getQuery("system");
        query.whereEqualTo("os", "Android");
        query.findInBackground(new FindCallback<NCMBObject>() {
            @Override
            public void done(List<NCMBObject> result, NCMBException e){
                if(result == null){
                    //ネットワークエラー処理
                }else if (result.isEmpty() != true){
                    //バージョン比較
                    String forceUpdateVersion = (String)result.get(0).get("forceUpdateVersion");
                    String[] targetVersion = forceUpdateVersion.replace('.',',').split(",");
                    String[] currentVersion = versionName.replace('.',',').split(",");


                    //バージョン番号が3桁を超えることはまずない。
                    long targetVersionCd = (
                                            Integer.parseInt(targetVersion[0]) * 1000 + Integer.parseInt(targetVersion[1])
                                            ) * 1000 + Integer.parseInt(targetVersion[2]);

                    long currentVersionCd = (
                                            Integer.parseInt(currentVersion[0]) * 1000 + Integer.parseInt(currentVersion[1])
                                            ) * 1000 + Integer.parseInt(currentVersion[2]);


                    if(currentVersionCd >= targetVersionCd){
                        return;
                    }

                    Log.d("targetVersionCd", String.valueOf(targetVersionCd));
                    Log.d("currentVersionCd", String.valueOf(currentVersionCd));

                    //バージョン更新
                    AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(callFromActivity);
                    alertDialogBuilder.setCancelable(false);
                    alertDialogBuilder.setTitle("更新情報");
                    alertDialogBuilder.setMessage("最新版にアップデートしてご利用下さい");
                    alertDialogBuilder.setPositiveButton("はい",
                            new DialogInterface.OnClickListener() {
                                @Override
                                public void onClick(DialogInterface dialog, int which) {
                                    Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=yourApp"));
                                    try {
                                        callFromActivity.startActivity(intent);
                                    } catch (Exception e) {
                                        e.printStackTrace();
                                    }

                                    callFromActivity.finish();
                                }
                            });

                    alertDialogBuilder.show();

                } else {
                    //サーバエラー処理
                }
            }
        });

    }
}

```

checkForceUpdateメソッドがちょっと長くなってるので汎用化出来そうなメソッドを共通部分に移したほうがいいかも。

###呼び出すActivity
```java:MainActivity.java
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
     if( getIntent().getBooleanExtra("isCheckUpdate",false)){
        MobileBackend mobileBackend = new MobileBackend(this);
        mobileBackend.checkForceUpdate(Method.getVersion(getApplicationContext()));
     }
}
```

###共通部分
```java:Method.java
    /**
     * バージョンを取得する
     */
    public static String getVersion(Context context){
        PackageManager pm = context.getPackageManager();
        String versionName = "";
        try{
            PackageInfo packageInfo = pm.getPackageInfo(context.getPackageName(), 0);
            versionName = packageInfo.versionName;
        }catch(PackageManager.NameNotFoundException e){
            e.printStackTrace();
        }
        return versionName;
    }
```

起動画面でputExtraして呼び出し元が起動画面かどうかをチェックしてます。
本当はgetCallingActivityでクラス名が取得したかったんですが、おそらくFLAG_ACTIVITY_CLEAR_TOPで情報が削除されてしまうので仕方なく。
###結果
起動画面後にダイアログが表示されて、メイン画面の上でこんな感じになりました。
![Kobito.GdiOPM.png](https://qiita-image-store.s3.amazonaws.com/0/69919/d6655351-7fc0-3393-14f9-437c8fe3b768.png 
"Kobito.GdiOPM.png")
バックキーも無効にしてあるので、はいを押してアプリが終了してストアに接続します。

気が向いたらちゃんとしたレイアウトに変えようと思いますが、とりあえずはこれでいいかな。

#iOS(Swift)自動更新処理
Androidと同じような感じで作っていきます。
##サーバからデータを取得
```swift:.swift
import Foundation
import NCMB

class MobileBackend{
    var apiURL:String
    var columns:[String] = Array()
    /* production
    let API_KEY = ""
    let CL_KEY = ""
 */
    
    /* dev production
    let API_KEY = ""
    let CL_KEY = ""
    */

    /* dev devlopment   */
    let API_KEY = "yourAppKey"
    let CL_KEY = "yourAppClKey"

    
    init(){
        apiURL = MobileBackendConfig.Pharmacy.url + MobileBackendConfig.Pharmacy.apiVersion
        NCMB.setApplicationKey(API_KEY, clientKey: CL_KEY)
    }
        
    /**
    強制更新チェック
    */
    func checkForceUpdate(view:UIViewController){
        let versionName: String = NSBundle.mainBundle().objectForInfoDictionaryKey("CFBundleShortVersionString") as! String
        
        let query = NCMBQuery(className: "system")
        query.whereKey("os", equalTo: "iOS")
        query.findObjectsInBackgroundWithBlock({(NSArray objects, NSError error) in
            if (error == nil) {
                if(objects.count > 0) {
                    //バージョン比較
                    let currentVersion = versionName.componentsSeparatedByString(".")
                    var forceUpdateVersion = objects[0].objectForKey("forceUpdateVersion") as! String
                    let targetVersion = forceUpdateVersion.componentsSeparatedByString(".")
                    
                    //バージョン番号が3桁を超えることはまずない。
                    let targetVersionCd = (targetVersion[0].toInt()! * 1000 + targetVersion[1].toInt()!) * 1000 + targetVersion[2].toInt()!;
                    let currentVersionCd = (currentVersion[0].toInt()! * 1000 + currentVersion[1].toInt()!) * 1000 + currentVersion[2].toInt()!;
                    if(currentVersionCd >= targetVersionCd){
                        return
                    }
                    Alert.showSimpleAlert("更新情報",
                        message: "最新版にアップデートしてご利用ください",
                        defaultTitle: "はい",
                        view: view,
                        handler:{
                            (action:UIAlertAction!) -> Void in
                            let itunesURL:String = "itms-apps://itunes.apple.com/app/1007874407"
                            let url = NSURL(string:itunesURL)
                            let app:UIApplication = UIApplication.sharedApplication()
                            app.openURL(url!)
                            
                            Alert.showSimpleAlert("確認",
                                message: "最新版にアップデートしましたか？",
                                defaultTitle: "はい",
                                view: view,
                                handler:{
                                    (action:UIAlertAction!) -> Void in
                                    self.checkForceUpdate(view)
                            })
                            
                            
                    })
                    
                }else{
                    println("MobileBackend.get count 0")
                }
            }else{
                println("MobileBackend.get error occur")
            }
        })
    }
    
}
```
iOSはアプリを終了するとリジェクト対象になるようなので、ダイアログを出して再確認するようにしました。
再帰が深くなるとクラッシュすると思いますが、いい方法が思い浮かばなかったので・・・
更新しないで使われてデータが壊れるよりはいいかなってかんじです。
※Alert.showSimpleAlertメソッドはアラートをラップしてるだけなので、標準のダイアログを出してるだけです。

##呼び出すView
```swift:MainViewController.swift
    override func viewDidLoad() {
        super.viewDidLoad()
        
        //強制更新チェック
        if(AppDelegate.isInit){
            let mobileBackend = MobileBackend()
            mobileBackend.checkForceUpdate(self)
            AppDelegate.isInit = false
        }
    }
```

起動時の判定は一番簡単にAppDelegateに変数をもたせました。

```swift:AppDelegate.swift
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
    static var isInit = true
```

##結果
![Kobito.00DAdH.png](https://qiita-image-store.s3.amazonaws.com/0/69919/9545ffe5-50ff-e486-f6d8-d612f81e4752.png "Kobito.00DAdH.png")

はいでAppStoreに飛ばされる
もう一回表示したら

![Kobito.Wv0QK5.png](https://qiita-image-store.s3.amazonaws.com/0/69919/5815e457-7d7d-c775-df2d-27c2d37e8a61.png "Kobito.Wv0QK5.png")

更新チェックに行って、更新しないと永遠とループする。

#参考
【Android】アプリのバージョン名とバージョン番号を取得する
http://qiita.com/katsuhisaishii/items/25c885fe7e60a2273395
iOSアプリのバージョンを取得する
http://qiita.com/arthur87/items/802d17387ae46fb44fc2

[iOS/Android]公開アプリのバージョンを取得してアップデートを促す方法
http://ameblo.jp/rhythmicallife/entry-11955421850.html

[Swift]URLスキームを使ってAppStoreのレビューページに飛ばす方法
http://qiita.com/naoyashiga/items/09d9947880f467ed4422

http://qiita.com/hiroo0529/items/73d52829bd36e3324ee0