ひらい ぶらり Hi-Library

ぷろぐらみんぐについて。ときどきどうでもいいことについて。

SmartWatch2のapkをAndroidStudioでビルドしてみる #vgadvent2013

この記事はVOYAGE GROUP エンジニアブログ : Advent Calendar 2013の16日目の記事になります。

ガジェッターの皆さんこんにちは。@shinbashiです。
昨今では猫も杓子もスマホスマホですが、スマホといえばあれですよね。

f:id:shin_bashi:20131216232756j:plain

SmartWatch2

これですよね。大体の人はもう持ってると思います。便利ですよねー。
なんせ通知が腕時計でみれますし、他にも通知が腕時計でみれますし、たまにカップラーメン食べるときにタイマーつかえますし。
他にもSmartWatch2からandroid端末のカメラのシャッターを切ったり(僕は写真とりませんが)、雨雲が近づいてきたら知らせてくれたりします(空見ればいいんじゃないかな)。

すごくべんりですよね!

実際個人的にはXperiaZ1は大きすぎてポケットから取り出すのも億劫なので、通知をサッと見れるだけでもすごく便利です。

ガジェッターとしてはここで満足ですが、開発者としてはやっぱり自分でアプリを作りたいですね。
新しいもの好きの諸兄は開発はすでにAndroidStudioで行っていると思います。僕もそうです。
ただしSmartWatch用のSDKがgradleに対応しておらず、ADT(eclipce)用のものしか配布されていません。
そこでどうにかAndroidStudioで開発できないか検証してみました。

そんなわけでこの記事はandroidアプリ開発にAndroidStudioを使っており、eclipceでのSmartWatchの開発環境の構築方法をすでに理解している方向けの超ニッチな内容になっております。

eclipceでの環境構築は

KOBE GDG: Android SmartWatch2を触ってみる 1

こちらに丁寧にまとめられていますので、参考にしてください。

それでは初めて行きたいと思います。
最初に申しておきますが、すごく泥臭いです。

まず、ADTの機能でant用に作られているSmartExtensionAPIとSmartExtensionUtilsをgradle用にエクスポートしません。 そもそも僕の環境でexportがまともに動かないことと、AndroidStudio0.3.7現在モジュールをインポートする機能が無いため下手にエクスポートするとディレクトリ構造がぐっちゃぐちゃになってAndroidStudioで快適に開発するつもりが、逆に面倒になりそうでしたのでスマートなやり方はあきらめました。

\1. 新規にAndroidStudioでプロジェクトを作成。SmartExtensionAPIとSmartExtensionUtilsのsrcやらresやらAndroidManifest.xmlをコピーします

\2. 注意点として

  • SmartExtensionAPIのAndroidManifestに書いてあるpackagenameとsrc以下にあるディレクトリ構成は若干違う
  • AndroidManifestにはを追記する必要がある。ビルドに失敗する

などがあります。

\3. CompleSDKにSonyのAPIを指定できないので、同じように動作させるためにSmartExtensionAPIモジュールにANDROID_HOME/add-ons/addon-sony_add-on_sdk_2_1-sony-16/libs以下のjarファイルを追加します

\4. SmartExtensionUtilsはSmartExtensionAPIに依存しているのでbuild.gradleの末尾あたりに

dependencies {
    compile project(':SmartExtensionAPI')
}

モジュールのインポートする旨を記述します

\5. 実際に作成するSmartWatch用のプロジェクトを生成します。今回はSampleアプリを1と同じようにしてgradle用に移してきました。SampleSensorExtensionを使ったので、今回はSmartWatch2用アプリのプロジェクトとモジュールの名前はSampleSensorExtensionとします。

\6. SmartExtensionAPIとSmartExtensionUtilsをインポートします。インポートする機能がないので、それぞれのモジュールディレクトリ(SmartExtensionAPI/SmartExtensionAPI)を、現在のプロジェクトにまるっとコピーします。

\7. SampeSensorExtensionモジュールSmartExtensionUtilsに依存しているのでbuild.gradleの末尾に例によって

