CoreDataを追加したらARCのエラーが出た

 
久々!約2年ぶり!!

ARCがデフォルトONになってからの現象でしょうか。
新規プロジェクト作成時にUse Core Dataのチェックせずに作って、後になって追加する時にハマりました。

既存のプロジェクトへUse Core Dataオンにしたサンプルからコードをコピペしてみたところ、

ARC Semantic Issue
Receiver type 'NSManagedObjectContext' for instance message is a forward declaration

 
とか言われてコンパイルが通りませんでした…

ぱっと見エラーと関係なさそうなのですが、プリコンパイルヘッダの修正が漏れていました。

#ifdef __OBJC__
  #import <UIKit/UIKit.h>
  #import <Foundation/Foundation.h>
  #import <CoreData/CoreData.h>
#endif

 
pchファイルにCoreDataのヘッダを追加してやればOKでした。
pchファイルはSupporting Filesの中にいます。


参考
https://github.com/ideashower/ShareKit/issues/259

Google Cloud Messaging for Android (GCM)を使ってみた

★2014.3.18追記あり

Android 4.1のAPIが公開されましたね!

お仕事でC2DMを使おうとしていた矢先に、

Android Cloud to Device Messaging (C2DM) is deprecated.

http://developer.android.com/guide/google/gcm/c2dm.html

というわけで、C2DMの進化版、GCMを使ってみました。

基本的な使い方はC2DMと似てるので、移行はそんなに難しくなかったです。

一から作るのも、
GCM: Getting Started
ここの手順をなぞればさくっと出来る感じです。
僕はさくっと出来なかったので備忘録かわりに書いておきます。

Sender IDを取得

Google APIs Console page
Google APIs ConsoleでGCM serviceをONにします。

API projectがまだない場合はCreate project...します。
プロジェクトを作るとアドレスバーに

https://code.google.com/apis/console/#project:4815162342

こんな感じでproject IDが表示されるので#project:の後の数字を控えておきます。
このIDがC2DMのSender IDに該当します。
(上のIDはご本家に書いてあったのコピーしてるので読み替えて下さい)

GCM serviceをONにする為に、

  1. 左側のメニューからServicesを選択
  2. Google Cloud MessagingのトグルをON
  3. 何か言われるのでとりあえずaccept

API Key(=Authorization key)を取得

C2DMの時に使っていたAuthorization keyはここで作ります。

  1. 引き続きAPIs ConsoleでAPI Accessを選択し、Create new Server key。
  2. IP制限とか必要なければそのままCreate。
  3. Key for server apps (with IP locking)のAPI key:の値をメモ。

これでSender IDとAuthorization keyの作成は終わり。
次はアプリ側を作ります。

アプリ側

便利なライブラリをご本家が公開してくれたので、SDK Managerを使って落とします。
Extras > Google Cloud Messaging for Android Library
です。

gcm.jarってのが、YOUR_SDK_ROOT/extras/google/gcm-client/distの中にあるので組み込みます。

お次はちょい面倒なManifestの編集。

<uses-sdk android:minSdkVersion="8" android:targetSdkVersion="xx"/>

GCMはAndroid 2.2以上でしか使えません。

次に

<permission android:name="my_app_package.permission.C2D_MESSAGE" android:protectionLevel="signature" />
<uses-permission android:name="my_app_package.permission.C2D_MESSAGE" /> 
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />

my_app_packageのとこはアプリのパッケージ名を入れる。
この辺はC2DMと同じ。

applicationの中は、こんな感じで。

<application android:icon="@drawable/icon" android:label="@string/app_name">
  <service android:name="my_app_package.GCMIntentService" />

  <receiver
    android:name="com.google.android.gcm.GCMBroadcastReceiver"
    android:permission="com.google.android.c2dm.permission.SEND" >

    <intent-filter>
      <action android:name="com.google.android.c2dm.intent.RECEIVE" />
      <action android:name="com.google.android.c2dm.intent.REGISTRATION" />
      <category android:name="my_app_package" />
    </intent-filter>

  </receiver>
