2015年5月7日木曜日

Gralleryから選択した画像のMD5値を取得する


GralleryにIntentを投げて選択した画像が過去に選択したものかを判断するためにMD5値を計算してそれを比べることにした。

まず、GralleryにIntentを投げる。
 @Override
    protected void onStart() {
        super.onStart();
        Intent intent = new Intent();
        intent.setType("image/*");
        intent.setAction(Intent.ACTION_GET_CONTENT);
        startActivityForResult(intent, 100);
    }

onActivityResultにて帰ってきたIntentからMD5値を計算する。
 @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        if (requestCode == 100 && resultCode == RESULT_OK) {
            try {
                ContentResolver contentResolver = getContentResolver();

                // Intentより画像ファイルのパスを取得する
                String[] columns = { ImageColumns.DATA };
                Cursor cursor = contentResolver.query(data.getData(), columns, null, null, null);
                if(cursor==null) return;
                cursor.moveToFirst();
                String path = cursor.getString(cursor.getColumnIndex(ImageColumns.DATA));
                
                // ファイルを読んでMD5値を計算する
                FileInputStream fis = new FileInputStream(path);

                MessageDigest md = MessageDigest.getInstance("MD5");
                DigestInputStream dis = new DigestInputStream(fis, md);

                byte [] buffer = new byte[1024];
                while(dis.read(buffer) == 1024) { }

                byte[] digest = md.digest();
                StringBuilder md5 = new StringBuilder();
                for (int i = 0;i < digest.length;i++) {
                    md5.append(Integer.toHexString(digest[i] >> 4 & 0xf));
                    md5.append(Integer.toHexString(digest[i]& 0xf));
                }
                Log.v(TAG, md5.toString());

                fis.close();
                cursor.close();
            } catch (Exception e) {

            }
        }
    }

2015年3月29日日曜日

Bundleに頼らないBitmapの扱い方

必要になった背景

  • Nudiaという画像編集アプリを作ったが、!!! FAILED BINDER TRANSACTION !!!が頻発した
  • 画像を利用するActivityやFragmentそれぞれでメンバ変数にBitmapを保持しており、それら全てでBundleにBitmapを格納していたことが原因
Bundleに保存していると、アプリがシステムから落とされた場合でも、データをリストアすることできるのでとても便利ではある。しかし、Bungleに格納できるデータ容量は有限のため、Bundleに頼らないBitmapの保持方法を考えなければいけない。

考えた案

  1. 編集都度ファイルに保存し、必要な度ファイルからBitmapを読み込む
  2. Bitmapをホストするシングルトンクラスを作りそこを通してアクセスするようにする
1については頻繁に更新読み込みをするため現実的ではないので不採用。消去法的に2になるが一つ問題が出てきた。
その問題とはシングルトン自体がシステムから解放されてしまった場合、ホストされているBitmapも一緒にメモリから消えてしまうことだった。


この問題に対して次の図のように対応をした。
シングルトンとホストされるBitmapは変わらないが、メモリが少なくなってきたこと等をトリガとしてアプリのデータ領域にBitmapのキャッシュを持つようにした。
また、アプリのシステムからの終了後の復帰時にはホストするBitmapがnullであった場合にキャッシュからデータを読み込む様にした。

いまのところ差し当たり問題はないが、今後他のユースケースで問題が出てくるかもしれないので注意が必要。


2015年3月25日水曜日

著作権違反によるアプリリジェクト

そういえば、アプリを公開した後いつまで経ってもGooglePlayの検索結果に現れないためおかしいと思ってデベロッパーコンソールを見てみたところ・・・


リジェクトされていましたwww

以下Googleからの通告内容


下記の問題を解決し、修正したバージョンを公開してください。
削除の理由: コンテンツ ポリシーの知的財産権条項となりすましまたは虚偽の振る舞い条項への違反。詳しくは、知的財産権の侵害なりすましに関するポリシーについてのヘルプ記事をご覧ください。
  • アプリ、または Google Play ストアに掲載されている情報(タイトル、説明、ロゴ、プロモーション用スクリーンショットなど)で、第三者に帰属する保護対象の著作物を不正に使用してはなりません
  • アプリのアイコンやプロモーション用スクリーンショットに、既存のサービスに類似した紛らわしい画像を使用してはなりません