dependencies{
    compile project(':SmartExtensionUtils')
}

を追記します。

\8. ProjectStructureを開き、Modulesの中からSampleSensorExtensionを選択し、dependenciesタブから+を押してModule dependencyを選び、SmartExtensionUtilsを追加します

\9. 心の赴くままにSmartWatch用アプリを開発してください。

f:id:shin_bashi:20131217003457j:plain

動いた!

僕の場合は何やら警告がでてましたが、今回の目的はとにかくAndroidStudioでビルドできるかどうかの検証なのでここまでとします。

結論:できたけどeclipceで開発したほうが速そう

AndroidStudioを使うことで開発速度は上がるかもしれませんが、環境構築が面倒なのでネックですね。モジュールのインポートくらいボタンポチでできるようにしたいもんですね。GoogleがクリスマスプレゼントにAndroidStudioを正式リリースとかしてくれないかなぁとか思います。

明日はモンスターハンターとかモンスターストライクが好きなモンスター @kuromatu さんです!きっとモンスターっぽいなにかを語ってくれるんじゃないかと踏んでいます。

話題のKLABのPlaygroundのsoをビルドしようとして失敗した

MacでPlaygroundをandroid用にビルドしようとして微妙にコケました。

ビルド手順はこちらを参考にすると非常にわかりやすいです

ブライテクノBlog 2D/2.5Dゲームエンジン Playgroundのセットアップ http://brightechno.com/blog/archives/150

NDKのパスは同じ名前でパス通してあるし、特にイジる必要なさそうだなーと思ってたらコケました。

>> 1 error generated.
>> jni/Android/CSockReadStream.cpp:42:14: error: use of undeclared identifier 'close'

こんなエラーが沢山。NDK読み込めてない? と思って.bash_profileを除いてみたら、ANDROID_NDK_ROOTがexportついてなかった。 直接この変数見てたのか・・・、やっぱりちゃんと確認しないとだめだなーと思ってもう一度ビルド。コケた。

何がおかしいんだろと思ってndkを入れなおしてみたり色々試してみたけど変化なし。 再度.bash_profileを見てみると

ANDROIDNDK_ROOT・・・?

typoかーーーーーー

export $ANDROID_NDK_ROOT=/path~~~~~

で動きました。

orz

GooglePlay で アプリを別のアカウントに移管する

「アプリを移譲することになった」 「やっぱり別のアカウントでアプリを管理したい」

ってのは割とよくある話だと思います。 こちらからどうぞ(英語必須)

https://support.google.com/googleplay/android-developer/contact/dev_registration?extra.IssueType=transfer

うまく表示されない場合は、

https://support.google.com/googleplay/android-developer/

  1. のページ下部にある言語を選択から「English」を選択。
  2. 「CONTACT US」を押して
  3. Registration and Account Issues を選択
  4. I would like to have my apps transferred to a new account を選択
  5. Please fill out this form to resolve your issue. からformの部分がリンクになっているのでそこからフォームに必要事項を記入するとよいらしいです。

実際やったわけでないので、本当に出来るのかどうかは知らないのですが、以前見かけた記事(現在リンク切れ)では、移行先のアドレスから確認のメールを送信するなどの手順を踏んで移行できたとのことです。 ヘルプを読む限りでは、現在のアカウントをキャンセルして、新しいアカウントに全てアプリを移す。というような書き方をされていますが、説明にきちんと書けばアプリ単体を別アカウントに移すこともできるようです。

androidアプリ開発におけるカメラ問題に決着をつけようじゃないか 解決編

  • 条件振り分けで取得方法を変える
  • getContentResolver().insert()を使わない。MediaScannerConnection等を使って標準ギャラリーには反映させる。

そんな訳で、上記の具体的な解決方法を書く。

まず、getContentResolver()を使わないので、別の方法でカメラの保存先を取得する。

public class CameraActivity extends Activity {

    priate static final int REQUET_IMAGE_CAPTURE = 1;
    private Uri imageUri;

    // 省略