</application>

GCMBroadcastReceiverはライブラリの中に入ってて、my_app_package.GCMIntentServiceは自分で用意します。

GCMIntentService

GCMBaseIntentServiceを継承してGCMIntentServiceクラスを作ります。
GCMBaseIntentServiceもライブラリの中にあります。

メッセージのレシーバは
Android開発 C2DMを触ってみよう
を参考にさせてもらいました。

コンストラクタでさっきメモったproject ID(=Sender ID)を親クラスに渡します。

package my_app_package;

import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.os.Message;
import android.util.Log;

import com.google.android.gcm.GCMBaseIntentService;

public class GCMIntentService extends GCMBaseIntentService {
	
    public GCMIntentService() {
        super("SENDER_ID");
    }

    @Override
    public void onRegistered(Context context, String registrationId) {
        Log.w("registration id:", registrationId);
        sendMessage("id:" + registrationId);
    }
 
    @Override
    protected void onUnregistered(Context context, String registrationId) {
        sendMessage("C2DM Unregistered");
    }
 
    @Override
    public void onError(Context context, String errorId) {
        sendMessage("err:" + errorId);
    }
 
    @Override
    protected void onMessage(Context context, Intent intent) {
        String str = intent.getStringExtra("message");
        Log.w("message:", str);
        sendMessage(str);
    }
   
    private void sendMessage(String str) {
        //本体側に通知するなりなんなり
    }
}

registrationした時はonRegistered、なにかメッセージが来るとonMessageに飛んできます。
あとは、

onRecoverableError(Context context, String errorId)

なんてのもあるみたい。


レシーバが書けたら本体のActivityをいじります。

import com.google.android.gcm.GCMRegistrar;

して、onCreateの中で

GCMRegistrar.checkDevice(this);
GCMRegistrar.checkManifest(this);
final String regId = GCMRegistrar.getRegistrationId(this);
if (regId.equals("")) {
  GCMRegistrar.register(this, "SENDER_ID");
} else {
  Log.v(TAG, "Already registered");
}

ここでもSender IDを使います。
GCMRegistrar.checkDevice()でデバイスが対応しているかどうか、
GCMRegistrar.checkManifest()でマニフェストの設定があってるか見てくれます。
NGの時は例外投げるそうです。

これでアプリを起動すると、registration IDが取れるはずです。
registration IDも後で使うのでメモ。

サーバ側

アプリでregistration出来たら、サーバからpushしてあげればメッセージが受け取れます。

本家では便利なライブラリ使ってサーブレット作ってねって言ってましたが
サーブレットっておいしいの?な僕はまた
Android開発 C2DMを触ってみよう
を参考にさせてもらいました。
最初の方で取ったAPI keyとさっきのregistration IDを使います。

<?php

$url = 'https://android.googleapis.com/gcm/send';

$registration_id = 'AAAAAAAAAAAAAAAAAAA'; //registration IDはここ
$message = 'Hello, GCM!!';

$header = array(
  'Content-Type: application/x-www-form-urlencoded;charset=UTF-8',
  'Authorization: key=XXXXXXXXXX', //API keyはここ
);
$post_list = array(
  'registration_id' => $registration_id,
  'collapse_key' => 'update',
  'data.message' => $message,
);
$post = http_build_query($post_list, '&');

$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_FAILONERROR, 1);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
curl_setopt($ch, CURLOPT_POST, TRUE);
curl_setopt($ch, CURLOPT_HTTPHEADER, $header);
curl_setopt($ch, CURLOPT_POSTFIELDS, $post);
curl_setopt($ch, CURLOPT_TIMEOUT, 5);

//CA証明書の検証をしない
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);

$ret = curl_exec($ch);
 
var_dump($ret);

?>

このスクリプトを叩くとレシーバのonMessageがコールされます。
成功した時はPHP側で"id=XXXXXXX"とメッセージのIDが表示されます。

投げ先のURLや、GCMサーバの応答などなどは
GCM Architectural Overview
に細かく載ってます。