保護対象の著作物には一般に、商品名、ブランド、画像、ロゴ、音楽などの作品が含まれます。
ポリシーに準拠するように、アプリやその掲載情報を変更してください。そのためには、アプリやその掲載情報から、保護対象の著作物に該当する可能性のあるすべてのコンテンツを削除します。
こうしたコンテンツを利用する許可を得ている場合は、Google Play ヘルプセンターからご連絡いただき、許可内容が確認できる正式な証明書をお送りください。
削除はすべて追跡の対象となり、削除が繰り返されるとアプリの公開停止につながります。アプリの停止はデベロッパー アカウントのステータスに影響を与える要因となり、Google Play を利用できなくなる可能性がある点にご注意ください。
この通知は、ご自身が公開中の他のアプリに対する事前通知でもあります。カタログ内の他のアプリ全てが上記のポリシー(ただしこれに限定されません)に違反していないことを直ちに確認することで、今後他のアプリで同様のアップロードの不承認やアプリの停止が生じることを回避できます。アプリを公開する前に、アプリがデベロッパー販売 / 配布契約コンテンツ ポリシーに準拠していることをご確認ください。
今回の判断が誤りであると思われる場合は、こちらの Google Play ヘルプセンターの記事をご利用ください。

どうやらアプリを開いたときに出るトップ画面に操作説明のための入れていた画像が著作権違反となったようでした。
別の著作権が寛容な画像に差し替えたところ無事に公開されました。

Appleのように人手では審査はしていないと思われますので、おそらくアップしたApkを展開して中の画像をGoogleお得意の画像検索をして著作権違反を自動的に調べているのでしょうか?

まあひとまず公開ができたので良かったですが、著作権のあたりもしっかりケアしないといけないという良い教訓になりました。





AndroidアプリNudia公開

ようやく以前より作っていたアプリが完成しました。

Android上で画像を編集するアプリです。
主な編集用途は水玉コラを作るものなのですが、水玉コラを作る前に画像に好きなように書き込みをできるようにしています。

そのため、水着を肌色に塗ってぼかしを入れて水玉コラにすると裸のように見える!?・・・と思います。

是非インストールしてみてください。
https://play.google.com/store/apps/details?id=com.nsunrise.nudia




"201306251500" by Don Le is licensed under CC BY 2.0

2015年3月21日土曜日

FragmentのonCreateOptionsMenuでMenuを追加するときの注意点


あるFragmentを持つActivityがあるとする。
Menuの追加処理は必ずしもActivityだけではなくてFragment側でもonCreateOptionsMenuをオーバライドしてやればできる。
これを利用すると、Fragmentの切り替え時にそのFragmentに合ったMenuを表示することができる。

そのときの注意点。

ダメな例
 
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
    super.onCreateOptionsMenu(menu, inflater);
    inflater.inflate(R.menu.main, menu);
}

正しい例
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
    super.onCreateOptionsMenu(menu, inflater);
    if(!menu.hasVisibleItems())
        inflater.inflate(R.menu.main, menu);
}
menu.hasVisibleItems()で既にmenuがあることを確認しないと、FragmentがaddされたときにMenuがどんどん増えてしまう。
addする前にremoveしていれば既に追加したMenuは消えてくれるが、removeせずにaddを呼ぶ場合などは注意が必要。


2015年3月1日日曜日

マルチタッチではMotionEvent.ACTION_MASKでマスクする


ScrollViewでマルチタッチを検出したくてScrollViewを継承したカスタムビューを作り、 onInterceptTouchEventにてMotionEvent.ACTION_POINTER_DOWNを待ち構えたが 何故かイベントがキャッチされない。 
調べてみたところ、マルチタッチの場合はMotionEventのactionの値をMotionEvent.ACTION_MASKで マスクしてやらなければいけないらしい。

ダメな書き方
 public boolean onInterceptTouchEvent(MotionEvent event) {
    int action = event.getAction();
    switch (action) {
        case MotionEvent.ACTION_POINTER_DOWN:
            Log.v(TAG, "Action_pointer_down");
            break;

        case MotionEvent.ACTION_POINTER_UP:
            Log.v(TAG, "Action_pointer_up");
            break;
    }
    return false;
}

