ラベル ADK の投稿を表示しています。 すべての投稿を表示
ラベル ADK の投稿を表示しています。 すべての投稿を表示

2025年12月4日木曜日

adk-java を Android アプリに組み込むための長い道のり

この記事は JP_Google Developer Experts Advent Calendar 2025 の4日目の記事です。

adk-java ライブラリをアプリに追加する


adk-java を Android アプリに入れるのなんて簡単でしょ。 https://google.github.io/adk-docs/#java に書いてある通りに build.gradle.kts に dependencies { implementation("com.google.adk:google-adk:0.4.0") } を書けばいいんでしょ。 そう思っていました....


Android Studio の [New Project...] からまっさらなプロジェクトを作って(もちろんこの時点ではビルドできるしアプリも実行できる)、上の依存を一行追加して Sync してビルドすると....

はい、エラー...

Duplicate class io.modelcontextprotocol.json.McpJsonInternal found in modules mcp-core-0.14.0.jar -> mcp-core-0.14.0 (io.modelcontextprotocol.sdk:mcp-core:0.14.0) and mcp-json-0.14.0.jar -> mcp-json-0.14.0 (io.modelcontextprotocol.sdk:mcp-json:0.14.0) ...


同じクラスファイル(io.modelcontextprotocol.json.McpJsonInternal)が異なる2つのライブラリ/モジュール(mcp-coreとmcp-json)から重複して含まれてますね...
ということで片方を exclude します。全部で3のライブラリで重複があるようです。 dependencies { implementation("com.google.adk:google-adk:0.4.0") { exclude(group = "io.modelcontextprotocol.sdk", module = "mcp-json") exclude(group = "javax.annotation", module = "javax.annotation-api") exclude(group = "org.slf4j", module = "jcl-over-slf4j") } ... }

これでうまくいくかと思いきや、またしてもエラー

Invalid build configuration. Attempt to create a global synthetic for 'Record desugaring' without a global-synthetics consumer.


adk-java の中で Java 16 以降で導入された Record クラスを使用しているが、それを古い Android バージョンでも動作させるための処理(Desugaring: デシュガーリング)が設定されてないと言われていますね...

一番簡単な解決策はプロジェクトの Javaの互換性バージョンを Java 17 に統一することなのでそうします。 kotlin { jvmToolchain(17) } android { ... compileOptions { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 } }

今度こそビルド通るかと思いきや、またしてもエラー

MethodHandle.invoke and MethodHandle.invokeExact are only supported starting with Android O (--min-api 26): Lautovalue/shaded/com/google/common/hash/ChecksumHashFunction$ChecksumMethodHandles;->updateByteBuffer(Ljava/util/zip/Checksum;Ljava/nio/ByteBuffer;)Z


MethodHandle.invoke と MethodHandle.invokeExact は Android O 以降じゃないとサポートしてないと言われています...

ということで minSdk バージョンを 26 にします。 android { ... defaultConfig { ... minSdk = 26 ... } ... }

4度目...さすがにビルド通ってほしいが、まだエラー

Execution failed for task ':app:mergeDebugJavaResource'.
> A failure occurred while executing com.android.build.gradle.internal.tasks.MergeJavaResWorkAction
> 2 files found with path 'mozilla/public-suffix-list.txt' from inputs:
- org.apache.httpcomponents:httpclient:4.5.14/httpclient-4.5.14.jar
- org.apache.httpcomponents.client5:httpclient5:5.3.1/httpclient5-5.3.1.jar



異なる2つのライブラリに、全く同じパスとファイル名を持つリソースファイルが存在するという典型的なエラーです。
競合しているファイル(このエラーだと mozilla/public-suffix-list.txt)をビルドに含めないように設定します。
一つ対応しても同じエラーが別のファイルに対して何回も出てくるので、全部 excludes に追加します。 android { ... packaging { resources { excludes += "mozilla/public-suffix-list.txt" excludes += "META-INF/DEPENDENCIES" excludes += "META-INF/LICENSE.md" excludes += "META-INF/NOTICE.md" excludes += "META-INF/io.netty.versions.properties" excludes += "META-INF/INDEX.LIST" } } }