Interpreting a success responseやInterpreting an error response辺りに
レスポンス周りが載ってるので、上手くいかない時は参考になるかも。


★2014.3.18追記
コメントしていただいた、

//CA証明書の検証をしない
curl_setopt($curl,CURLOPT_SSL_VERIFYPEER,false);

をサーバ側のスクリプトに追記しました!

★2016.1.6追記
上記の追記したスクリプトの変数名が元のスクリプトと合っていなかったので修正しました!
コメントありがとうございました。

まとめ

はてな記法ちゃんと使えばもっと読みやすくなるんだろうか。。。

JNI(.so)を使ったJARライブラリのandroidでのロードの仕方

なにこのタイトル!
初めてのandroidネタです。

お急ぎの方は最後の方から見て下さい。

Javaのライブラリはjar形式で配布可能なんですね。
公式ではやるなって言ってるような気がしますが、クライアント様様様の要求でjarで出さなきゃいけない時もありますね。

普通ならサクッとエクスポートしてオッケーなのですが、ライブラリの中でJNIなんか使ってると大変です。
/lib/armeabi配下に.soを入れてjarをエクスポートするところまでは問題なし。
しかしそのjarを組み込んでアプリをビルドしようとすると怒られます。

The library 'hoge.jar' contains native libraries that will not run on the device.
とか、
lib/ is reserved for NDK libraries.
とか言われます。

色々見てみたんですがlibに置いて動かすのはうまくいきませんでした。。
ぐーぐるの中の人が

I'm afraid there's no work around, you'll need to manually add the
library to the project that includes the jar file.

Google グループ

って言ってるのでダメなのかなーって感じ。
結構前の記事なので今は違うのかもですが、どうにもうまくいかない。。