    private void runCamera() {
        Bundle extras = getIntent().getExtras();

        // 標準のカメラ撮影時の保存場所を取得
        File pathExternalPublicDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM);
        String filename = System.currentTimeMillis() + ".jpg";
        File capturedFile = new File( pathExternalPublicDir, filename );
        Uri imageUri = Uri.fromFile( capturedFile );
        Intent intent = new Intent( MediaStore.ACTION_IMAGE_CAPTURE );
        intent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri);
        startActivityForResult(intent, REQUEST_IMAGE_CAPTURE);
    }

とりあえず、これでカメラで撮った画像は端末標準の保存先に保存されることになる getContentResolver()も使ってないので、onActivityResultで取得する画像データにはexifがついてくるはず。 すべての機種で試したわけではないのでわからないけど、とりあえず持っている端末で試したところexifはついてきた

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        // experiaとそれ以外で処理を分ける
        Uri pictureUri = (data != null && data.getData() != null) ? data.getData() : imageUri;
        if (pictureUri == null) {
            // IS03とかだとUriが取れないらしく、サムネイルクラスのしょぼい画像を取得するしかない
            Bitmap bmp = (Bitmap) data.getExtras().get("data");
            return;
        }
        // 標準ギャラリーにスキャンさせる
        MediaScannerConnection.scanFile( // API Level 8
                this, // Context
                new String[]{pictureUri.getPath()},
                new String[]{"image/jpeg"},
                null);
        }

        int orientation = ImageUtil.getOrientation(pictureUri);
        // 回転方向を取得して適切に回転させる

        Bitmap bmp = ImageUtil.createBitmapFromUri(this, pictureUri);
    }  
public class ImageUtil {

    public static Bitmap createBitmapFromUri(Context context, Uri uri) {
        ContentResolver contentResolver = context.getContentResolver();
        InputStream inputStream = null;
        BitmapFactory.Options imageOptions;
        Bitmap imageBitmap = null;

        // メモリ上に画像を読み込まず、画像サイズ情報のみを取得する
        try {
            inputStream = contentResolver.openInputStream(uri);
            imageOptions = new BitmapFactory.Options();
            imageOptions.inJustDecodeBounds = true;
            BitmapFactory.decodeStream(inputStream, null, imageOptions);
            assert inputStream != null;
            inputStream.close();
            // もし読み込む画像が大きかったら縮小して読み込む
            inputStream = contentResolver.openInputStream(uri);
            if (imageOptions.outWidth > 2048 && imageOptions.outHeight > 2048) {
                imageOptions = new BitmapFactory.Options();
                imageOptions.inSampleSize = 2;
                imageBitmap = BitmapFactory.decodeStream(inputStream, null, imageOptions);
            } else {
                imageBitmap = BitmapFactory.decodeStream(inputStream, null, null);
            }
            inputStream.close();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return imageBitmap;
    }

    public static int getOrientation(Uri uri) {
        ExifInterface exifInterface;

        try {
            exifInterface = new ExifInterface(uri.getPath());
        } catch (IOException e) {
            return 0;
        }

        int exifR = exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED);
        int orientation = 0;
        switch (exifR) {
            case ExifInterface.ORIENTATION_ROTATE_90:
                orientation = 90;
                break;
            case ExifInterface.ORIENTATION_ROTATE_180:
                orientation = 180;
                break;
            case ExifInterface.ORIENTATION_ROTATE_270:
                orientation = 270;
                break;
            default:
                orientation = 0;
                break;
        }
        return orientation;
    }
}

とりあえず、これでBitmapとorientationを取得することができたので、あとは適当に画像回転させるなりエラー処理を書けばよさそう。

androidアプリ開発におけるカメラ問題に決着をつけようじゃないか 問題編

そろそろ決着をつけようじゃないか。 鬼門鬼門ってもういい加減いいだろ。androidが発表されてから何年経ってると思ってるんだ。 情報で揃ってるだろJKwwwwwwww

そんな風に思ってた時期が僕にもありました。

