diff --git a/apps/Tag/AndroidManifest.xml b/apps/Tag/AndroidManifest.xml index 4414d7360..ba01fff2b 100644 --- a/apps/Tag/AndroidManifest.xml +++ b/apps/Tag/AndroidManifest.xml @@ -20,26 +20,33 @@ own application, the package name must be changed from "com.example.*" to come from a domain that you own or have control over. --> + package="com.android.apps.tag" +> + + + + - + - - - + + + - + + - + - - diff --git a/apps/Tag/res/drawable/ic_launcher_nfc.png b/apps/Tag/res/drawable-hdpi/ic_launcher_nfc.png similarity index 100% rename from apps/Tag/res/drawable/ic_launcher_nfc.png rename to apps/Tag/res/drawable-hdpi/ic_launcher_nfc.png diff --git a/apps/Tag/res/drawable/ic_menu_desk_clock.png b/apps/Tag/res/drawable-hdpi/ic_menu_desk_clock.png similarity index 100% rename from apps/Tag/res/drawable/ic_menu_desk_clock.png rename to apps/Tag/res/drawable-hdpi/ic_menu_desk_clock.png diff --git a/apps/Tag/res/drawable/ic_menu_tag.png b/apps/Tag/res/drawable-hdpi/ic_menu_tag.png similarity index 100% rename from apps/Tag/res/drawable/ic_menu_tag.png rename to apps/Tag/res/drawable-hdpi/ic_menu_tag.png diff --git a/apps/Tag/res/drawable-mdpi/ic_launcher_nfc.png b/apps/Tag/res/drawable-mdpi/ic_launcher_nfc.png new file mode 100644 index 000000000..525f5b500 Binary files /dev/null and b/apps/Tag/res/drawable-mdpi/ic_launcher_nfc.png differ diff --git a/apps/Tag/res/drawable-mdpi/ic_menu_desk_clock.png b/apps/Tag/res/drawable-mdpi/ic_menu_desk_clock.png new file mode 100644 index 000000000..e4fa67196 Binary files /dev/null and b/apps/Tag/res/drawable-mdpi/ic_menu_desk_clock.png differ diff --git a/apps/Tag/res/drawable-mdpi/ic_menu_tag.png b/apps/Tag/res/drawable-mdpi/ic_menu_tag.png new file mode 100644 index 000000000..e0b3eb73d Binary files /dev/null and b/apps/Tag/res/drawable-mdpi/ic_menu_tag.png differ diff --git a/apps/Tag/res/values/styles.xml b/apps/Tag/res/layout/tag_divider.xml similarity index 77% rename from apps/Tag/res/values/styles.xml rename to apps/Tag/res/layout/tag_divider.xml index 40e29fb1d..b6b1b7cd4 100644 --- a/apps/Tag/res/values/styles.xml +++ b/apps/Tag/res/layout/tag_divider.xml @@ -14,10 +14,9 @@ See the License for the specific language governing permissions and limitations under the License. --> - - - - - + \ No newline at end of file diff --git a/apps/Tag/res/layout/tag_text.xml b/apps/Tag/res/layout/tag_text.xml new file mode 100644 index 000000000..903865055 --- /dev/null +++ b/apps/Tag/res/layout/tag_text.xml @@ -0,0 +1,26 @@ + + + + \ No newline at end of file diff --git a/apps/Tag/res/layout/tag_viewer_list.xml b/apps/Tag/res/layout/tag_viewer_list.xml new file mode 100644 index 000000000..3d322d539 --- /dev/null +++ b/apps/Tag/res/layout/tag_viewer_list.xml @@ -0,0 +1,21 @@ + + + + diff --git a/apps/Tag/res/values/strings.xml b/apps/Tag/res/values/strings.xml index 3d0432765..9b736784e 100644 --- a/apps/Tag/res/values/strings.xml +++ b/apps/Tag/res/values/strings.xml @@ -20,4 +20,10 @@ help and info Saved + + Recent + + + Saved + diff --git a/apps/Tag/src/com/android/apps/tag/MockNdefMessages.java b/apps/Tag/src/com/android/apps/tag/MockNdefMessages.java new file mode 100644 index 000000000..3acdc5c64 --- /dev/null +++ b/apps/Tag/src/com/android/apps/tag/MockNdefMessages.java @@ -0,0 +1,224 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apps.tag; + +/** + * Tags that we've seen in the field, for testing purposes. + */ +public class MockNdefMessages { + + /** + * A real NFC tag containing an NFC "smart poster". This smart poster + * consists of the text "NFC Forum Type 4 Tag" in english combined with + * the URL "http://www.nxp.com/nfc" + */ + public static final byte[] REAL_NFC_MSG = new byte[] { + (byte) 0xd1, // MB=1 ME=1 CF=0 SR=1 IL=0 TNF=001 + (byte) 0x02, // Type Length = 2 + (byte) 0x2b, // Payload Length = 43 + (byte) 0x53, (byte) 0x70, // Type = {'S', 'p'} (smart poster) + + // begin smart poster payload + // begin smart poster record #1 + (byte) 0x91, // MB=1 ME=0 CF=0 SR=1 IL=0 TNF=001 + (byte) 0x01, // Type Length = 1 + (byte) 0x17, // Payload Length = 23 + (byte) 0x54, // Type = {'T'} (Text data) + (byte) 0x02, // UTF-8 encoding, language code length = 2 + (byte) 0x65, (byte) 0x6e, // language = {'e', 'n'} (english) + + // Begin text data within smart poster record #1 + (byte) 0x4e, // 'N' + (byte) 0x46, // 'F' + (byte) 0x43, // 'C' + (byte) 0x20, // ' ' + (byte) 0x46, // 'F' + (byte) 0x6f, // 'o' + (byte) 0x72, // 'r' + (byte) 0x75, // 'u' + (byte) 0x6d, // 'm' + (byte) 0x20, // ' ' + (byte) 0x54, // 'T' + (byte) 0x79, // 'y' + (byte) 0x70, // 'p' + (byte) 0x65, // 'e' + (byte) 0x20, // ' ' + (byte) 0x34, // '4' + (byte) 0x20, // ' ' + (byte) 0x54, // 'T' + (byte) 0x61, // 'a' + (byte) 0x67, // 'g' + // end Text data within smart poster record #1 + // end smart poster record #1 + + // begin smart poster record #2 + (byte) 0x51, // MB=0 ME=1 CF=0 SR=1 IL=0 TNF=001 + (byte) 0x01, // Type Length = 1 + (byte) 0x0c, // Payload Length = 12 + (byte) 0x55, // Type = { 'U' } (URI) + + // begin URI data within smart poster record #2 + (byte) 0x01, // URI Prefix = 1 ("http://www.") + (byte) 0x6e, // 'n' + (byte) 0x78, // 'x' + (byte) 0x70, // 'p' + (byte) 0x2e, // '.' + (byte) 0x63, // 'c' + (byte) 0x6f, // 'o' + (byte) 0x6d, // 'm' + (byte) 0x2f, // '/' + (byte) 0x6e, // 'n' + (byte) 0x66, // 'f' + (byte) 0x63 // 'c' + // end URI data within smart poster record #2 + // end smart poster record #2 + // end smart poster payload + }; + + + /** + * A Smart Poster containing a URL and no text. This message was created + * using the NXP reference phone. + */ + private static final byte[] SMART_POSTER_URL_NO_TEXT = new byte[] { + (byte) 0xd1, (byte) 0x02, (byte) 0x0f, (byte) 0x53, (byte) 0x70, (byte) 0xd1, + (byte) 0x01, (byte) 0x0b, (byte) 0x55, (byte) 0x01, (byte) 0x67, (byte) 0x6f, + (byte) 0x6f, (byte) 0x67, (byte) 0x6c, (byte) 0x65, (byte) 0x2e, (byte) 0x63, + (byte) 0x6f, (byte) 0x6d + }; + + /** + * A plain text tag in english. Generated using the NXP evaluation tool. + */ + private static final byte[] ENGLISH_PLAIN_TEXT = new byte[] { + (byte) 0xd1, (byte) 0x01, (byte) 0x1c, (byte) 0x54, (byte) 0x02, (byte) 0x65, + (byte) 0x6e, (byte) 0x53, (byte) 0x6f, (byte) 0x6d, (byte) 0x65, (byte) 0x20, + (byte) 0x72, (byte) 0x61, (byte) 0x6e, (byte) 0x64, (byte) 0x6f, (byte) 0x6d, + (byte) 0x20, (byte) 0x65, (byte) 0x6e, (byte) 0x67, (byte) 0x6c, (byte) 0x69, + (byte) 0x73, (byte) 0x68, (byte) 0x20, (byte) 0x74, (byte) 0x65, (byte) 0x78, + (byte) 0x74, (byte) 0x2e + }; + + /** + * Smart Poster containing a URL and Text. Generated using the NXP + * evaluation tool. + */ + private static final byte[] SMART_POSTER_URL_AND_TEXT = new byte[] { + (byte) 0xd1, (byte) 0x02, (byte) 0x1c, (byte) 0x53, (byte) 0x70, (byte) 0x91, + (byte) 0x01, (byte) 0x09, (byte) 0x54, (byte) 0x02, (byte) 0x65, (byte) 0x6e, + (byte) 0x47, (byte) 0x6f, (byte) 0x6f, (byte) 0x67, (byte) 0x6c, (byte) 0x65, + (byte) 0x51, (byte) 0x01, (byte) 0x0b, (byte) 0x55, (byte) 0x01, (byte) 0x67, + (byte) 0x6f, (byte) 0x6f, (byte) 0x67, (byte) 0x6c, (byte) 0x65, (byte) 0x2e, + (byte) 0x63, (byte) 0x6f, (byte) 0x6d + }; + + /** + * A plain URI. Generated using the NXP evaluation tool. + */ + private static final byte[] URI = new byte[] { + (byte) 0xd1, (byte) 0x01, (byte) 0x0b, (byte) 0x55, (byte) 0x01, (byte) 0x67, + (byte) 0x6f, (byte) 0x6f, (byte) 0x67, (byte) 0x6c, (byte) 0x65, (byte) 0x2e, + (byte) 0x63, (byte) 0x6f, (byte) 0x6d + }; + + /** + * A vcard. Generated using the NXP evaluation tool. + */ + private static final byte[] VCARD = new byte[] { + (byte) 0xc2, (byte) 0x0c, (byte) 0x00, (byte) 0x00, (byte) 0x01, (byte) 0x05, + (byte) 0x74, (byte) 0x65, (byte) 0x78, (byte) 0x74, (byte) 0x2f, (byte) 0x78, + (byte) 0x2d, (byte) 0x76, (byte) 0x43, (byte) 0x61, (byte) 0x72, (byte) 0x64, + (byte) 0x42, (byte) 0x45, (byte) 0x47, (byte) 0x49, (byte) 0x4e, (byte) 0x3a, + (byte) 0x56, (byte) 0x43, (byte) 0x41, (byte) 0x52, (byte) 0x44, (byte) 0x0d, + (byte) 0x0a, (byte) 0x56, (byte) 0x45, (byte) 0x52, (byte) 0x53, (byte) 0x49, + (byte) 0x4f, (byte) 0x4e, (byte) 0x3a, (byte) 0x33, (byte) 0x2e, (byte) 0x30, + (byte) 0x0d, (byte) 0x0a, (byte) 0x46, (byte) 0x4e, (byte) 0x3a, (byte) 0x4a, + (byte) 0x6f, (byte) 0x65, (byte) 0x20, (byte) 0x47, (byte) 0x6f, (byte) 0x6f, + (byte) 0x67, (byte) 0x6c, (byte) 0x65, (byte) 0x20, (byte) 0x45, (byte) 0x6d, + (byte) 0x70, (byte) 0x6c, (byte) 0x6f, (byte) 0x79, (byte) 0x65, (byte) 0x65, + (byte) 0x0d, (byte) 0x0a, (byte) 0x41, (byte) 0x44, (byte) 0x52, (byte) 0x3b, + (byte) 0x54, (byte) 0x59, (byte) 0x50, (byte) 0x45, (byte) 0x3d, (byte) 0x57, + (byte) 0x4f, (byte) 0x52, (byte) 0x4b, (byte) 0x3a, (byte) 0x3b, (byte) 0x3b, + (byte) 0x31, (byte) 0x36, (byte) 0x30, (byte) 0x30, (byte) 0x20, (byte) 0x41, + (byte) 0x6d, (byte) 0x70, (byte) 0x68, (byte) 0x69, (byte) 0x74, (byte) 0x68, + (byte) 0x65, (byte) 0x61, (byte) 0x74, (byte) 0x72, (byte) 0x65, (byte) 0x20, + (byte) 0x50, (byte) 0x61, (byte) 0x72, (byte) 0x6b, (byte) 0x77, (byte) 0x61, + (byte) 0x79, (byte) 0x3b, (byte) 0x39, (byte) 0x34, (byte) 0x30, (byte) 0x34, + (byte) 0x33, (byte) 0x20, (byte) 0x4d, (byte) 0x6f, (byte) 0x75, (byte) 0x6e, + (byte) 0x74, (byte) 0x61, (byte) 0x69, (byte) 0x6e, (byte) 0x20, (byte) 0x56, + (byte) 0x69, (byte) 0x65, (byte) 0x77, (byte) 0x0d, (byte) 0x0a, (byte) 0x54, + (byte) 0x45, (byte) 0x4c, (byte) 0x3b, (byte) 0x54, (byte) 0x59, (byte) 0x50, + (byte) 0x45, (byte) 0x3d, (byte) 0x50, (byte) 0x52, (byte) 0x45, (byte) 0x46, + (byte) 0x2c, (byte) 0x57, (byte) 0x4f, (byte) 0x52, (byte) 0x4b, (byte) 0x3a, + (byte) 0x36, (byte) 0x35, (byte) 0x30, (byte) 0x2d, (byte) 0x32, (byte) 0x35, + (byte) 0x33, (byte) 0x2d, (byte) 0x30, (byte) 0x30, (byte) 0x30, (byte) 0x30, + (byte) 0x0d, (byte) 0x0a, (byte) 0x45, (byte) 0x4d, (byte) 0x41, (byte) 0x49, + (byte) 0x4c, (byte) 0x3b, (byte) 0x54, (byte) 0x59, (byte) 0x50, (byte) 0x45, + (byte) 0x3d, (byte) 0x49, (byte) 0x4e, (byte) 0x54, (byte) 0x45, (byte) 0x52, + (byte) 0x4e, (byte) 0x45, (byte) 0x54, (byte) 0x3a, (byte) 0x73, (byte) 0x75, + (byte) 0x70, (byte) 0x70, (byte) 0x6f, (byte) 0x72, (byte) 0x74, (byte) 0x40, + (byte) 0x67, (byte) 0x6f, (byte) 0x6f, (byte) 0x67, (byte) 0x6c, (byte) 0x65, + (byte) 0x2e, (byte) 0x63, (byte) 0x6f, (byte) 0x6d, (byte) 0x0d, (byte) 0x0a, + (byte) 0x54, (byte) 0x49, (byte) 0x54, (byte) 0x4c, (byte) 0x45, (byte) 0x3a, + (byte) 0x53, (byte) 0x6f, (byte) 0x66, (byte) 0x74, (byte) 0x77, (byte) 0x61, + (byte) 0x72, (byte) 0x65, (byte) 0x20, (byte) 0x45, (byte) 0x6e, (byte) 0x67, + (byte) 0x69, (byte) 0x6e, (byte) 0x65, (byte) 0x65, (byte) 0x72, (byte) 0x0d, + (byte) 0x0a, (byte) 0x4f, (byte) 0x52, (byte) 0x47, (byte) 0x3a, (byte) 0x47, + (byte) 0x6f, (byte) 0x6f, (byte) 0x67, (byte) 0x6c, (byte) 0x65, (byte) 0x0d, + (byte) 0x0a, (byte) 0x55, (byte) 0x52, (byte) 0x4c, (byte) 0x3a, (byte) 0x68, + (byte) 0x74, (byte) 0x74, (byte) 0x70, (byte) 0x3a, (byte) 0x2f, (byte) 0x2f, + (byte) 0x77, (byte) 0x77, (byte) 0x77, (byte) 0x2e, (byte) 0x67, (byte) 0x6f, + (byte) 0x6f, (byte) 0x67, (byte) 0x6c, (byte) 0x65, (byte) 0x2e, (byte) 0x63, + (byte) 0x6f, (byte) 0x6d, (byte) 0x0d, (byte) 0x0a, (byte) 0x45, (byte) 0x4e, + (byte) 0x44, (byte) 0x3a, (byte) 0x56, (byte) 0x43, (byte) 0x41, (byte) 0x52, + (byte) 0x44, (byte) 0x0d, (byte) 0x0a + }; + + /** + * Send the text message "hello world" to a phone number. This was generated using + * the NXP reference phone. + */ + private static final byte[] SEND_TEXT_MESSAGE = new byte[] { + (byte) 0xd1, (byte) 0x02, (byte) 0x25, (byte) 0x53, (byte) 0x70, (byte) 0xd1, + (byte) 0x01, (byte) 0x21, (byte) 0x55, (byte) 0x00, (byte) 0x73, (byte) 0x6d, + (byte) 0x73, (byte) 0x3a, (byte) 0x31, (byte) 0x36, (byte) 0x35, (byte) 0x30, + (byte) 0x32, (byte) 0x35, (byte) 0x33, (byte) 0x30, (byte) 0x30, (byte) 0x30, + (byte) 0x30, (byte) 0x3f, (byte) 0x62, (byte) 0x6f, (byte) 0x64, (byte) 0x79, + (byte) 0x3d, (byte) 0x48, (byte) 0x65, (byte) 0x6c, (byte) 0x6c, (byte) 0x6f, + (byte) 0x20, (byte) 0x77, (byte) 0x6f, (byte) 0x72, (byte) 0x6c, (byte) 0x64 + }; + + /** + * Call Google. Generated using the NXP reference phone. + */ + private static final byte[] CALL_GOOGLE = new byte[] { + (byte) 0xd1, (byte) 0x02, (byte) 0x10, (byte) 0x53, (byte) 0x70, (byte) 0xd1, + (byte) 0x01, (byte) 0x0c, (byte) 0x55, (byte) 0x05, (byte) 0x31, (byte) 0x36, + (byte) 0x35, (byte) 0x30, (byte) 0x32, (byte) 0x35, (byte) 0x33, (byte) 0x30, + (byte) 0x30, (byte) 0x30, (byte) 0x30 + }; + + /** + * All the real ndef messages we've seen in the field. + */ + public static final byte[][] ALL_MOCK_MESSAGES = new byte[][] { + REAL_NFC_MSG, SMART_POSTER_URL_NO_TEXT, ENGLISH_PLAIN_TEXT, + SMART_POSTER_URL_AND_TEXT, URI, VCARD, SEND_TEXT_MESSAGE, + CALL_GOOGLE + }; + +} diff --git a/apps/Tag/src/com/android/apps/tag/NdefUtil.java b/apps/Tag/src/com/android/apps/tag/NdefUtil.java index 87efb9e43..eec7c1918 100644 --- a/apps/Tag/src/com/android/apps/tag/NdefUtil.java +++ b/apps/Tag/src/com/android/apps/tag/NdefUtil.java @@ -16,21 +16,19 @@ package com.android.apps.tag; -import android.util.Log; -import com.google.common.base.Preconditions; -import com.google.common.collect.BiMap; -import com.google.common.collect.ImmutableBiMap; +import com.android.apps.tag.record.ParsedNdefRecord; +import com.android.apps.tag.record.SmartPoster; +import com.android.apps.tag.record.TextRecord; +import com.android.apps.tag.record.UriRecord; import com.google.common.collect.Iterables; import com.google.common.primitives.Bytes; + +import android.net.Uri; import android.nfc.NdefMessage; import android.nfc.NdefRecord; -import java.io.UnsupportedEncodingException; -import java.net.URI; -import java.net.URISyntaxException; import java.nio.charset.Charsets; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; /** @@ -42,55 +40,9 @@ public class NdefUtil { private static final byte[] EMPTY = new byte[0]; /** - * NFC Forum "URI Record Type Definition" - * - * This is a mapping of "URI Identifier Codes" to URI string prefixes, - * per section 3.2.2 of the NFC Forum URI Record Type Definition document. + * Create a new {@link NdefRecord} containing the supplied {@link Uri}. */ - private static final - BiMap URI_PREFIX_MAP = ImmutableBiMap.builder() - .put((byte) 0x00, "") - .put((byte) 0x01, "http://www.") - .put((byte) 0x02, "https://www.") - .put((byte) 0x03, "http://") - .put((byte) 0x04, "https://") - .put((byte) 0x05, "tel:") - .put((byte) 0x06, "mailto:") - .put((byte) 0x07, "ftp://anonymous:anonymous@") - .put((byte) 0x08, "ftp://ftp.") - .put((byte) 0x09, "ftps://") - .put((byte) 0x0A, "sftp://") - .put((byte) 0x0B, "smb://") - .put((byte) 0x0C, "nfs://") - .put((byte) 0x0D, "ftp://") - .put((byte) 0x0E, "dav://") - .put((byte) 0x0F, "news:") - .put((byte) 0x10, "telnet://") - .put((byte) 0x11, "imap:") - .put((byte) 0x12, "rtsp://") - .put((byte) 0x13, "urn:") - .put((byte) 0x14, "pop:") - .put((byte) 0x15, "sip:") - .put((byte) 0x16, "sips:") - .put((byte) 0x17, "tftp:") - .put((byte) 0x18, "btspp://") - .put((byte) 0x19, "btl2cap://") - .put((byte) 0x1A, "btgoep://") - .put((byte) 0x1B, "tcpobex://") - .put((byte) 0x1C, "irdaobex://") - .put((byte) 0x1D, "file://") - .put((byte) 0x1E, "urn:epc:id:") - .put((byte) 0x1F, "urn:epc:tag:") - .put((byte) 0x20, "urn:epc:pat:") - .put((byte) 0x21, "urn:epc:raw:") - .put((byte) 0x22, "urn:epc:") - .put((byte) 0x23, "urn:nfc:") - .build(); - - /** - * Create a new {@link NdefRecord} containing the supplied {@link URI}. - */ - public static NdefRecord toUriRecord(URI uri) { + public static NdefRecord toUriRecord(Uri uri) { byte[] uriBytes = uri.toString().getBytes(Charsets.UTF_8); /* @@ -109,131 +61,33 @@ public class NdefUtil { NdefRecord.RTD_URI, EMPTY, payload); } - /** - * Convert {@link NdefRecord} into a {@link URI}. - * - * TODO: This class does not handle NdefRecords where the TNF - * (Type Name Format) of the class is {@link NdefRecord#TNF_ABSOLUTE_URI}. - * This should be fixed. - * - * @throws URISyntaxException if the {@code NdefRecord} contains an - * invalid URI. - * @throws IllegalArgumentException if the NdefRecord is not a - * record containing a URI. - */ - public static URI toURI(NdefRecord record) throws URISyntaxException { - Preconditions.checkArgument(record.getTnf() == NdefRecord.TNF_WELL_KNOWN); - Preconditions.checkArgument(Arrays.equals(record.getType(), NdefRecord.RTD_URI)); - - byte[] payload = record.getPayload(); - - /* - * payload[0] contains the URI Identifier Code, per the - * NFC Forum "URI Record Type Definition" section 3.2.2. - * - * payload[1]...payload[payload.length - 1] contains the rest of - * the URI. - */ - - String prefix = URI_PREFIX_MAP.get(payload[0]); - byte[] fullUri = Bytes.concat( - prefix.getBytes(Charsets.UTF_8), - Arrays.copyOfRange(payload, 1, payload.length)); - - return new URI(new String(fullUri, Charsets.UTF_8)); + public static Iterable getTextFields(NdefMessage message) { + return Iterables.filter(getObjects(message), TextRecord.class); } - public static boolean isURI(NdefRecord record) { - try { - toURI(record); - return true; - } catch (IllegalArgumentException e) { - return false; - } catch (URISyntaxException e) { - return false; - } - } - - /** - * Extracts payload text from Text type ndef record. - * - * @param record A ndef record. Must be {@link NdefRecord#TYPE_TEXT}. - * @return text payload. - */ - public static String toText(NdefRecord record) { - Preconditions.checkArgument(record.getTnf() == NdefRecord.TNF_WELL_KNOWN); - Preconditions.checkArgument(Arrays.equals(record.getType(), NdefRecord.RTD_TEXT)); - try { - - byte[] payload = record.getPayload(); - - /* - * payload[0] contains the "Status Byte Encodings" field, per - * the NFC Forum "Text Record Type Definition" section 3.2.1. - * - * bit7 is the Text Encoding Field. - * - * if (Bit_7 == 0): The text is encoded in UTF-8 - * if (Bit_7 == 1): The text is encoded in UTF16 - * - * Bit_6 is reserved for future use and must be set to zero. - * - * Bits 5 to 0 are the length of the IANA language code. - */ - - String textEncoding = ((payload[0] & 0200) == 0) ? "UTF-8" : "UTF-16"; - int languageCodeLength = payload[0] & 0077; - - return new String(payload, - languageCodeLength + 1, - payload.length - languageCodeLength - 1, - textEncoding); - } catch (UnsupportedEncodingException e) { - // should never happen unless we get a malformed tag. - throw new IllegalArgumentException(e); - } - } - - public static boolean isText(NdefRecord record) { - try { - toText(record); - return true; - } catch (IllegalArgumentException e) { - return false; - } - } - - public static Iterable getTextFields(NdefMessage message) { - return Iterables.filter(getObjects(message), String.class); - } - - public static Iterable getURIs(NdefMessage message) { - return Iterables.filter(getObjects(message), URI.class); + public static Iterable getUris(NdefMessage message) { + return Iterables.filter(getObjects(message), UriRecord.class); } /** * Parse the provided {@code NdefMessage}, extracting all known * objects from the message. Typically this list will consist of - * {@link String}s corresponding to NDEF text records, or {@link URI}s + * {@link String}s corresponding to NDEF text records, or {@link Uri}s * corresponding to NDEF URI records. *

* TODO: Is this API too generic? Should we keep it? */ - private static Iterable getObjects(NdefMessage message) { - try { - List retval = new ArrayList(); - for (NdefRecord record : message.getRecords()) { - if (isURI(record)) { - retval.add(toURI(record)); - } else if (isText(record)) { - retval.add(toText(record)); - } else if (SmartPoster.isPoster(record)) { - retval.add(SmartPoster.from(record)); - } + public static Iterable getObjects(NdefMessage message) { + List retval = new ArrayList(); + for (NdefRecord record : message.getRecords()) { + if (UriRecord.isUri(record)) { + retval.add(UriRecord.parse(record)); + } else if (TextRecord.isText(record)) { + retval.add(TextRecord.parse(record)); + } else if (SmartPoster.isPoster(record)) { + retval.add(SmartPoster.parse(record)); } - return retval; - } catch (URISyntaxException e) { - throw new IllegalArgumentException(e); } + return retval; } } diff --git a/apps/Tag/src/com/android/apps/tag/SaveTag.java b/apps/Tag/src/com/android/apps/tag/SaveTag.java deleted file mode 100644 index 6a5f66fcc..000000000 --- a/apps/Tag/src/com/android/apps/tag/SaveTag.java +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Copyright (C) 2010 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.apps.tag; - -import android.app.Activity; -import android.app.AlertDialog; -import android.app.Dialog; -import android.content.DialogInterface; -import android.nfc.NdefMessage; -import android.nfc.NdefTag; -import android.nfc.NfcAdapter; -import android.os.Bundle; -import android.util.Log; -import android.view.WindowManager; -import android.widget.Toast; - -/** - * An {@code Activity} which handles a broadcast of a new tag that the device just discovered. - */ -public class SaveTag extends Activity implements DialogInterface.OnClickListener { - private static final String TAG = "SaveTag"; - - @Override - protected void onStart() { - super.onStart(); - - getWindow().addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED - | WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD - | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON - | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON - | WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON - | WindowManager.LayoutParams.FLAG_DIM_BEHIND - ); - - showDialog(1); - NdefTag tag = getIntent().getParcelableExtra(NfcAdapter.EXTRA_TAG); - NdefMessage[] msgs = tag.getNdefMessages(); - - if (msgs.length == 0) { - Log.d(TAG, "No NDEF messages"); - return; - } - if (msgs.length > 1) { - Log.d(TAG, "Multiple NDEF messages, only saving first"); - } - String s = toHexString(msgs[0].toByteArray()); - Log.d("SaveTag", s); - Toast.makeText(this.getBaseContext(), "SaveTag: " + s, Toast.LENGTH_SHORT).show(); - } - - @Override - protected Dialog onCreateDialog(int id, Bundle args) { - return new AlertDialog.Builder(this) - .setTitle("Welcome! T2000 Festival") - .setPositiveButton("Save", this) - .setNegativeButton("Cancel", this) - .create(); - } - - @Override - public void onClick(DialogInterface dialog, int which) { - finish(); - } - - @Override - protected void onStop() { - super.onStop(); - dismissDialog(1); - } - - private static final char[] hexDigits = "0123456789abcdef".toCharArray(); - - private static String toHexString(byte[] bytes) { - StringBuilder sb = new StringBuilder(3 * bytes.length); - for (byte b : bytes) { - sb.append("(byte) 0x") - .append(hexDigits[(b >> 4) & 0xf]) - .append(hexDigits[b & 0xf]).append(", "); - } - return sb.toString(); - } -} diff --git a/apps/Tag/src/com/android/apps/tag/TagAdapter.java b/apps/Tag/src/com/android/apps/tag/TagAdapter.java new file mode 100644 index 000000000..04bc46aa4 --- /dev/null +++ b/apps/Tag/src/com/android/apps/tag/TagAdapter.java @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apps.tag; + +import com.android.apps.tag.TagDBHelper.NdefMessagesTable; + +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.nfc.FormatException; +import android.nfc.NdefMessage; +import android.nfc.NdefRecord; +import android.text.format.DateUtils; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Adapter; +import android.widget.CursorAdapter; +import android.widget.TextView; +import com.android.apps.tag.record.SmartPoster; +import com.android.apps.tag.record.TextRecord; +import com.android.apps.tag.record.UriRecord; + +/** + * A custom {@link Adapter} that renders tag entries for a list. + */ +public class TagAdapter extends CursorAdapter { + + private final LayoutInflater mInflater; + + public TagAdapter(Context context) { + super(context, null, false); + mInflater = LayoutInflater.from(context); + } + + @Override + public void bindView(View view, Context context, Cursor cursor) { + TextView mainLine = (TextView) view.findViewById(R.id.title); + TextView dateLine = (TextView) view.findViewById(R.id.date); + + NdefMessage msg = null; + try { + msg = new NdefMessage(cursor.getBlob(cursor.getColumnIndex(NdefMessagesTable.BYTES))); + } catch (FormatException e) { + Log.e("foo", "poorly formatted message", e); + } + + if (msg == null) { + mainLine.setText("Invalid tag"); + } else { + try { + SmartPoster poster = SmartPoster.parse(msg.getRecords()[0]); + TextRecord title = poster.getTitle(); + if (title != null) { + mainLine.setText(title.getText()); + } + } catch (IllegalArgumentException e) { + // Not a smart poster + NdefRecord record = msg.getRecords()[0]; + Uri uri = null; + try { + uri = UriRecord.parse(record).getUri(); + mainLine.setText(uri.toString()); + } catch (IllegalArgumentException e2) { + mainLine.setText("Not a smart poster or URL"); + } + } + } + dateLine.setText(DateUtils.getRelativeTimeSpanString( + context, cursor.getLong(cursor.getColumnIndex(NdefMessagesTable.DATE)))); + } + + @Override + public View newView(Context context, Cursor cursor, ViewGroup parent) { + return mInflater.inflate(R.layout.tag_list_item, null); + } +} diff --git a/apps/Tag/src/com/android/apps/tag/TagBroadcastReceiver.java b/apps/Tag/src/com/android/apps/tag/TagBroadcastReceiver.java deleted file mode 100644 index b5ef1e740..000000000 --- a/apps/Tag/src/com/android/apps/tag/TagBroadcastReceiver.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright (C) 2010 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.apps.tag; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.nfc.NdefTag; - -import android.nfc.NdefMessage; -import android.nfc.NfcAdapter; - -/** - * When we receive a new NDEF tag, start the activity to - * process the tag. - */ -public class TagBroadcastReceiver extends BroadcastReceiver { - - @Override - public void onReceive(Context context, Intent intent) { - if (intent.getAction().equals(NfcAdapter.ACTION_NDEF_TAG_DISCOVERED)) { - NdefTag tag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG); - Intent i = new Intent(context, SaveTag.class) - .putExtra(NfcAdapter.EXTRA_TAG, tag) - .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - context.startActivity(i); - } - } -} diff --git a/apps/Tag/src/com/android/apps/tag/TagBrowserActivity.java b/apps/Tag/src/com/android/apps/tag/TagBrowserActivity.java index a4cbb86a6..38d3bfab8 100644 --- a/apps/Tag/src/com/android/apps/tag/TagBrowserActivity.java +++ b/apps/Tag/src/com/android/apps/tag/TagBrowserActivity.java @@ -16,6 +16,7 @@ package com.android.apps.tag; +import android.app.Activity; import android.app.TabActivity; import android.content.Intent; import android.content.res.Resources; @@ -23,34 +24,27 @@ import android.os.Bundle; import android.widget.TabHost; /** - * A browsing {@code Activity} that displays the saved tags in categories under tabs. + * A browsing {@link Activity} that displays the saved tags in categories under tabs. */ public class TagBrowserActivity extends TabActivity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - // While we're doing development, delete the database every time we start. - getBaseContext().getDatabasePath("Tags.db").delete(); - setContentView(R.layout.main); Resources res = getResources(); TabHost tabHost = getTabHost(); - Intent i = new Intent().setClass(this, TagList.class); - Intent iSavedList = new Intent().setClass(this, TagList.class) - .putExtra(TagList.SHOW_SAVED_ONLY, true); - Intent iRecentList = new Intent().setClass(this, TagList.class); - - TabHost.TabSpec spec1 = tabHost.newTabSpec("1") - .setIndicator("Saved", res.getDrawable(R.drawable.ic_menu_tag)) - .setContent(iSavedList); + TabHost.TabSpec spec1 = tabHost.newTabSpec("saved") + .setIndicator(getText(R.string.tab_saved), res.getDrawable(R.drawable.ic_menu_tag)) + .setContent(new Intent().setClass(this, TagList.class) + .putExtra(TagList.EXTRA_SHOW_SAVED_ONLY, true)); tabHost.addTab(spec1); - TabHost.TabSpec spec2 = tabHost.newTabSpec("2") - .setIndicator("Recent", res.getDrawable(R.drawable.ic_menu_desk_clock)) - .setContent(iRecentList); + TabHost.TabSpec spec2 = tabHost.newTabSpec("recent") + .setIndicator(getText(R.string.tab_recent), res.getDrawable(R.drawable.ic_menu_desk_clock)) + .setContent(new Intent().setClass(this, TagList.class)); tabHost.addTab(spec2); } } diff --git a/apps/Tag/src/com/android/apps/tag/TagCursorAdapter.java b/apps/Tag/src/com/android/apps/tag/TagCursorAdapter.java deleted file mode 100644 index a658268e6..000000000 --- a/apps/Tag/src/com/android/apps/tag/TagCursorAdapter.java +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright 2010 Google Inc. All Rights Reserved. - -package com.android.apps.tag; - -import android.content.Context; -import android.database.Cursor; -import android.text.format.DateUtils; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.Adapter; -import android.widget.CursorAdapter; -import android.widget.TextView; - -/** - * A custom {@link Adapter} that renders tag entries for a list. - */ -public class TagCursorAdapter extends CursorAdapter { - - private final LayoutInflater mInflater; - - public TagCursorAdapter(Context context, Cursor c) { - super(context, c); - - mInflater = LayoutInflater.from(context); - } - - @Override - public void bindView(View view, Context context, Cursor cursor) { - TextView mainLine = (TextView) view.findViewById(R.id.title); - TextView dateLine = (TextView) view.findViewById(R.id.date); - - // TODO(benkomalo): either write a cursor abstraction, or use constants for column indices. - mainLine.setText(cursor.getString(cursor.getColumnIndex("bytes"))); - dateLine.setText(DateUtils.getRelativeTimeSpanString( - context, cursor.getLong(cursor.getColumnIndex("date")))); - } - - @Override - public View newView(Context context, Cursor cursor, ViewGroup parent) { - return mInflater.inflate(R.layout.tag_list_item, null); - } -} diff --git a/apps/Tag/src/com/android/apps/tag/TagDBHelper.java b/apps/Tag/src/com/android/apps/tag/TagDBHelper.java index 06fa9e001..3cb7844c1 100644 --- a/apps/Tag/src/com/android/apps/tag/TagDBHelper.java +++ b/apps/Tag/src/com/android/apps/tag/TagDBHelper.java @@ -22,146 +22,110 @@ import android.content.Context; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import android.database.sqlite.SQLiteStatement; +import android.net.Uri; import android.nfc.FormatException; import android.nfc.NdefMessage; import android.nfc.NdefRecord; -import java.net.URI; -import java.util.Date; - /** * Database utilities for the saved tags. */ public class TagDBHelper extends SQLiteOpenHelper { - private static final int DATABASE_VERSION = 1; + private static final String DATABASE_NAME = "tags.db"; + private static final int DATABASE_VERSION = 3; - private static final String NDEF_MSG = "create table NdefMessage (" - + "_id INTEGER NOT NULL, " - + "bytes BLOB NOT NULL, " - + "date INTEGER NOT NULL, " - + "saved TEXT NOT NULL default 0," // boolean - + "PRIMARY KEY(_id)" - + ")"; + public interface NdefMessagesTable { + public static final String TABLE_NAME = "nedf_msg"; - private static final String INSERT = - "INSERT INTO NdefMessage (bytes, date, saved) values (?, ?, ?)"; + public static final String _ID = "_id"; + public static final String TITLE = "title"; + public static final String BYTES = "bytes"; + public static final String DATE = "date"; + public static final String SAVED = "saved"; + } - /** - * A real NFC tag containing an NFC "smart poster". This smart poster - * consists of the text "NFC Forum Type 4 Tag" in english combined with - * the URL "http://www.nxp.com/nfc" - */ - public static final byte[] REAL_NFC_MSG = new byte[] { - (byte) 0xd1, // MB=1 ME=1 CF=0 SR=1 IL=0 TNF=001 - (byte) 0x02, // Type Length = 2 - (byte) 0x2b, // Payload Length = 43 - (byte) 0x53, (byte) 0x70, // Type = {'S', 'p'} (smart poster) + private static TagDBHelper sInstance; - // begin smart poster payload - // begin smart poster record #1 - (byte) 0x91, // MB=1 ME=0 CF=0 SR=1 IL=0 TNF=001 - (byte) 0x01, // Type Length = 1 - (byte) 0x17, // Payload Length = 23 - (byte) 0x54, // Type = {'T'} (Text data) - (byte) 0x02, // UTF-8 encoding, language code length = 2 - (byte) 0x65, (byte) 0x6e, // language = {'e', 'n'} (english) + public static synchronized TagDBHelper getInstance(Context context) { + if (sInstance == null) { + sInstance = new TagDBHelper(context.getApplicationContext()); + } + return sInstance; + } - // Begin text data within smart poster record #1 - (byte) 0x4e, // 'N' - (byte) 0x46, // 'F' - (byte) 0x43, // 'C' - (byte) 0x20, // ' ' - (byte) 0x46, // 'F' - (byte) 0x6f, // 'o' - (byte) 0x72, // 'r' - (byte) 0x75, // 'u' - (byte) 0x6d, // 'm' - (byte) 0x20, // ' ' - (byte) 0x54, // 'T' - (byte) 0x79, // 'y' - (byte) 0x70, // 'p' - (byte) 0x65, // 'e' - (byte) 0x20, // ' ' - (byte) 0x34, // '4' - (byte) 0x20, // ' ' - (byte) 0x54, // 'T' - (byte) 0x61, // 'a' - (byte) 0x67, // 'g' - // end Text data within smart poster record #1 - // end smart poster record #1 - - // begin smart poster record #2 - (byte) 0x51, // MB=0 ME=1 CF=0 SR=1 IL=0 TNF=001 - (byte) 0x01, // Type Length = 1 - (byte) 0x0c, // Payload Length = 12 - (byte) 0x55, // Type = { 'U' } (URI) - - // begin URI data within smart poster record #2 - (byte) 0x01, // URI Prefix = 1 ("http://www.") - (byte) 0x6e, // 'n' - (byte) 0x78, // 'x' - (byte) 0x70, // 'p' - (byte) 0x2e, // '.' - (byte) 0x63, // 'c' - (byte) 0x6f, // 'o' - (byte) 0x6d, // 'm' - (byte) 0x2f, // '/' - (byte) 0x6e, // 'n' - (byte) 0x66, // 'f' - (byte) 0x63 // 'c' - // end URI data within smart poster record #2 - // end smart poster record #2 - // end smart poster payload - }; - - public TagDBHelper(Context context) { - this(context, "Tags.db"); + private TagDBHelper(Context context) { + this(context, DATABASE_NAME); } @VisibleForTesting - public TagDBHelper(Context context, String dbFile) { + TagDBHelper(Context context, String dbFile) { super(context, dbFile, null, DATABASE_VERSION); } @Override public void onCreate(SQLiteDatabase db) { - db.execSQL(NDEF_MSG); + db.execSQL("CREATE TABLE " + NdefMessagesTable.TABLE_NAME + " (" + + NdefMessagesTable._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " + + NdefMessagesTable.TITLE + " TEXT NOT NULL DEFAULT ''," + + NdefMessagesTable.BYTES + " BLOB NOT NULL, " + + NdefMessagesTable.DATE + " INTEGER NOT NULL, " + + NdefMessagesTable.SAVED + " INTEGER NOT NULL DEFAULT 0" + // boolean + ");"); + db.execSQL("CREATE INDEX msgIndex ON " + NdefMessagesTable.TABLE_NAME + " (" + + NdefMessagesTable.DATE + " DESC, " + + NdefMessagesTable.SAVED + " ASC" + + ")"); + + addTestData(db); + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + // Drop everything and recreate it for now + db.execSQL("DROP TABLE IF EXISTS " + NdefMessagesTable.TABLE_NAME); + onCreate(db); + } + + private void addTestData(SQLiteDatabase db) { // A fake message containing 1 URL NdefMessage msg1 = new NdefMessage(new NdefRecord[] { - NdefUtil.toUriRecord(URI.create("http://www.google.com")) + NdefUtil.toUriRecord(Uri.parse("http://www.google.com")) }); // A fake message containing 2 URLs NdefMessage msg2 = new NdefMessage(new NdefRecord[] { - NdefUtil.toUriRecord(URI.create("http://www.youtube.com")), - NdefUtil.toUriRecord(URI.create("http://www.android.com")) + NdefUtil.toUriRecord(Uri.parse("http://www.youtube.com")), + NdefUtil.toUriRecord(Uri.parse("http://www.android.com")) }); - insert(db, msg1, false); - insert(db, msg2, true); + insertNdefMessage(db, msg1, false); + insertNdefMessage(db, msg2, true); try { - // A real message obtained from an NFC Forum Type 4 tag. - NdefMessage msg3 = new NdefMessage(REAL_NFC_MSG); - insert(db, msg3, false); + // insert some real messages we found in the field. + for (byte[] msg : MockNdefMessages.ALL_MOCK_MESSAGES) { + NdefMessage msg3 = new NdefMessage(msg); + insertNdefMessage(db, msg3, false); + } } catch (FormatException e) { throw new RuntimeException(e); } } - private void insert(SQLiteDatabase db, NdefMessage msg, boolean isSaved) { - SQLiteStatement stmt = db.compileStatement(INSERT); - stmt.bindString(1, new String(msg.toByteArray())); // TODO: This should be a blob - stmt.bindLong(2, System.currentTimeMillis()); - String isSavedStr = isSaved ? "1" : "0"; - stmt.bindString(3, isSavedStr); - stmt.executeInsert(); - stmt.close(); - } - - @Override - public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + public void insertNdefMessage(SQLiteDatabase db, NdefMessage msg, boolean isSaved) { + SQLiteStatement stmt = null; + try { + stmt = db.compileStatement("INSERT INTO " + NdefMessagesTable.TABLE_NAME + + "(" + NdefMessagesTable.BYTES + ", " + NdefMessagesTable.DATE + ", " + + NdefMessagesTable.SAVED + ") values (?, ?, ?)"); + stmt.bindBlob(1, msg.toByteArray()); + stmt.bindLong(2, System.currentTimeMillis()); + stmt.bindLong(3, isSaved ? 1 : 0); + stmt.executeInsert(); + } finally { + if (stmt != null) stmt.close(); + } } } diff --git a/apps/Tag/src/com/android/apps/tag/TagList.java b/apps/Tag/src/com/android/apps/tag/TagList.java index 369ef6557..45f6f6577 100644 --- a/apps/Tag/src/com/android/apps/tag/TagList.java +++ b/apps/Tag/src/com/android/apps/tag/TagList.java @@ -16,42 +16,47 @@ package com.android.apps.tag; +import com.android.apps.tag.TagDBHelper.NdefMessagesTable; + +import android.app.Activity; import android.app.AlertDialog; import android.app.Dialog; import android.app.ListActivity; import android.content.DialogInterface; +import android.content.Intent; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; +import android.nfc.FormatException; +import android.nfc.NdefMessage; +import android.os.AsyncTask; import android.os.Bundle; +import android.util.Log; import android.view.Menu; import android.view.View; import android.widget.ListView; -import android.widget.SimpleCursorAdapter; /** - * An {@code Activity} that displays a flat list of tags that can be "opened". + * An {@link Activity} that displays a flat list of tags that can be "opened". */ public class TagList extends ListActivity implements DialogInterface.OnClickListener { - private SQLiteDatabase db; - private Cursor cursor; - static final String SHOW_SAVED_ONLY = "show_saved_only"; + static final String TAG = "TagList"; + + static final String EXTRA_SHOW_SAVED_ONLY = "show_saved_only"; + + SQLiteDatabase mDatabase; + TagAdapter mAdapter; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - boolean showSavedOnly = getIntent().getBooleanExtra(SHOW_SAVED_ONLY, false); - db = new TagDBHelper(getBaseContext()).getReadableDatabase(); - String selection = showSavedOnly ? "saved=1" : null; + boolean showSavedOnly = getIntent().getBooleanExtra(EXTRA_SHOW_SAVED_ONLY, false); + mDatabase = TagDBHelper.getInstance(this).getReadableDatabase(); + String selection = showSavedOnly ? NdefMessagesTable.SAVED + "=1" : null; - // TODO: Use an AsyncQueryHandler so that DB queries are not done on UI thread. - cursor = db.query( - "NdefMessage", - new String[] { "_id", "bytes", "date" }, - selection, - null, null, null, null); - - setListAdapter(new TagCursorAdapter(this, cursor)); + new TagLoaderTask().execute(selection); + mAdapter = new TagAdapter(this); + setListAdapter(mAdapter); registerForContextMenu(getListView()); } @@ -75,22 +80,53 @@ public class TagList extends ListActivity implements DialogInterface.OnClickList @Override protected void onDestroy() { - if (cursor != null) { - cursor.close(); - } - if (db != null) { - db.close(); + if (mAdapter != null) { + mAdapter.changeCursor(null); } super.onDestroy(); } @Override protected void onListItemClick(ListView l, View v, int position, long id) { - showDialog(1); - super.onListItemClick(l, v, position, id); + Cursor cursor = mAdapter.getCursor(); + cursor.moveToPosition(position); + byte[] tagBytes = cursor.getBlob(cursor.getColumnIndexOrThrow(NdefMessagesTable.BYTES)); + try { + NdefMessage msg = new NdefMessage(tagBytes); + Intent intent = new Intent(this, TagViewer.class); + intent.putExtra(TagViewer.EXTRA_MESSAGE, msg); + intent.putExtra(TagViewer.EXTRA_TAG_DB_ID, id); + startActivity(intent); + } catch (FormatException e) { + Log.e(TAG, "bad format for tag " + id + ": " + tagBytes, e); + return; + } } @Override public void onClick(DialogInterface dialog, int which) { } + + final class TagLoaderTask extends AsyncTask { + @Override + public Cursor doInBackground(String... args) { + String selection = args[0]; + Cursor cursor = mDatabase.query( + NdefMessagesTable.TABLE_NAME, + new String[] { + NdefMessagesTable._ID, + NdefMessagesTable.BYTES, + NdefMessagesTable.DATE, + NdefMessagesTable.TITLE }, + selection, + null, null, null, null); + cursor.getCount(); + return cursor; + } + + @Override + protected void onPostExecute(Cursor cursor) { + mAdapter.changeCursor(cursor); + } + } } diff --git a/apps/Tag/src/com/android/apps/tag/TagViewer.java b/apps/Tag/src/com/android/apps/tag/TagViewer.java new file mode 100644 index 000000000..aac93c6d1 --- /dev/null +++ b/apps/Tag/src/com/android/apps/tag/TagViewer.java @@ -0,0 +1,145 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apps.tag; + +import android.app.Activity; +import android.content.Intent; +import android.database.sqlite.SQLiteDatabase; +import android.graphics.Color; +import android.nfc.NdefMessage; +import android.nfc.NdefTag; +import android.nfc.NfcAdapter; +import android.os.AsyncTask; +import android.util.Log; +import android.view.ContextThemeWrapper; +import android.view.LayoutInflater; +import android.view.WindowManager; +import android.widget.LinearLayout; +import android.widget.TextView; +import com.android.apps.tag.record.ParsedNdefRecord; +import com.android.apps.tag.record.SmartPoster; +import com.android.apps.tag.record.TextRecord; +import com.android.apps.tag.record.UriRecord; + +/** + * An {@link Activity} which handles a broadcast of a new tag that the device just discovered. + */ +public class TagViewer extends Activity { + static final String TAG = "SaveTag"; + static final String EXTRA_TAG_DB_ID = "db_id"; + static final String EXTRA_MESSAGE = "msg"; + + long mTagDatabaseId; + + @Override + protected void onStart() { + super.onStart(); + + getWindow().addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED + | WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD + | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON + | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON + | WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON + | WindowManager.LayoutParams.FLAG_DIM_BEHIND + ); + + Intent intent = getIntent(); + NdefMessage[] msgs = null; + NdefTag tag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG); + if (tag == null) { + // Maybe it came from the database? + mTagDatabaseId = intent.getLongExtra(EXTRA_TAG_DB_ID, -1); + NdefMessage msg = intent.getParcelableExtra(EXTRA_MESSAGE); + if (msg != null) { + msgs = new NdefMessage[] { msg }; + } + } else { + msgs = tag.getNdefMessages(); + // TODO use a service to avoid the process getting reaped during saving + new SaveTagTask().execute(msgs); + } + + if (msgs == null || msgs.length == 0) { + Log.e(TAG, "No NDEF messages"); + finish(); + return; + } + + + LayoutInflater inflater = LayoutInflater.from( + new ContextThemeWrapper(this, android.R.style.Theme_Light)); + LinearLayout list = (LinearLayout) inflater.inflate(R.layout.tag_viewer_list, null, false); + // TODO figure out why the background isn't white, the CTW should force that... + list.setBackgroundColor(Color.WHITE); + setContentView(list); + buildTagViews(list, inflater, msgs); + } + + private void buildTagViews(LinearLayout list, LayoutInflater inflater, NdefMessage[] msgs) { + // The body of the dialog should use the light theme + + // Build the views from the logical records in the messages + boolean first = true; + for (NdefMessage msg : msgs) { + Iterable objects = NdefUtil.getObjects(msg); + for (ParsedNdefRecord object : objects) { + if (!first) { + list.addView(inflater.inflate(R.layout.tag_divider, list, false)); + first = false; + } + + if (object instanceof TextRecord) { + TextRecord textRecord = (TextRecord) object; + TextView text = (TextView) inflater.inflate(R.layout.tag_text, list, false); + text.setText(textRecord.getText()); + list.addView(text); + } else if (object instanceof UriRecord) { + UriRecord uriRecord = (UriRecord) object; + TextView text = (TextView) inflater.inflate(R.layout.tag_text, list, false); + text.setText(uriRecord.getUri().toString()); + list.addView(text); + } else if (object instanceof SmartPoster) { + TextView text = (TextView) inflater.inflate(R.layout.tag_text, list, false); + SmartPoster poster = (SmartPoster) object; + TextRecord title = poster.getTitle(); + if (title != null) { + text.setText(title.getText()); + } + list.addView(text); + } + } + } + } + + final class SaveTagTask extends AsyncTask { + @Override + public Void doInBackground(NdefMessage... msgs) { + TagDBHelper helper = TagDBHelper.getInstance(TagViewer.this); + SQLiteDatabase db = helper.getWritableDatabase(); + db.beginTransaction(); + try { + for (NdefMessage msg : msgs) { + helper.insertNdefMessage(db, msg, false); + } + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + return null; + } + } +} diff --git a/apps/Tag/src/com/android/apps/tag/record/ParsedNdefRecord.java b/apps/Tag/src/com/android/apps/tag/record/ParsedNdefRecord.java new file mode 100644 index 000000000..8de4a3f23 --- /dev/null +++ b/apps/Tag/src/com/android/apps/tag/record/ParsedNdefRecord.java @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apps.tag.record; + +/** + * TODO: come up with a better name. + */ +public interface ParsedNdefRecord { + + // Just a placeholder for now. Probably not needed nor desired. + public String getRecordType(); +} diff --git a/apps/Tag/src/com/android/apps/tag/SmartPoster.java b/apps/Tag/src/com/android/apps/tag/record/SmartPoster.java similarity index 70% rename from apps/Tag/src/com/android/apps/tag/SmartPoster.java rename to apps/Tag/src/com/android/apps/tag/record/SmartPoster.java index 1e107235b..54ac42405 100644 --- a/apps/Tag/src/com/android/apps/tag/SmartPoster.java +++ b/apps/Tag/src/com/android/apps/tag/record/SmartPoster.java @@ -14,22 +14,24 @@ * limitations under the License. */ -package com.android.apps.tag; +package com.android.apps.tag.record; +import com.android.apps.tag.NdefUtil; import com.google.common.base.Preconditions; import com.google.common.collect.Iterables; + +import android.nfc.FormatException; import android.nfc.NdefMessage; import android.nfc.NdefRecord; -import android.nfc.FormatException; + +import java.util.Arrays; import javax.annotation.Nullable; -import java.net.URI; -import java.util.Arrays; /** * A representation of an NFC Forum "Smart Poster". */ -public class SmartPoster { +public class SmartPoster implements ParsedNdefRecord { /** * NFC Forum Smart Poster Record Type Definition section 3.2.1. @@ -39,7 +41,7 @@ public class SmartPoster { * This record is optional." */ - private final String titleRecord; + private final TextRecord mTitleRecord; /** * NFC Forum Smart Poster Record Type Definition section 3.2.1. @@ -48,32 +50,32 @@ public class SmartPoster { * records are just metadata about this record. There MUST be one URI * record and there MUST NOT be more than one." */ - private final URI uriRecord; + private final UriRecord mUriRecord; - private SmartPoster(URI uri, @Nullable String title) { - uriRecord = Preconditions.checkNotNull(uri); - titleRecord = title; + private SmartPoster(UriRecord uri, @Nullable TextRecord title) { + mUriRecord = Preconditions.checkNotNull(uri); + mTitleRecord = title; } - public URI getURI() { - return uriRecord; + public UriRecord getUriRecord() { + return mUriRecord; } /** - * Returns the title of the smartposter. This may be {@code null}. + * Returns the title of the smart poster. This may be {@code null}. */ - public String getTitle() { - return titleRecord; + public TextRecord getTitle() { + return mTitleRecord; } - public static SmartPoster from(NdefRecord record) { + public static SmartPoster parse(NdefRecord record) { Preconditions.checkArgument(record.getTnf() == NdefRecord.TNF_WELL_KNOWN); Preconditions.checkArgument(Arrays.equals(record.getType(), NdefRecord.RTD_SMART_POSTER)); try { NdefMessage subRecords = new NdefMessage(record.getPayload()); - URI uri = Iterables.getOnlyElement(NdefUtil.getURIs(subRecords)); - Iterable textFields = NdefUtil.getTextFields(subRecords); - String title = null; + UriRecord uri = Iterables.getOnlyElement(NdefUtil.getUris(subRecords)); + Iterable textFields = NdefUtil.getTextFields(subRecords); + TextRecord title = null; if (!Iterables.isEmpty(textFields)) { title = Iterables.get(textFields, 0); } @@ -86,10 +88,15 @@ public class SmartPoster { public static boolean isPoster(NdefRecord record) { try { - from(record); + parse(record); return true; } catch (IllegalArgumentException e) { return false; } } + + @Override + public String getRecordType() { + return "SmartPoster"; + } } diff --git a/apps/Tag/src/com/android/apps/tag/record/TextRecord.java b/apps/Tag/src/com/android/apps/tag/record/TextRecord.java new file mode 100644 index 000000000..29d380d85 --- /dev/null +++ b/apps/Tag/src/com/android/apps/tag/record/TextRecord.java @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apps.tag.record; + +import android.nfc.NdefRecord; +import com.google.common.base.Preconditions; + +import java.io.UnsupportedEncodingException; +import java.util.Arrays; + +/** + * An NFC Text Record + */ +public class TextRecord implements ParsedNdefRecord { + + private final String mLanguageCode; + private final String mText; + + private TextRecord(String languageCode, String text) { + mLanguageCode = Preconditions.checkNotNull(languageCode); + mText = Preconditions.checkNotNull(text); + } + + @Override + public String getRecordType() { + return "Text"; + } + + public String getText() { + return mText; + } + + public String getLanguageCode() { + return mLanguageCode; + } + + // TODO: deal with text fields which span multiple NdefRecords + public static TextRecord parse(NdefRecord record) { + Preconditions.checkArgument(record.getTnf() == NdefRecord.TNF_WELL_KNOWN); + Preconditions.checkArgument(Arrays.equals(record.getType(), NdefRecord.RTD_TEXT)); + try { + + byte[] payload = record.getPayload(); + + /* + * payload[0] contains the "Status Byte Encodings" field, per + * the NFC Forum "Text Record Type Definition" section 3.2.1. + * + * bit7 is the Text Encoding Field. + * + * if (Bit_7 == 0): The text is encoded in UTF-8 + * if (Bit_7 == 1): The text is encoded in UTF16 + * + * Bit_6 is reserved for future use and must be set to zero. + * + * Bits 5 to 0 are the length of the IANA language code. + */ + + String textEncoding = ((payload[0] & 0200) == 0) ? "UTF-8" : "UTF-16"; + int languageCodeLength = payload[0] & 0077; + + String languageCode = new String(payload, 1, languageCodeLength, "US-ASCII"); + String text = new String(payload, + languageCodeLength + 1, + payload.length - languageCodeLength - 1, + textEncoding); + return new TextRecord(languageCode, text); + + } catch (UnsupportedEncodingException e) { + // should never happen unless we get a malformed tag. + throw new IllegalArgumentException(e); + } + } + + public static boolean isText(NdefRecord record) { + try { + parse(record); + return true; + } catch (IllegalArgumentException e) { + return false; + } + } +} diff --git a/apps/Tag/src/com/android/apps/tag/record/UriRecord.java b/apps/Tag/src/com/android/apps/tag/record/UriRecord.java new file mode 100644 index 000000000..eeba30bd8 --- /dev/null +++ b/apps/Tag/src/com/android/apps/tag/record/UriRecord.java @@ -0,0 +1,135 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apps.tag.record; + +import android.net.Uri; +import android.nfc.NdefRecord; +import com.google.common.base.Preconditions; +import com.google.common.collect.BiMap; +import com.google.common.collect.ImmutableBiMap; +import com.google.common.primitives.Bytes; + +import java.nio.charset.Charsets; +import java.util.Arrays; + +/** + * A parsed record containing a Uri. + */ +public class UriRecord implements ParsedNdefRecord { + private static final byte[] EMPTY = new byte[0]; + + /** + * NFC Forum "URI Record Type Definition" + * + * This is a mapping of "URI Identifier Codes" to URI string prefixes, + * per section 3.2.2 of the NFC Forum URI Record Type Definition document. + */ + private static final BiMap URI_PREFIX_MAP = ImmutableBiMap.builder() + .put((byte) 0x00, "") + .put((byte) 0x01, "http://www.") + .put((byte) 0x02, "https://www.") + .put((byte) 0x03, "http://") + .put((byte) 0x04, "https://") + .put((byte) 0x05, "tel:") + .put((byte) 0x06, "mailto:") + .put((byte) 0x07, "ftp://anonymous:anonymous@") + .put((byte) 0x08, "ftp://ftp.") + .put((byte) 0x09, "ftps://") + .put((byte) 0x0A, "sftp://") + .put((byte) 0x0B, "smb://") + .put((byte) 0x0C, "nfs://") + .put((byte) 0x0D, "ftp://") + .put((byte) 0x0E, "dav://") + .put((byte) 0x0F, "news:") + .put((byte) 0x10, "telnet://") + .put((byte) 0x11, "imap:") + .put((byte) 0x12, "rtsp://") + .put((byte) 0x13, "urn:") + .put((byte) 0x14, "pop:") + .put((byte) 0x15, "sip:") + .put((byte) 0x16, "sips:") + .put((byte) 0x17, "tftp:") + .put((byte) 0x18, "btspp://") + .put((byte) 0x19, "btl2cap://") + .put((byte) 0x1A, "btgoep://") + .put((byte) 0x1B, "tcpobex://") + .put((byte) 0x1C, "irdaobex://") + .put((byte) 0x1D, "file://") + .put((byte) 0x1E, "urn:epc:id:") + .put((byte) 0x1F, "urn:epc:tag:") + .put((byte) 0x20, "urn:epc:pat:") + .put((byte) 0x21, "urn:epc:raw:") + .put((byte) 0x22, "urn:epc:") + .put((byte) 0x23, "urn:nfc:") + .build(); + + private final Uri mUri; + + private UriRecord(Uri uri) { + this.mUri = Preconditions.checkNotNull(uri); + } + + @Override + public String getRecordType() { + return "Uri"; + } + + public Uri getUri() { + return mUri; + } + + /** + * Convert {@link android.nfc.NdefRecord} into a {@link android.net.Uri}. + * + * TODO: This class does not handle NdefRecords where the TNF + * (Type Name Format) of the class is {@link android.nfc.NdefRecord#TNF_ABSOLUTE_URI}. + * This should be fixed. + * + * @throws IllegalArgumentException if the NdefRecord is not a + * record containing a URI. + */ + public static UriRecord parse(NdefRecord record) { + Preconditions.checkArgument(record.getTnf() == NdefRecord.TNF_WELL_KNOWN); + Preconditions.checkArgument(Arrays.equals(record.getType(), NdefRecord.RTD_URI)); + + byte[] payload = record.getPayload(); + + /* + * payload[0] contains the URI Identifier Code, per the + * NFC Forum "URI Record Type Definition" section 3.2.2. + * + * payload[1]...payload[payload.length - 1] contains the rest of + * the URI. + */ + + String prefix = URI_PREFIX_MAP.get(payload[0]); + byte[] fullUri = Bytes.concat( + prefix.getBytes(Charsets.UTF_8), + Arrays.copyOfRange(payload, 1, payload.length)); + + return new UriRecord(Uri.parse(new String(fullUri, Charsets.UTF_8))); + } + + public static boolean isUri(NdefRecord record) { + try { + parse(record); + return true; + } catch (IllegalArgumentException e) { + return false; + } + } +} diff --git a/apps/Tag/tests/src/com/android/apps/tag/NdefUtilTest.java b/apps/Tag/tests/src/com/android/apps/tag/NdefUtilTest.java index 12baf6f9c..24070ae12 100644 --- a/apps/Tag/tests/src/com/android/apps/tag/NdefUtilTest.java +++ b/apps/Tag/tests/src/com/android/apps/tag/NdefUtilTest.java @@ -17,6 +17,7 @@ package com.android.apps.tag; import android.test.AndroidTestCase; +import com.android.apps.tag.record.TextRecord; import com.google.common.primitives.Bytes; import android.nfc.NdefRecord; @@ -46,6 +47,6 @@ public class NdefUtilTest extends AndroidTestCase { ); NdefRecord record = new NdefRecord(NdefRecord.TNF_WELL_KNOWN, NdefRecord.RTD_TEXT, new byte[0], data); - assertEquals(word, NdefUtil.toText(record)); + assertEquals(word, TextRecord.parse(record).getText()); } } diff --git a/apps/Tag/tests/src/com/android/apps/tag/SmartPosterTest.java b/apps/Tag/tests/src/com/android/apps/tag/SmartPosterTest.java index b7eb86973..8225197ce 100644 --- a/apps/Tag/tests/src/com/android/apps/tag/SmartPosterTest.java +++ b/apps/Tag/tests/src/com/android/apps/tag/SmartPosterTest.java @@ -18,16 +18,17 @@ package com.android.apps.tag; import android.test.AndroidTestCase; import android.nfc.NdefMessage; +import com.android.apps.tag.record.SmartPoster; /** * Tests for {@link SmartPoster}. */ public class SmartPosterTest extends AndroidTestCase { public void testSmartPoster() throws Exception { - NdefMessage msg = new NdefMessage(TagDBHelper.REAL_NFC_MSG); + NdefMessage msg = new NdefMessage(MockNdefMessages.REAL_NFC_MSG); - SmartPoster poster = SmartPoster.from(msg.getRecords()[0]); - assertEquals("NFC Forum Type 4 Tag", poster.getTitle()); - assertEquals("http://www.nxp.com/nfc", poster.getURI().toString()); + SmartPoster poster = SmartPoster.parse(msg.getRecords()[0]); + assertEquals("NFC Forum Type 4 Tag", poster.getTitle().getText()); + assertEquals("http://www.nxp.com/nfc", poster.getUriRecord().getUri().toString()); } } diff --git a/build/tools/patch_windows_sdk.sh b/build/tools/patch_windows_sdk.sh index 3cc087527..687526172 100755 --- a/build/tools/patch_windows_sdk.sh +++ b/build/tools/patch_windows_sdk.sh @@ -55,7 +55,12 @@ cp $V ${TOPDIR}sdk/files/find_java.bat $LIB/ cp $V ${TOPDIR}sdk/apkbuilder/etc/apkbuilder.bat $TOOLS/ cp $V ${TOPDIR}sdk/ddms/app/etc/ddms.bat $TOOLS/ cp $V ${TOPDIR}sdk/traceview/etc/traceview.bat $TOOLS/ -cp $V ${TOPDIR}sdk/hierarchyviewer2/app/etc/hierarchyviewer.bat $TOOLS/ +if [ -f ${TOPDIR}sdk/hierarchyviewer2/app/etc/hierarchyviewer.bat ]; then + cp $V ${TOPDIR}sdk/hierarchyviewer2/app/etc/hierarchyviewer.bat $TOOLS/ +else + # That's ok because currently GB uses Tools_r7 but we'll ship Tools_r8 from master-open. + echo "WARNING: Ignoring ${TOPDIR}sdk/hierarchyviewer2/app/etc/hierarchyviewer.bat [ok for GB+Tools r8]" +fi cp $V ${TOPDIR}sdk/layoutopt/app/etc/layoutopt.bat $TOOLS/ cp $V ${TOPDIR}sdk/draw9patch/etc/draw9patch.bat $TOOLS/ cp $V ${TOPDIR}sdk/sdkmanager/app/etc/android.bat $TOOLS/