正しい書き方
 public boolean onInterceptTouchEvent(MotionEvent event) {
    int action = event.getAction();
    switch (action & MotionEvent.ACTION_MASK) {
        case MotionEvent.ACTION_POINTER_DOWN:
            Log.v(TAG, "Action_pointer_down");
            break;

        case MotionEvent.ACTION_POINTER_UP:
            Log.v(TAG, "Action_pointer_up");
            break;
    }
    return false;
}

なぜこんなことをしなければいけないかはAndroid Developerにちゃんと書いてあった。
It returns the masked action being performed, without including the pointer index bits.

つまり、actionの値にはポインターのインデックスを示すbitがあり、マスクしないとそこの値も含めた値になってしまうためそれぞれのアクションの条件値と異なってしまうということらしい。


2014年8月20日水曜日

AndroidでOpenCVを使って背景差分を取得する

Androidで画像に移った被写体との背景差分を取りたくて色々調べてみたところOpenCVというライブラリを使えばできることがわかった。

背景差分処理前の画像

背景差分処理後の画像

作ったサンプルコードは以下の通り

public class MainActivity extends Activity {
 static {
        if (!OpenCVLoader.initDebug()) {
            // Handle initialization error
        } else {
        }
    }
 private final static String TAG = "BGsubstractorTest";
 @Override
 protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_main);

  if (savedInstanceState == null) {
   getFragmentManager().beginTransaction()
     .add(R.id.container, new PlaceholderFragment()).commit();
  }
 }

 @Override
 public boolean onCreateOptionsMenu(Menu menu) {
  getMenuInflater().inflate(R.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);
 }

 /**
  * A placeholder fragment containing a simple view.
  */
 public static class PlaceholderFragment extends Fragment {
  ImageView mImageView;

  public PlaceholderFragment() {
  }

  @Override
  public View onCreateView(LayoutInflater inflater, ViewGroup container,
    Bundle savedInstanceState) {
   View rootView = inflater.inflate(R.layout.fragment_main, container,
     false);
   return rootView;
  }
  
  @Override
  public void onActivityCreated(Bundle savedInstanceState) {
   super.onActivityCreated(savedInstanceState);
   
   Mat input = new Mat();
   BackgroundSubtractorMOG mog = new BackgroundSubtractorMOG();
   Mat output = new Mat();
   
   Bitmap bitmap1 = loadBitmap(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM) + "/Camera/IMG_20140818_000833235.jpg");
   Utils.bitmapToMat(bitmap1, input);
   Imgproc.cvtColor(input, input, Imgproc.COLOR_RGBA2RGB);
   
   mog.apply(input, output);
   
   Bitmap bitmap2 = loadBitmap(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM) + "/Camera/IMG_20140818_000849955.jpg");
   Utils.bitmapToMat(bitmap2, input);
   Imgproc.cvtColor(input, input, Imgproc.COLOR_RGBA2RGB);
   
   mog.apply(input, output);
   
   Bitmap bitmap3 = Bitmap.createBitmap(input.width(), input.height(), Bitmap.Config.ARGB_8888);
   Utils.matToBitmap(output, bitmap3);
   refreshImage(bitmap3);
  }
  
  private Bitmap loadBitmap(String imagefilePath) {
   BitmapFactory.Options options = new BitmapFactory.Options();
         options.inSampleSize = 4;
         options.inPreferredConfig = Config.ARGB_8888;
   Bitmap bm = BitmapFactory.decodeFile(imagefilePath, options);
   return bm;
  }
  
  private void refreshImage(Bitmap bmp) {
      mImageView = (ImageView)getActivity().findViewById(R.id.imageView1);
      mImageView.setImageBitmap(bmp);
  }
 }
}

つまづいた所としてはBackgroundSubtractorMOGに読み込ませる画像がRGBA形式だとエラーになってしまう。
そのため一度RGB形式に直してやらないとうまく動かない。

Imgproc.cvtColor(input, input, Imgproc.COLOR_RGBA2RGB);