androidにおけるカメラから画像を取得する際の問題

  • 端末によって取得方法が異なる
    • 予めMediaStore.EXTRA_OUTPUT にUriを渡して置く方法
    • どういうわけか、onActivityResult のIntentのgetData()で取れるUriに入っているものを取得する方法(Xperia2.0系?)
    • それでも取れないからdata.getExtras().get("data")をBitmapにキャストして使う方法(IS03?)
  • 基本的に、横向き扱いである
    • getContentResolver()でinsertすると、なぜかexifが取れない

(´・ω・`)・・・多すぎだろJK。もっと気軽に使えてもいいだろ・・・。

解決策

  • 条件振り分けで取得方法を変える
  • getContentResolver().insert()を使わない。MediaScannerConnection等を使って標準ギャラリーには反映させる。

明日起きたらコードをもうちょっと整理してのっけます。

Android Studio:resource entry is already defined.

ビルド時にこんなエラーが出た。 9patchにしなければいけない画像が9patchになっていなかったので、以下の様な作業をしたら発生。 プロジェクトをリビルドしてもダメだった。リビルドってクリーン&ビルドじゃないのだろうか?

  1. hogehoge.png をリネームして hogehoge.9.png に変更
  2. そのままhogehoge.9.pngをandroid studioで9patch編集
  3. ビルド
  4. エラー

解決策

  1. hogehoge.9.png を hogehoge_tmp.9.png に変更
  2. ビルド→失敗(hogehoge.9.pngが無いと怒られる)
  3. hogehoge_tmp.9.png を hogehoge.9.png に変更
  4. ビルド→成功

追記: Twitterにてアドバイスをもらいました

@kimukou_26「ASに関してはjetGradleが出来るまで外部実行でgradlewを登録しておくしかないと思います~(^-^;)」 https://twitter.com/kimukou_26/status/340439887234871296

ListViewの中にViewPagerを入れる場合の注意

今回ListViewのheaderに、横にスワイプしたりフリックしたりすることで画像が切り替わる様なビューを入れることになったのですが、困ったことになりました。

ここまで読んでピンと来て解決策だけ知りたい方はすっ飛ばして下の方を読んでくだい。

で、どんなことが起きたのかと言いますと。

f:id:shin_bashi:20130529184624p:plain

青い部分がListView、で、そのHeaderの中に入っているViewPagerが緑の部分です。 ViewPagerの部分は横フリックしたら動く、縦にフリックすればListViewが動く。という期待をしていたのですが、少し違いました。

f:id:shin_bashi:20130529184912p:plain

上記のオレンジ色の軌道を指が動いた時にどうなるかというと、赤い丸で囲まれてるように少しでもViewPager上でy軸方向に動いてしまった瞬間に、ViewPagerからイベントは外れて、ListViewにわたってしまいます。1pxもずらさずに横にスワイプした時にした期待した動作をしてくれません。

そんなわけで、ViewPagerを継承して、ViewPagerが動いている間はListViewにイベントが移譲されないように拡張して解決しました。

public class MyViewPager extends ViewPager{
    private ListView listView;

    public MyViewPager(Context context) {
        super(context);
    }
    public MyViewPager(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public void setListView(ListView listView) {
        this.listView = listView;
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent motionEvent) {
        if (motionEvent.getAction() == MotionEvent.ACTION_UP){
            listView.requestDisallowInterceptTouchEvent(false);
        }
        return super.onInterceptTouchEvent(motionEvent);
    }

    @Override
    protected void onPageScrolled(int position, float offset, int offsetPixel) {
        if (offset > 0.01 || 0.99 < offset ) {
            listView.requestDisallowInterceptTouchEvent(true);
        }
        super.onPageScrolled(position, offset, offsetPixel);
    }
}

参考 http://mochi34.blogspot.jp/2012/12/viewpagerscrollview.html

onPageScrolledで、1%以上y軸に動きが無ければ、ListViewに処理を渡さないようにしました。このへんはおこのみで数字を変えれば良いかなぁと思います。 大体これで期待する動作をしてくれるようになりました。 なお詳しくはないんですが、iOSでは標準でこんなふうに動いてくれるようですね。