1. FukMo10

    Posted

    FukMo10
Changes in title
+AndroidStudioでプッシュ通知 撮影した画像を自動で共有するカメラアプリを作ってみた
Changes in tags
Changes in body
Source | HTML | Preview
@@ -0,0 +1,575 @@
+* 写真を撮影すると自動でプッシュ通知を送信
+* 受信したプッシュ通知をタップすると自動的に画像が表示される
+
+な写真共有カメラアプリを作っていきます。
+Android Studioでプロジェクトにgoogle Play Servicesを設定する方法なんかも書いてあります。
+
+
+実装にはmBaaSの一つである、[ニフティクラウド mobile backend](http://mb.cloud.nifty.com/)のプッシュ機能を使っていきます。
+これを利用すると、プッシュ通知のためにサーバを立てたり、GCMやAPNsとの連携を自分で作ったりしなくていいので、簡単にプッシュ通知を実装できます。しかも無料で使える。
+(※そもそもmBaaSとは?→http://mb.cloud.nifty.com/about.htm)
+
+特にmobile backendのプッシュ通知にはリッチプッシュ機能というものがあり、プッシュ通知をタップすると指定したURLに自動でアクセスするように設定することができます。
+今回はこの機能を利用していきます。
+
+
+# 実装の流れ
+
+## 下準備
+
+1. mBaaSの登録(無料のプランで問題ないです)→http://mb.cloud.nifty.com/
+2. Android Studioのインストール・セットアップ(省略)
+3. [Google Developers Console](https://console.developers.google.com/)でのAPIkey取得(省略)
+
+それぞれ詳細は省略しますが、3は以下のドキュメントがわかりやすいです。
+http://mb.cloud.nifty.com/doc/current/tutorial/push_setup_android.html
+ここで取得するAPIkeyの他にプロジェクト番号(project number)も後で使うので、メモ帳にでもコピペしておくと楽です。
+(プロジェクト番号はOverview画面の一番上にある数字だけの方です。)
+
+## アプリ作成
+
+アプリは以下のような流れで作成します。
+
+1. シンプルなカメラアプリを作成する
+2. 撮影した画像をmBaaSのファイルストアに保存させる
+3. Google Play Servicesをプロジェクトに設定する 
+4. プッシュ通知の受信端末情報を登録する
+5. リッチプッシュを動かすためのカスタムレシーバーを作る
+6. 撮影時にプッシュ通知を送信する
+
+1,2については[前回の記事](http://qiita.com/heppoko_dev/items/e2dc82d766c2804de391)で作成したものをそのまま使うので、ここでは省略します。
+
+### 3. Google Play Servicesをプロジェクトに設定する 
+
+プッシュ通知を使うにはgoogle Play Serviceと連携する必要があります。
+Android Studioでは以下のようにしてプロジェクトに設定します。
+(もちろん[公式ドキュメント(英語)](https://developers.google.com/android/guides/setup)を参考にしてセットアップしていただいても大丈夫です。)
+
+1. SDK Managerで「google Play services」をDLする(下の方にあります)
+2. File-> Project Structure -> Dependanciesタブを開いて左下の「+」をクリック
+3. 1 library dependancyからplay-serviceを選択して「OK」
+これで登録されます。(しばらくライブラリの連携でぐるぐる動きます。)
+ただ、これだけだとうまく連携されない場合があるので、その場合は「sync Project with Gradle Files」ボタンを押すと動くようになります。(アイコンバーのデバイスマネージャーの左隣にある丸と下矢印のようなマークです。)
+
+### 4. プッシュ通知の受信端末情報を登録する
+
+プッシュ通知を受け取るためには、端末の情報を登録する必要があるので、その部分を実装していきます。
+公式のドキュメントがあるので、主にこちらを参照してください。
+前回同様最終系のコードを最後にまとめて掲載するので、分かりにくい場合はそちらを見ながらやってみてください。
+
+[チュートリアル (Android) : Android端末にプッシュ通知を行う | ニフティクラウド mobile backend](http://mb.cloud.nifty.com/doc/current/tutorial/android_push.html)
+
+この段階では「Push通知を有効にする」の項目までやればOKです。
+また、「mobile backendのAPIキー追加」は既に前回終わっているので不要です。
+
+なので、やることは
+
+* ダッシュボードへのAPIkeyの入力
+* AndroidManifest.xmlへのパーミッションとreceiverタグの追加
+* MainActivity.javaのonCreateに端末情報登録のコードを追加
+
+の3つだけです。
+[「Push通知を有効にする」](http://mb.cloud.nifty.com/doc/current/tutorial/android_push.html#Push通知を有効にする)で「SENDER_ID」を置き換え忘れないように注意してください。
+(Google Developers Consoleで取得したプロジェクト番号です。)
+
+余裕があれば「ダッシュボードからプッシュ通知を送信する」でプッシュ通知が届くか動作確認してみてください。
+この時点ではタップしても何も起こりません。
+
+### 5. リッチプッシュを動かすためのカスタムレシーバーを作る
+
+プッシュ通知をタップしたときアクションを実装していきます。
+こちらも公式ドキュメントを基に実装していきますが、少し情報が足りない部分があるので、補足していきます。
+
+まず、AndroidManifest.xmlに以下の設定を追加します。
+先ほど追加したreceiverタグとはまた別物なので、その下にでも追加してください。
+
+```
+<receiver android:name="com.example.username.ncmbcamera.MyCustomReceiver">
+<intent-filter>
+ <action android:name="com.example.username.ncmbcamera.UPDATE_STATUS" />
+</intent-filter>
+
+```
+
+続いてカスタムレシーバーを作成します。
+MainActivity.javaがあるパッケージ名のフォルダを右クリック->New->JavaClassを選択し「MyCustomReceiver.java」を作成します。
+そして以下のコードをコピペしてください。
+
+```
+package com.example.username.ncmbcamera;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.Iterator;
+
+public class MyCustomReceiver extends BroadcastReceiver {
+ private static final String TAG = "MyCustomReceiver";
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ try {
+ String action = intent.getAction();
+ String channel = intent.getExtras().getString("com.nifty.Channel");
+ JSONObject json = new JSONObject(intent.getExtras().getString("com.nifty.Data"));
+
+ Iterator itr = json.keys();
+ while (itr.hasNext()) {
+ String key = (String) itr.next();
+ }
+ } catch (JSONException e) {
+ // エラー処理
+ }
+ }
+}
+
+```
+
+次にMainActivity.javaに以下の二つのメソッドを追加します。
+
+```
+ @Override
+ public void onResume(){
+ super.onResume();
+ NCMBPush.richPushHandler(this, getIntent(), true);
+ }
+
+ @Override
+ public void onDestroy(){
+ super.onDestroy();
+ NCMBPush.closeRichPush();
+ }
+
+```
+
+これでリッチプッシュが受信可能になるので、ダッシュボードからURLを入力したプッシュ通知を送信してみてください。
+届いたプッシュ通知をタップするとアプリが起動して、webviewで指定したURLのページが表示されるはずです。
+
+### 6. 撮影時にプッシュ通知を送信する
+
+アプリからプッシュ通知を送信する部分を実装していきます。
+file.saveInBackgroundのコールバック関数の中(今回のコードではdoneメソッド)に以下のコードを追加します。
+
+```
+NCMBPush push = new NCMBPush();
+push.setMessage("Picture shared!"); //プッシュが届いた時に表示されるメッセージ
+String applicationID = "YOUR_APPLICATION_ID";
+String url = "https://mb.api.cloud.nifty.com/2013-09-01/applications/" + applicationID + "/publicFiles/" + fileName;
+try{
+ JSONObject json= new JSONObject("{\"target\": [android]}");
+ push.setData(json);
+}catch(JSONException e1) {
+//エラー処理
+}
+push.setRichUrl(url);
+push.sendInBackground();
+
+```
+
+また、以下の2点について設定を行います。
+
+今回は画像を共有する手段としてmBaaSの公開ファイル機能を使いました。
+これを利用することで、URLからファイルにアクセスできるようになります。
+
+この機能を使うためには、**ダッシュボードの「アプリ設定」->「データ・ファイルストア」で「HTTPSでの取得」を有効にして保存ボタンを押してください**。
+もしクラスのパーミッション画面が出た場合には、全員に対して読み込み権限を許可に設定してください。
+→[ダッシュボードの使い方:ファイルストア](http://mb.cloud.nifty.com/doc/current/dashboard/filestore.html#ファイルクラスのパーミッションを設定する)
+
+公開ファイル機能でファイルにアクセスするURLは以下のようになります。
+https://mb.api.cloud.nifty.com/2013-09-01/applications/アプリケーションID/publicFiles/ファイル名
+ここでアプリケーションIDとは、ダッシュボードを開いてる時に/applications/の後ろについている文字列です。
+**上のコードの中の「YOUR_APPLICATION_ID」を自分のアプリケーションIDに置き換えてください。**
+
+
+以上で実装完了です。
+アプリで写真を撮影したらプッシュ通知が届くはずです。
+
+### 注意
+
+バグがあってアプリが起動中の状態でプッシュ通知をタップしても画像が表示されないようです。
+一台の端末で試す場合は一度アプリを終了してからプッシュ通知をタップしてください。
+
+また、カメラアプリそのものの部分をちゃんと作っていないので、設定によっては表示される画像が小さい、90度回転しているなどの不具合があるかもしれません。
+時間が出来たら直したいですが、気になる方がいたら直してコメントいただけると大変助かります。
+(そもそもCameraクラスが非推奨だったりしますが。。。)
+
+# まとめ
+
+今回はmBaaSのリッチプッシュ通知機能を使って画像を共有するアプリを作りました。
+このままだと写真を撮ったら必ずプッシュ通知が送られてしまいますが、もう少し作り込めば共有するかどうかを選択することもできます。
+また、[会員管理機能](http://mb.cloud.nifty.com/doc/current/fnguide/user.html)と組み合わせれば、グループや相手を指定して共有することも可能です。
+プッシュの出し分けが簡単にできるのもmBaaSのメリットの一つですね。
+
+
+余裕があれば次回はその辺りまで作り込んでみたいと思います。
+
+# 完成系のコード(前回から変更があるもののみ)
+
+MainActivity.java
+
+```
+package com.example.username.ncmbcamera;
+
+import android.hardware.Camera;
+import android.hardware.Camera.Size;
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.support.v7.app.AppCompatActivity;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.MotionEvent;
+import android.view.SurfaceHolder;
+import android.view.SurfaceView;
+import android.view.View;
+import android.view.View.OnTouchListener;
+import android.view.ViewGroup;
+
+import com.nifty.cloud.mb.NCMB;
+import com.nifty.cloud.mb.NCMBException;
+import com.nifty.cloud.mb.NCMBFile;
+import com.nifty.cloud.mb.NCMBInstallation;
+import com.nifty.cloud.mb.NCMBPush;
+import com.nifty.cloud.mb.ProgressCallback;
+import com.nifty.cloud.mb.RegistrationCallback;
+import com.nifty.cloud.mb.SaveCallback;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.FileOutputStream;
+import java.util.Calendar;
+
+public class MainActivity extends AppCompatActivity {
+ final static private String TAG = "NCMB Camera";
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_main);
+
+ NCMB.initialize(this, "APP_KEY", "CLI_KEY");
+
+     // 今回追加した部分(端末の登録)
+ final NCMBInstallation installation = NCMBInstallation.getCurrentInstallation();
+ installation.getRegistrationIdInBackground("SENDER_ID", new RegistrationCallback() {
+ @Override
+ public void done(NCMBException e) {
+ if (e == null) {
+ // 成功
+ try {
+ installation.save();
+ } catch (NCMBException le) {
+ // サーバ側への保存エラー
+ }
+ } else {
+ // エラー
+ }
+ }
+ });
+ NCMBPush.setDefaultPushCallback(this, MainActivity.class);
+     // ここまで
+
+ if (savedInstanceState == null) {
+ getSupportFragmentManager().beginTransaction().add(R.id.container, new CameraFragment()).commit();
+ }
+ }
+
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ getMenuInflater().inflate(R.menu.menu_main, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ int id = item.getItemId();
+ if (id == R.id.action_settings) {
+ return true;
+ }
+
+ return super.onOptionsItemSelected(item);
+ }
+
+ /**
+ * カメラ撮影用フラグメント
+ */
+ public static class CameraFragment extends Fragment {
+
+ // ------------------------------------------------------------
+ // メンバー変数
+ // ------------------------------------------------------------
+ private Camera camera_; // カメラインスタンス
+ View rootView_; // ルートView
+ SurfaceView surfaceView_; // プレビュー用SurfaceView
+
+ // ------------------------------------------------------------
+ // リスナー
+ // ------------------------------------------------------------
+
+ // Surfaceリスナー
+ private SurfaceHolder.Callback surfaceListener_ = new SurfaceHolder.Callback() {
+ // Surface作成
+ public void surfaceCreated(SurfaceHolder holder) {
+ // カメラインスタンスを取得
+ camera_ = Camera.open();
+ try {
+ camera_.setPreviewDisplay(holder);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ // Surface破棄時
+ public void surfaceDestroyed(SurfaceHolder holder) {
+ // カメラインスタンス開放
+ camera_.release();
+ camera_ = null;
+ }
+
+ // Surface変更時
+ // プレビューのパラメーターを設定し、プレビューを開始する
+ public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
+ Log.d(TAG, "surfaceChanged width:" + width + " height:" + height);
+
+ Camera.Parameters parameters = camera_.getParameters();
+
+ // デバッグ用表示
+ Size size = parameters.getPictureSize();
+ Log.d(TAG, "getPictureSize width:" + size.width + " size.height:" + size.height);
+ size = parameters.getPreviewSize();
+ Log.d(TAG, "getPreviewSize width:" + size.width + " size.height:" + size.height);
+
+ // プレビューのサイズを変更
+ // parameters.setPreviewSize(width, height); // 画面サイズに合わせて変更しようとしたが失敗する
+ parameters.setPreviewSize(640, 480); // 使用できるサイズはカメラごとに決まっている
+
+ // パラメーターセット
+ camera_.setParameters(parameters);
+ // プレビュー開始
+ camera_.startPreview();
+ }
+ };
+
+ // シャッターが押されたときに呼ばれるコールバック
+ private Camera.ShutterCallback shutterListener_ = new Camera.ShutterCallback() {
+ public void onShutter() {
+ }
+ };
+
+ // JPEGイメージ生成後に呼ばれるコールバック
+ private Camera.PictureCallback pictureListener_ = new Camera.PictureCallback() {
+ // データ生成完了
+ public void onPictureTaken(byte[] data, Camera camera) {
+ // SDカードにJPEGデータを保存する
+ if (data != null) {
+ FileOutputStream fos = null;
+
+ try {
+ Calendar calendar = Calendar.getInstance();
+ int year = calendar.get(Calendar.YEAR);
+ int month = calendar.get(Calendar.MONTH);
+ int day = calendar.get(Calendar.DAY_OF_MONTH);
+ int hour = calendar.get(Calendar.HOUR_OF_DAY);
+ int minute = calendar.get(Calendar.MINUTE);
+ int second = calendar.get(Calendar.SECOND);
+ final String fileName = "" + year + (month + 1) + day + hour + minute + second + ".jpg";
+ System.out.println("fileName: " + fileName);
+
+ NCMBFile file = new NCMBFile(fileName, data);
+ file.saveInBackground(new SaveCallback() {
+ public void done(NCMBException e) {
+ // 今回追加した部分(プッシュの送信)
+ NCMBPush push = new NCMBPush();
+ push.setMessage("Picture shared!"); //プッシュが届いた時に表示されるメッセージ
+ String applicationID = "APPLICATION_ID";
+ String url = "https://mb.api.cloud.nifty.com/2013-09-01/applications/" + applicationID + "/publicFiles/" + fileName;
+ try{
+ JSONObject json= new JSONObject("{\"target\": [android]}");
+ push.setData(json);
+ }catch(JSONException e1) {
+ }
+ push.setRichUrl(url);
+ push.sendInBackground();
+                   //ここまで
+ }
+ }, new ProgressCallback() {
+ public void done(Integer percentDone) {
+ // 進捗状況。1 - 100 のpercentDoneを返す
+ }
+ });
+ }catch (Exception e){
+ System.out.println("error in saveInBackground");
+ e.printStackTrace();
+
+ }
+
+ // プレビューを再開する
+ camera.startPreview();
+ }
+ }
+ };
+
+ // 画面タッチ時のコールバック
+ OnTouchListener ontouchListener_ = new OnTouchListener() {
+ @Override
+ public boolean onTouch(View v, MotionEvent event) {
+ if (event.getAction() == MotionEvent.ACTION_DOWN) {
+ if (camera_ != null) {
+ // 撮影実行
+ camera_.takePicture(shutterListener_, null, pictureListener_);
+ }
+ }
+ return false;
+ }
+ };
+
+ // ------------------------------------------------------------
+ // Fragment
+ // ------------------------------------------------------------
+
+ // Fragmentコンストラクタ
+ public CameraFragment() {
+ }
+
+ // View作成
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ // View作成
+ rootView_ = inflater.inflate(R.layout.fragment_main, container, false);
+
+ // View内のView取得
+ surfaceView_ = (SurfaceView) rootView_ .findViewById(R.id.surface_view);
+
+ // SurfaceHolder設定
+ SurfaceHolder holder = surfaceView_.getHolder();
+ holder.addCallback(surfaceListener_);
+ holder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
+
+ // タッチリスナー設定
+ rootView_.setOnTouchListener(ontouchListener_);
+
+ return rootView_;
+ }
+ }
+
+  // 今回追加した部分(リッチプッシュの起動)
+ @Override
+ public void onResume(){
+ super.onResume();
+ NCMBPush.richPushHandler(this, getIntent(), true);
+ }
+
+ @Override
+ public void onDestroy(){
+ super.onDestroy();
+ NCMBPush.closeRichPush();
+ }
+  // ここまで
+}
+
+```
+
+MyCustomReceiver.java
+
+```
+package com.example.username.ncmbcamera;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.Iterator;
+
+public class MyCustomReceiver extends BroadcastReceiver {
+ private static final String TAG = "MyCustomReceiver";
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ try {
+ String action = intent.getAction();
+ String channel = intent.getExtras().getString("com.nifty.Channel");
+ JSONObject json = new JSONObject(intent.getExtras().getString("com.nifty.Data"));
+
+ Iterator itr = json.keys();
+ while (itr.hasNext()) {
+ String key = (String) itr.next();
+ }
+ } catch (JSONException e) {
+ // エラー処理
+ }
+ }
+}
+
+
+```
+
+AndroidManifest.xml
+
+```
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.example.username.ncmbcamera" >
+
+ <uses-permission android:name="android.permission.CAMERA" />
+ <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+ <uses-permission android:name="android.permission.INTERNET" />
+ <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+
+ <uses-permission android:name="android.permission.GET_ACCOUNTS" />
+ <uses-permission android:name="android.permission.WAKE_LOCK" />
+ <uses-permission android:name="android.permission.VIBRATE" />
+ <uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />
+ <permission android:name="com.example.username.ncmbcamera.permission.C2D_MESSAGE" android:protectionLevel="signature" />
+ <uses-permission android:name="com.example.username.ncmbcamera.permission.C2D_MESSAGE" />
+
+ <application
+ android:allowBackup="true"
+ android:icon="@mipmap/ic_launcher"
+ android:label="@string/app_name"
+ android:theme="@style/AppTheme" >
+ <activity
+ android:name=".MainActivity"
+ android:label="@string/app_name" >
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+ <receiver
+ android:name="com.nifty.cloud.mb.NCMBGCMBroadcastReceiver"
+ android:permission="com.google.android.c2dm.permission.SEND" >
+ <intent-filter>
+ <action android:name="com.google.android.c2dm.intent.RECEIVE" />
+ <category android:name="com.example.username.ncmbcamera" />
+ </intent-filter>
+ </receiver>
+
+ <receiver android:name="com.example.username.ncmbcamera.MyCustomReceiver">
+ <intent-filter>
+ <action android:name="com.example.username.ncmbcamera.UPDATE_STATUS" />
+ </intent-filter>
+ </receiver>
+ </application>
+
+</manifest>
+
+```
+
+
+今回はここまで。
+
+ーー
+ニフティクラウド mobile backend:http://mb.cloud.nifty.com/
+
+ーー