そうは言ってもクライアントが(以下略

というわけで、stackoverflow。
いつもお世話になってる猛者達も結構苦労したみたいです。
lib下に置いて動かす解決方法は見つからなかったですが、代わりにこんなのがありました。
Mixing Android External JAR and JNI gone wild - Stack Overflow

回答の中に、zipにしてassetsに突っ込んで、ロードする時に解凍すればおkって言ってる人がいたので試したところ上手くいきました。
zipにしてるのはassetsにサイズ制限があるからだと思いますが、zipじゃなくてsoのままでも上手くいきました。

まぁこの方法も、公式的にはアレなんですけど、クライ(以下略

jniのmakefileの最後に

cp -p ../libs/armeabi/libhoge.so ../assets/.

とか書いておくと楽チンですね。

アプリからの呼び出しはこんな感じ。
例外処理は省いてるので自前でごにょごにょして下さい。

String path = "/data/data/" + HogeActivity.getContext().getPackageName() + "/libhoge.so";
InputStream is = HogeActivity.getContext().getResources().getAssets().open("libhoge.so");
File fileout = new File(path);
OutputStream os = new FileOutputStream(fileout);
final int DEFAULT_BUFFER_SIZE = 1024 * 4;
byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
int n = 0;
while (-1 != (n = is.read(buffer))) {
	os.write(buffer, 0, n);
}
is.close();
os.close();
System.load(fileout.toString());

ちなみにnexus s(ICS)で動作確認しました。
他の機種でうまくいくかは分かりません。
うまくいったよーとか、ダメだーとか、こうすればもっといい感じってやり方をご存知の方はぜひぜひ教えて下さい。

 

新しいiPadの機種名

銀ダコはおいしいですね。

iOSの機種の判別方法は用途に合わせて色々あります。

単純にiPadiPhoneかはたまたiPodか取得したい場合は、

NSString *model = [[UIDevice currentDevice] model];

で取得出来ます。
出力はそのまま"iPad"、"iPhone"、"iPod"。

もっと細かい機種を取得したいときは、

struct utsname uts;
uname(&uts);
NSString *machine = [NSString stringWithCString:uts.machine];

とか、

size_t size;
sysctlbyname("hw.machine", NULL, &size, NULL, 0);
char *buf = (char *)malloc(size);
sysctlbyname("hw.machine", buf, &size, NULL, 0);
free(buf);

で取得出来ます。

どっちの方がいいとかは分かりません。
教えて偉い人!

この前出た新しいiPadは"iPad3,1"でした。

 

再生終了時に何かする

前回の続編的なものです。

再生&フルスクリーンをプログラムからやったので、終了時も何かやってみよう!です。

javascriptで何かさせる場合は前回の

  [webView stringByEvaluatingJavaScriptFromString:@"var v = document.getElementById('video');v.webkitEnterFullscreen();"];

に少し手を加えて

  [webView stringByEvaluatingJavaScriptFromString:@"var v = document.getElementById('video');v.webkitEnterFullscreen();
    v.addEventListener('ended',function(){var v = document.getElementById('video');v.webkitExitFullScreen();});"];

こんな感じに実装します。
この例の場合、動画の最後まで再生したあとにフルスクリーンを解除するようにしています。

objective-cで何かしたいよ!

という場合、javascriptから直接コールバックする方法はないみたいですが、裏技的にdocument.locationを利用することでそれっぽい動きをさせることが出来ます。

さっきの例を少しいじって

  [webView stringByEvaluatingJavaScriptFromString:@"var v = document.getElementById('video');v.webkitEnterFullscreen();
    v.addEventListener('ended',function(){document.location='hoge:fuga:param1:param2';});"];

とした上で、objective-c側で

- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
  
  NSString *requestString = [[request URL] absoluteString];
  NSArray *components = [requestString componentsSeparatedByString:@":"];
  if (1 < [components count] && [[components objectAtIndex:0] isEqualToString:@"hoge"]) {
    if ([[components objectAtIndex:1] isEqualToString:@"fuga"]) {

      NSLog(@"%@", [components objectAtIndex:2]); // param1
      NSLog(@"%@", [components objectAtIndex:3]); // param2 変数を渡すことも出来ます
      
      return NO;
    }
  }
  return YES;
}

でOKです。

  [webView setDelegate:self];

をお忘れなく!!


こっちはここを参考にしました。というかほとんど丸写しですね。
http://www.codingventures.com/2008/12/using-uiwebview-to-render-svg-files/

iPhone/iPadのwebviewで動画を自動再生&フルスクリーン化

ブラウザのhtml5対応は日進月歩なので今のところ(iOS5.0)の挙動ということで。

Autoplay

html5で使えるようになった

  NSString *httpString = @"<video id='video' width='500' height='300' src='http://hoge.com/hoge.mp4' controls></video>";
  [webView loadHTMLString:httpString baseURL:nil];

controls属性を追加しておけば再生ボタンなんかもブラウザが勝手に用意してくれます。
便利。

ただ、どうしても(クライアントの無茶な要望などで)自動再生させたい時があります。
たとえそれが悪いことだと分かっていても。
Autoplay is bad for all users | Punkchip

そうは言ってもクライアントが(ry
というわけでwebViewのプロパティをいじります。
htmlをロードする前に

  [webView setMediaPlaybackRequiresUserAction:NO];

としてやればOKです。

フルスクリーン

iPhoneだと再生時に問答無用でフルスクリーンになるのですが、iPadの場合はブラウザ内で再生しようとします。
controls付けてれば最大化ボタンが表示されるけど、付けたくない場合やプログラムで制御したい時もあります。

その場合、動画が再生状態であればjavascriptを使って

  [webView stringByEvaluatingJavaScriptFromString:@"var v = document.getElementById('video');v.webkitEnterFullscreen();"];

フルスクリーンで再生が出来ます。
前述のautoplayと組み合わせればフルスクリーン再生させるようなプログラムが出来ます。

再生状態で無くてもフルスクリーンにしたいのですが。。
Controlling Media with JavaScript
ここ読むとmetadata読むまではダメっぽいこと書いてあったので、preload属性付けたりしてみたんですがダメでした…


今のところはこんな感じですが、ブラウザの対応状況が変わると使えなくなってしまうトピックかもですね。

※続編的なものも書きました! 2012/1/16
再生終了時に何かする - azukinohirokiの日記