これでようやくビルドが通りました!エラーが多い...

都市の時間を返すエージェントを作ってみる


ようやくビルドが通ったので Quickstart (https://google.github.io/adk-docs/get-started/java/) の HelloTimeAgent を作ってみました。

が、これもハードルが高かった〜

API Key どう渡す?


まず困ったのが API key の渡し方。公式ドキュメントには

echo 'export GOOGLE_API_KEY="YOUR_API_KEY"' > .env

しろとしか書いてない。しかし Android は環境変数は使えません。

そこで、API key がセットされなくてクラッシュするところから呼び出し元をたどっていくと、LlmRegistry にデフォルト登録される LlmFactory を利用していることがわかりました。 public final class LlmRegistry { ... /** Map of model name patterns regex to factories. */ private static final Map<String, LlmFactory> llmFactories = new ConcurrentHashMap<>(); /** Registers default LLM factories, e.g. for Gemini models. */ static { registerLlm("gemini-.*", modelName -> Gemini.builder().modelName(modelName).build()); registerLlm("apigee/.*", modelName -> ApigeeLlm.builder().modelName(modelName).build()); } この LlmRegistry に登録されている "gemini-.*" に対応する LlmFactory を、API key をセットした Gemini に置き換えればいけそうだとわかりました。 object HelloTimeAgent { init { LlmRegistry.registerLlm("gemini-.*") { modelName -> Gemini.builder() .modelName(modelName) .apiKey("MY API KEY HERE") .build() } } ... } これで API key がセットされてなくてクラッシュすることはなくなりました。

tool 名の statec method がないと言われる


Tool はリフレクションで呼ばれていて、Java の static メソッドになっている必要がありました。 public class FunctionTool extends BaseTool { ... public static FunctionTool create(Class<?> cls, String methodName, boolean requireConfirmation) { for (Method method : cls.getMethods()) { if (method.getName().equals(methodName) && Modifier.isStatic(method.getModifiers())) { return create(null, method, requireConfirmation); } } throw new IllegalArgumentException( String.format("Static method %s not found in class %s.", methodName, cls.getName())); } Java の static メソッドになっていないとこんなエラーでクラッシュします。

Caused by: java.lang.IllegalArgumentException: Static method getCurrentTime not found in class com.example.myapplication.HelloTimeAgent.

Kotlin の object のメソッドを Java の static メソッドにするには @JvmStatic をつけます。 object HelloTimeAgent { ... @Annotations.Schema(description = "Get the current time for a given city") @JvmStatic fun getCurrentTime( @Annotations.Schema( name = "timeZone", description = "timeZone of the city to get the time for" ) timeZone: String ): ... ... }

tool の戻り値は Map じゃないとだめ?


以下のように時刻の文字列だけ返せばいいかなと思ったのですが、これではうまく行きませんでした。 object HelloTimeAgent { ... @Annotations.Schema(description = "Get the current time for a given city") @JvmStatic fun getCurrentTime( ... ): String { val zoneId = ZoneId.of(timeZone) val zonedDateTime = ZonedDateTime.now(zoneId) val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss") return zonedDateTime.format(formatter) } } このように Map で返すとうまくいきました。ちなみに timeZone は Map に含めなくても問題ありませんでした。 object HelloTimeAgent { ... @Annotations.Schema(description = "Get the current time for a given city") @JvmStatic fun getCurrentTime( ... ): Map<String, String> { val zoneId = ZoneId.of(timeZone) val zonedDateTime = ZonedDateTime.now(zoneId) val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss") return mapOf( "timeZone" to timeZone, // なくてもよい "forecast" to zonedDateTime.format(formatter) ) } }

うごいたーーー



全体


全体のコードはこんな感じです。 class MainActivity : ComponentActivity() { private val viewModel by viewModels<MainViewModel>() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() setContent { MyApplicationTheme { ChatScreen( messages = viewModel.messages, loading = viewModel.loading, onClickSubmit = { viewModel.submit(it) } ) } } } } @Composable fun ChatScreen( messages: List<MainViewModel.Message>, loading: Boolean, onClickSubmit: (String) -> Unit, ) { Scaffold { innerPadding -> Column( modifier = Modifier .fillMaxSize() .padding(innerPadding) ) { LazyColumn(modifier = Modifier.weight(1f)) { items(messages) { when (it.from) { MainViewModel.From.User -> { Row { Spacer(Modifier.weight(1f)) Box( Modifier .weight(4f) .padding(8.dp) .background( MaterialTheme.colorScheme.surfaceVariant, RoundedCornerShape(8.dp) ) .padding(8.dp) ) { Text(text = it.text) } } } MainViewModel.From.Agent -> { Row { Box( Modifier .weight(4f) .padding(8.dp) .background( MaterialTheme.colorScheme.surfaceVariant, RoundedCornerShape(8.dp) ) .padding(8.dp) ) { Text(text = it.text) } Spacer(Modifier.weight(1f)) } } } } if (loading) { item("loading") { CircularProgressIndicator( modifier = Modifier .fillMaxWidth() .wrapContentWidth() ) } } } Divider() Row( horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.padding(8.dp) ) { val state = rememberTextFieldState() TextField( state = state, lineLimits = TextFieldLineLimits.SingleLine, enabled = !loading, modifier = Modifier.weight(1f), ) Button( onClick = { onClickSubmit(state.text.toString()) state.clearText() }, enabled = !loading, ) { Text(text = "送信") } } } } } @Preview(showBackground = true) @Composable private fun Preview() { MyApplicationTheme { ChatScreen( messages = listOf( MainViewModel.Message(from = MainViewModel.From.User, text = "東京の時間は?"), MainViewModel.Message(from = MainViewModel.From.Agent, text = "9時です"), ), loading = false, onClickSubmit = {} ) } } class MainViewModel() : ViewModel() { private val runConfig = RunConfig.builder().build() private val runner = InMemoryRunner(HelloTimeAgent.initAgent()) private val session: Session = runner .sessionService() .createSession(runner.appName(), "user1234") .blockingGet() var messages = mutableStateListOf<Message>() var loading by mutableStateOf(false) fun submit(userInput: String) { if (loading) { return } messages.add(Message(from = From.User, text = userInput)) loading = true viewModelScope.launch { val event = withContext(Dispatchers.IO) { val userMsg = Content.fromParts(Part.fromText(userInput)) val events = runner.runAsync(session.userId(), session.id(), userMsg, runConfig) events.filter { it.finalResponse() }.blockingFirst() } messages.add(Message(from = From.Agent, text = event.stringifyContent())) loading = false } } data class Message(val from: From, val text: String) enum class From { User, Agent } } object HelloTimeAgent { init { LlmRegistry.registerLlm("gemini-.*") { modelName -> Gemini.builder() .modelName(modelName) .apiKey("API KEY HERE") .build() } } fun initAgent(): BaseAgent? { return LlmAgent.builder() .name("hello-time-agent") .description("Tells the current time in a specified city") .instruction( """ You are a helpful assistant that tells the current time in a city. Use the 'getCurrentTime' tool for this purpose. """ ) .model("gemini-2.5-flash") .tools(FunctionTool.create(HelloTimeAgent::class.java, "getCurrentTime")) .build(); } @Annotations.Schema(description = "Get the current time for a given city") @JvmStatic fun getCurrentTime( @Annotations.Schema( name = "timeZone", description = "timeZone of the city to get the time for" ) timeZone: String ): Map<String, String> { val zoneId = ZoneId.of(timeZone) val zonedDateTime = ZonedDateTime.now(zoneId) val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss") return mapOf( "timeZone" to timeZone, "forecast" to zonedDateTime.format(formatter) ) } }

2012年5月10日木曜日

「やきはらえ」って言ったら LED が光ってブラウザでアニメーションが走るようにしてみた。

追記: 2012/5/11

「やきはらえ」の気合いが足りないとのご指摘を受けましたので、撮り直しました。
もう少し気合い入れたいんだけど、うまくしゃべらないと「出来映え」とか認識されちゃうのよねー。。。
--------


前に金曜ロードショーでラピュタが放送されるときに
↓なことやって
「Y.A.M の 雑記帳: Android ADKを使ってバルスって言ったら青色LEDが光るようにしてみた。」

まーた、思いついたのでやりました。
(明日の金曜ロードショーはナウシカです)

ナウシカといえば、やっぱ「焼き払え!」「薙ぎ払え!」ですよね。

ということで、Android で音声認識して、「やきはらえ」と言ったら LED 点灯 + ブラウザでアニメーションさせるのを作ってみました。



今回の構成はこんな感じ



ブラジルの dart ハッカソンで Dart + Arduino でクイズをするという作品があって https://plus.google.com/u/0/115028528042654643072/posts/3iXwhtSr9C2 おおお、と思って最近 dart 始めたこともあって、まぁ、やってみたってところです。

ADK を使って LED を点灯させるところは前の(「Y.A.M の 雑記帳: Android ADKを使ってバルスって言ったら青色LEDが光るようにしてみた。」 )とほぼ一緒です(ADK のファームウェアは変えてません。

Android アプリ側は、getLastNonConfigurationInstance() が現在 deprecated になっていることもあって、Fragment を使うように変えました。ビューを持たない ADK と通信するようの Fragment を用意したので、前よりもさらに使い回ししやすくなってると思います。

Android で音声認識すると、LED 側と dart 側の2系列にメッセージが飛ぶような感じにしてます。



ブラウザ側のアニメーションは HTML5 の Canvas 機能を dart から使ってます。
うん、結構大変だったw

いちを github にあげてありますが、ちょっと Android 側にバグあるようでたまに固まります。 ちょっと直す時間なかった。。。

yanzm/Yakiharae - Github -



2011年12月9日金曜日

Android ADKを使ってバルスって言ったら青色LEDが光るようにしてみた。

今日の21時から「天空の城ラピュタ」が放送されるので、TLがバルスで盛り上がって楽しかったので作ってみましたw

RecognizerIntent を使って認識した文字がバルスだったら ADK で LED をつけてるだけです。
RecognizerIntent については adakoda 先生のサイト 音声認識(RecognizerIntent)を使用するには - 逆引きAndroid入門 -を参考にしました。
いつもありがとうございます。

Android アプリから LED の ON/OFF を切り替える方法は前回のエントリ(Android Hello ADK をつくった!)をみてください。



やってみたい人は GitHub においたのでどうぞ。
yanzm/SayBalus - GitHub -



2011年12月8日木曜日

Android Hello ADK つくった!

DemoKit を動かしてみたよエントリや、Arduino で ADK 自作したよエントリはあるのに、Android ADK 用の Android アプリのプログラミングについて初心者向けに書かれたエントリがぜーんぜんなかったので書くことにしました。

ちなみにわたくし、電子工作ブランク10年くらいです。 10年前にブレッドボード上にケーブルが空中回廊みたいになった電卓を作って以来です。

今回は USB Accessory モードの方についてです。
公式ドキュメントはこちら
http://developer.android.com/guide/topics/usb/accessory.html
私のブログでの和訳はこちら
http://y-anz-m.blogspot.com/2011/05/usb-accessory.html

さて、とりあえず開発環境の準備。
DemoKit 動かしてみたよエントリがたくさんあるのでその辺りみればだいたい書いてると思う。
ちなみに公式ドキュメントはこちら。
Android Open Accessory Development Kit | Android Developers

これの Getting Started with the ADK を見ながら(私のブログでの和訳はこちら http://y-anz-m.blogspot.com/2011/05/android-open-accessory-development-kit.html

  • Arduino Software : ADK は Arduino がベースになっているので、ADK で動かすプログラムは Arduino で書く。そのための IDE。コーディングは持ちろん、デバッグやファームインストールもこれで行う。
  • CapSence library : 人間の静電容量を感知するためのライブラリ。DemoKit の Android シールドにあるタッチセンサーの為に必要。これいれないとタッチセンサーを触っても DemoKit アプリ側で反応しない。
  • The ADK package : ADK board 用のファームウェアおよび ADK board とシールド用の hardware design files。

をまずはとってきて、ドキュメントの手順通りにセットアップする。

で、DemoKit を動かす。


さて、ここまではできるよ。手順通りにやればいいだけだからね。
でもさ、DemoKit 取り外して、アクセサリに LED 1個つなげて光らせるだけでも自分だけでやれって言われると、
はて、どうしたら、、、?ってなるのですよ。

とりあえず DemoKit のコード見るよねー。
アクセサリ側はまだいいんだけど、Android アプリ側がすごく複雑。。。
とりあえず LED を on/off するだけの最小構成が知りたいのに操作できる項目が多い(DemoKit では入力としてリレー、3色LEDx3。出力としてジョイスティック、ボタン、タッチセンサー、温度、湿度がある)のでレイアウトも複雑だし、クラスも多い。。。。

やはりここは HelloWorld ならぬ HelloADK が必要なのでは。

ということでつくりましたよっと。



1. Android app

まずは Android アプリ側から。

注意点: Build Target は 2.3.3 (API Level 10) の Google APIs にする。

USB Accessory モードはもともと USB Host と同じく Android 3.0 からサポートされたものなのですが、Android 2.3.4 にバックポートされています。このバックポートを使うには 2.3.3 の Google APIs を指定する必要があります!



LED を on / off するだけなんでレイアウトはトグルボタン1個でいいよね。
ADK に接続してるかどうかのステータスも出すようにしよう。

<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent" android:gravity="center" android:orientation="vertical" > <TextView android:id="@+id/status" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_margin="10dip" /> <ToggleButton android:id="@+id/toggleBtn" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_margin="10dip" /> <TextView android:id="@+id/ledState" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_margin="10dip" /> </LinearLayout>

では肝心の Activity 側。まずは UsbManager のインスタンスを取得します。
Add-on library (つまり 2.3.4 にバックポートされたライブラリ)を使っている場合は
UsbManager manager = UsbManager.getInstance(this);
3.0 以降のプラットフォーム標準の API を使う場合は
UsbManager manager = (UsbManager) getSystemService(Context.USB_SERVICE);

流れ的には

onCreate()
 ・UsbManager の取得
 ・オレオレIntent action でパーミッション依頼用の
  PendingIntent 作成
 ・オレオレIntent action とアクセサリが外されたときの
  BroadcastReceiver 登録
 ・画面のセットアップ

onResume()
 ・USBAccessory の一覧を取得して、権限チェック。
  あればアクセサリとの接続を開く
  (UsbAccessory からファイルデスクリプタを取得し
   ファイルデスクリプタから入出力用の Stream を取得。
   入力を受け取るためのスレッドを開始)
  、なければパーミッションを依頼

onPause()
 ・アクセサリとの接続閉じる

onDestroy()
 ・onCreate() で登録した BroadcastReceiver 解除

・View が操作されたら出力用ストリームにバイト列書き込む。
・入力受け取り用スレッドでバイト列受け取ったらUIスレッドに渡してViewに反映

こんな感じかな。 あとは、コメントみてくれ。

public class MainActivity extends Activity implements Runnable { private static final String TAG = "HelloLED"; private static final String ACTION_USB_PERMISSION = "com.uphyca.android.app.helloled.action.USB_PERMISSION"; private PendingIntent mPermissionIntent; private boolean mPermissionRequestPending; private UsbManager mUsbManager; private UsbAccessory mAccessory; ParcelFileDescriptor mFileDescriptor; FileInputStream mInputStream; FileOutputStream mOutputStream; private ToggleButton mToggleButton; private TextView mLedStatusView; private TextView mStatusView; private final BroadcastReceiver mUsbReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); if (ACTION_USB_PERMISSION.equals(action)) { synchronized (this) { // Intent からアクセサリを取得 UsbAccessory accessory = UsbManager.getAccessory(intent); // パーミッションがあるかチェック if (intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)) { // 接続を開く openAccessory(accessory); } else { Log.d(TAG, "permission denied for accessory " + accessory); } mPermissionRequestPending = false; } } else if (UsbManager.ACTION_USB_ACCESSORY_DETACHED.equals(action)) { // Intent からアクセサリを取得 UsbAccessory accessory = UsbManager.getAccessory(intent); if (accessory != null && accessory.equals(mAccessory)) { // 接続を閉じる closeAccessory(); } } } }; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // UsbManager のインスタンスを取得 mUsbManager = UsbManager.getInstance(this); // オレオレパーミッション用 Broadcast Intent mPermissionIntent = PendingIntent.getBroadcast(this, 0, new Intent(ACTION_USB_PERMISSION), 0); // オレオレパーミッション Intent とアクセサリが取り外されたときの Intent を登録 IntentFilter filter = new IntentFilter(); filter.addAction(ACTION_USB_PERMISSION); filter.addAction(UsbManager.ACTION_USB_ACCESSORY_DETACHED); registerReceiver(mUsbReceiver, filter); setContentView(R.layout.main); mToggleButton = (ToggleButton) findViewById(R.id.toggleBtn); mLedStatusView = (TextView) findViewById(R.id.ledState); mStatusView = (TextView) findViewById(R.id.status); mToggleButton.setOnCheckedChangeListener(new OnCheckedChangeListener() { @Override public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { byte command = 0x1; byte value = (byte) (isChecked ? 0x1 : 0x0); sendCommand(command, value); } }); enableControls(false); } @Override public void onResume() { super.onResume(); if (mInputStream != null && mOutputStream != null) { return; } // USB Accessory の一覧を取得 UsbAccessory[] accessories = mUsbManager.getAccessoryList(); UsbAccessory accessory = (accessories == null ? null : accessories[0]); if (accessory != null) { // Accessory にアクセスする権限があるかチェック if (mUsbManager.hasPermission(accessory)) { // 接続を開く openAccessory(accessory); } else { synchronized (mUsbReceiver) { if (!mPermissionRequestPending) { // パーミッションを依頼 mUsbManager.requestPermission(accessory, mPermissionIntent); mPermissionRequestPending = true; } } } } else { Log.d(TAG, "mAccessory is null"); } } @Override public void onPause() { super.onPause(); closeAccessory(); } @Override public void onDestroy() { unregisterReceiver(mUsbReceiver); super.onDestroy(); } private void openAccessory(UsbAccessory accessory) { // アクセサリにアクセスするためのファイルディスクリプタを取得 mFileDescriptor = mUsbManager.openAccessory(accessory); if (mFileDescriptor != null) { mAccessory = accessory; FileDescriptor fd = mFileDescriptor.getFileDescriptor(); // 入出力用のストリームを確保 mInputStream = new FileInputStream(fd); mOutputStream = new FileOutputStream(fd); // この中でアクセサリとやりとりする Thread thread = new Thread(null, this, "DemoKit"); thread.start(); Log.d(TAG, "accessory opened"); enableControls(true); } else { Log.d(TAG, "accessory open fail"); } } private void closeAccessory() { enableControls(false); try { if (mFileDescriptor != null) { mFileDescriptor.close(); } } catch (IOException e) { } finally { mFileDescriptor = null; mAccessory = null; } } private void enableControls(boolean enable) { if (enable) { mStatusView.setText("connected"); } else { mStatusView.setText("not connected"); } mToggleButton.setEnabled(enable); } private static final int MESSAGE_LED = 1; private class LedMsg { private byte on; public LedMsg(byte on) { this.on = on; } public boolean isOn() { if(on == 0x1) return true; else return false; } } // ここでアクセサリと通信する @Override public void run() { int ret = 0; byte[] buffer = new byte[16384]; int i; // アクセサリ -> アプリ while (ret >= 0) { try { ret = mInputStream.read(buffer); } catch (IOException e) { break; } i = 0; while (i < ret) { int len = ret - i; switch (buffer[i]) { case 0x1: // 2byte のオレオレプロトコル // 0x1 0x0 や 0x1 0x1 など if (len >= 2) { Message m = Message.obtain(mHandler, MESSAGE_LED); m.obj = new LedMsg(buffer[i + 1]); mHandler.sendMessage(m); } i += 2; break; default: Log.d(TAG, "unknown msg: " + buffer[i]); i = len; break; } } } } // UI スレッドで画面上の表示を変更 private Handler mHandler = new Handler() { @Override public void handleMessage(Message msg) { switch (msg.what) { case MESSAGE_LED: LedMsg o = (LedMsg) msg.obj; handleLedMessage(o); break; } } }; private void handleLedMessage(LedMsg l) { if(l.isOn()) { mLedStatusView.setText("ON"); } else { mLedStatusView.setText("OFF"); } } // アプリ -> アクセサリ public void sendCommand(byte command, byte value) { byte[] buffer = new byte[2]; if(value != 0x1 && value != 0x0) value = 0x0; // 2byte のオレオレプロトコル // 0x1 0x0 や 0x1 0x1 buffer[0] = command; buffer[1] = value; if (mOutputStream != null) { try { mOutputStream.write(buffer); } catch (IOException e) { Log.e(TAG, "write failed", e); } } } }

つぎにアクセサリをフィルターするための XML ファイルを作成
manufacturer と model と version はアクセサリ側のコードの aac() の引数をあわせる。

res/xml/accessory_filter.xml


最後の AndroidManifest.xml にいろいろ宣言
・<uses-library android:name="com.android.future.usb.accessory" />
・android.hardware.usb.action.USB_ACCESSORY_ATTACHED の intentfilter
・android.hardware.usb.action.USB_ACCESSORY_ATTACHED の meta-data

AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.uphyca.android.helloadk" android:versionCode="1" android:versionName="1.0" > <uses-sdk android:minSdkVersion="10" /> <application android:icon="@drawable/ic_launcher" android:label="@string/app_name" > <uses-library android:name="com.android.future.usb.accessory" /> <activity android:label="@string/app_name" android:launchMode="singleInstance" android:name=".MainActivity" android:taskAffinity="" > <intent-filter > <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> <intent-filter > <action android:name="android.hardware.usb.action.USB_ACCESSORY_ATTACHED" /> </intent-filter> <meta-data android:name="android.hardware.usb.action.USB_ACCESSORY_ATTACHED" android:resource="@xml/accessory_filter" /> </activity> </application> </manifest>

これでアプリ側は完了。



2. アクセサリ側

アプリ側で定義したオレオレプロトコルに沿って LED 用のデジタル出力ピンのレベルの HIGH/LOW を切り替えるだけ。

#include <Wire.h> #include <Max3421e.h> #include <Usb.h> #include <AndroidAccessory.h> #define LED 24 AndroidAccessory acc("uPhyca", "HelloADK", "DemoKit Arduino Board", "1.0", "http://www.android.com", "0000000012345678"); void setup(); void loop(); void init_leds() { // 24 の DIGITAL を LED 用の出力にする pinMode(LED, OUTPUT); } void setup() { Serial.begin(115200); Serial.print("\r\nStart"); init_leds(); acc.powerOn(); } void loop() { byte msg[2]; byte led; if (acc.isConnected()) { int len = acc.read(msg, sizeof(msg), 1); if (len > 0) { if (msg[0] == 0x1) { if(msg[1] == 0x1) { digitalWrite(LED, HIGH); msg[0] = 0x1; msg[1] = 0x1; acc.write(msg, 2); } else { digitalWrite(LED, LOW); msg[0] = 0x1; msg[1] = 0x2; acc.write(msg, 2); } } } } else { digitalWrite(LED, LOW); } delay(10); }

DIGITAL の 24 を LED のアノードにつないで、カソードを抵抗につないで、抵抗を GND につないでと。



最初に接続を許可するか聞かれるので OK する。



トグルボタンで LED の ON /OFF できたー。





GitHub にもおいといたよ!
yanzm/HelloADK - GitHub - http://goo.gl/bhG1t