Merge branch 'gingerbread' into gingerbread-release

This commit is contained in:
The Android Automerger
2010-10-15 09:06:00 -07:00
28 changed files with 993 additions and 535 deletions

View File

@@ -20,26 +20,33 @@
own application, the package name must be changed from "com.example.*" own application, the package name must be changed from "com.example.*"
to come from a domain that you own or have control over. --> to come from a domain that you own or have control over. -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.apps.tag"> package="com.android.apps.tag"
>
<uses-permission android:name="com.trustedlogic.trustednfc.permission.NFC_NOTIFY" />
<uses-permission android:name="com.trustedlogic.trustednfc.permission.NFC_RAW" />
<application android:label="Tags"> <application android:label="Tags">
<activity android:name="TagBrowserActivity" <activity android:name="TagBrowserActivity"
android:icon="@drawable/ic_launcher_nfc" android:icon="@drawable/ic_launcher_nfc"
android:theme="@style/Tags.TagBrowserTheme" > android:theme="@android:style/Theme.NoTitleBar"
>
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN"/> <action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/> <category android:name="android.intent.category.LAUNCHER"/>
</intent-filter> </intent-filter>
</activity> </activity>
<activity android:name="TagList"></activity> <activity android:name="TagList" />
<activity android:name="SaveTag"></activity>
<receiver android:name=".TagBroadcastReceiver"> <activity android:name="TagViewer"
android:theme="@android:style/Theme.Dialog"
>
<intent-filter> <intent-filter>
<action android:name= "com.trustedlogic.trustednfc.android.action.NDEF_TAG_DISCOVERED"/> <action android:name="android.nfc.action.NDEF_TAG_DISCOVERED"/>
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter> </intent-filter>
</receiver> </activity>
</application> </application>
<uses-permission android:name="com.trustedlogic.trustednfc.permission.NFC_NOTIFY"></uses-permission>
<uses-permission android:name="com.trustedlogic.trustednfc.permission.NFC_RAW"></uses-permission>
</manifest> </manifest>

View File

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -14,10 +14,9 @@
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
--> -->
<resources>
<style name="Tags.TagBrowserTheme" parent="@android:style/Theme.NoTitleBar"> <View xmlns:android="http://schemas.android.com/apk/res/android"
</style> android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:attr/listDivider"
</resources> />

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/text"
android:padding="4dip"
android:textAppearance="?android:attr/textAppearanceMedium"
android:layout_width="match_parent"
android:layout_height="?android:attr/listPreferredItemHeight"
android:singleLine="true"
android:gravity="center_vertical"
/>

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
/>

View File

@@ -20,4 +20,10 @@
<string name="help_and_info">help and info</string> <string name="help_and_info">help and info</string>
<string name="saved">Saved</string> <string name="saved">Saved</string>
<!-- The title of the tab that displays all recently scanned NFC tags -->
<string name="tab_recent">Recent</string>
<!-- The title of the tab that displays all saved NFC tags -->
<string name="tab_saved">Saved</string>
</resources> </resources>

View File

@@ -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
};
}

View File

@@ -16,21 +16,19 @@
package com.android.apps.tag; package com.android.apps.tag;
import android.util.Log; import com.android.apps.tag.record.ParsedNdefRecord;
import com.google.common.base.Preconditions; import com.android.apps.tag.record.SmartPoster;
import com.google.common.collect.BiMap; import com.android.apps.tag.record.TextRecord;
import com.google.common.collect.ImmutableBiMap; import com.android.apps.tag.record.UriRecord;
import com.google.common.collect.Iterables; import com.google.common.collect.Iterables;
import com.google.common.primitives.Bytes; import com.google.common.primitives.Bytes;
import android.net.Uri;
import android.nfc.NdefMessage; import android.nfc.NdefMessage;
import android.nfc.NdefRecord; import android.nfc.NdefRecord;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.Charsets; import java.nio.charset.Charsets;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.List; import java.util.List;
/** /**
@@ -42,55 +40,9 @@ public class NdefUtil {
private static final byte[] EMPTY = new byte[0]; private static final byte[] EMPTY = new byte[0];
/** /**
* NFC Forum "URI Record Type Definition" * Create a new {@link NdefRecord} containing the supplied {@link Uri}.
*
* 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 public static NdefRecord toUriRecord(Uri uri) {
BiMap<Byte, String> URI_PREFIX_MAP = ImmutableBiMap.<Byte, String>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) {
byte[] uriBytes = uri.toString().getBytes(Charsets.UTF_8); byte[] uriBytes = uri.toString().getBytes(Charsets.UTF_8);
/* /*
@@ -109,131 +61,33 @@ public class NdefUtil {
NdefRecord.RTD_URI, EMPTY, payload); NdefRecord.RTD_URI, EMPTY, payload);
} }
/** public static Iterable<TextRecord> getTextFields(NdefMessage message) {
* Convert {@link NdefRecord} into a {@link URI}. return Iterables.filter(getObjects(message), TextRecord.class);
*
* 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 boolean isURI(NdefRecord record) { public static Iterable<UriRecord> getUris(NdefMessage message) {
try { return Iterables.filter(getObjects(message), UriRecord.class);
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<String> getTextFields(NdefMessage message) {
return Iterables.filter(getObjects(message), String.class);
}
public static Iterable<URI> getURIs(NdefMessage message) {
return Iterables.filter(getObjects(message), URI.class);
} }
/** /**
* Parse the provided {@code NdefMessage}, extracting all known * Parse the provided {@code NdefMessage}, extracting all known
* objects from the message. Typically this list will consist of * 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. * corresponding to NDEF URI records.
* <p> * <p>
* TODO: Is this API too generic? Should we keep it? * TODO: Is this API too generic? Should we keep it?
*/ */
private static Iterable<Object> getObjects(NdefMessage message) { public static Iterable<ParsedNdefRecord> getObjects(NdefMessage message) {
try { List<ParsedNdefRecord> retval = new ArrayList<ParsedNdefRecord>();
List<Object> retval = new ArrayList<Object>();
for (NdefRecord record : message.getRecords()) { for (NdefRecord record : message.getRecords()) {
if (isURI(record)) { if (UriRecord.isUri(record)) {
retval.add(toURI(record)); retval.add(UriRecord.parse(record));
} else if (isText(record)) { } else if (TextRecord.isText(record)) {
retval.add(toText(record)); retval.add(TextRecord.parse(record));
} else if (SmartPoster.isPoster(record)) { } else if (SmartPoster.isPoster(record)) {
retval.add(SmartPoster.from(record)); retval.add(SmartPoster.parse(record));
} }
} }
return retval; return retval;
} catch (URISyntaxException e) {
throw new IllegalArgumentException(e);
}
} }
} }

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}
}

View File

@@ -16,6 +16,7 @@
package com.android.apps.tag; package com.android.apps.tag;
import android.app.Activity;
import android.app.TabActivity; import android.app.TabActivity;
import android.content.Intent; import android.content.Intent;
import android.content.res.Resources; import android.content.res.Resources;
@@ -23,34 +24,27 @@ import android.os.Bundle;
import android.widget.TabHost; 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 { public class TagBrowserActivity extends TabActivity {
@Override @Override
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState) {
super.onCreate(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); setContentView(R.layout.main);
Resources res = getResources(); Resources res = getResources();
TabHost tabHost = getTabHost(); TabHost tabHost = getTabHost();
Intent i = new Intent().setClass(this, TagList.class);
Intent iSavedList = new Intent().setClass(this, TagList.class) TabHost.TabSpec spec1 = tabHost.newTabSpec("saved")
.putExtra(TagList.SHOW_SAVED_ONLY, true); .setIndicator(getText(R.string.tab_saved), res.getDrawable(R.drawable.ic_menu_tag))
Intent iRecentList = new Intent().setClass(this, TagList.class); .setContent(new Intent().setClass(this, TagList.class)
.putExtra(TagList.EXTRA_SHOW_SAVED_ONLY, true));
TabHost.TabSpec spec1 = tabHost.newTabSpec("1")
.setIndicator("Saved", res.getDrawable(R.drawable.ic_menu_tag))
.setContent(iSavedList);
tabHost.addTab(spec1); tabHost.addTab(spec1);
TabHost.TabSpec spec2 = tabHost.newTabSpec("2") TabHost.TabSpec spec2 = tabHost.newTabSpec("recent")
.setIndicator("Recent", res.getDrawable(R.drawable.ic_menu_desk_clock)) .setIndicator(getText(R.string.tab_recent), res.getDrawable(R.drawable.ic_menu_desk_clock))
.setContent(iRecentList); .setContent(new Intent().setClass(this, TagList.class));
tabHost.addTab(spec2); tabHost.addTab(spec2);
} }
} }

View File

@@ -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);
}
}

View File

@@ -22,146 +22,110 @@ import android.content.Context;
import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper; import android.database.sqlite.SQLiteOpenHelper;
import android.database.sqlite.SQLiteStatement; import android.database.sqlite.SQLiteStatement;
import android.net.Uri;
import android.nfc.FormatException; import android.nfc.FormatException;
import android.nfc.NdefMessage; import android.nfc.NdefMessage;
import android.nfc.NdefRecord; import android.nfc.NdefRecord;
import java.net.URI;
import java.util.Date;
/** /**
* Database utilities for the saved tags. * Database utilities for the saved tags.
*/ */
public class TagDBHelper extends SQLiteOpenHelper { 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 (" public interface NdefMessagesTable {
+ "_id INTEGER NOT NULL, " public static final String TABLE_NAME = "nedf_msg";
+ "bytes BLOB NOT NULL, "
+ "date INTEGER NOT NULL, "
+ "saved TEXT NOT NULL default 0," // boolean
+ "PRIMARY KEY(_id)"
+ ")";
private static final String INSERT = public static final String _ID = "_id";
"INSERT INTO NdefMessage (bytes, date, saved) values (?, ?, ?)"; public static final String TITLE = "title";
public static final String BYTES = "bytes";
public static final String DATE = "date";
public static final String SAVED = "saved";
}
/** private static TagDBHelper sInstance;
* 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 public static synchronized TagDBHelper getInstance(Context context) {
// begin smart poster record #1 if (sInstance == null) {
(byte) 0x91, // MB=1 ME=0 CF=0 SR=1 IL=0 TNF=001 sInstance = new TagDBHelper(context.getApplicationContext());
(byte) 0x01, // Type Length = 1 }
(byte) 0x17, // Payload Length = 23 return sInstance;
(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 private TagDBHelper(Context context) {
(byte) 0x4e, // 'N' this(context, DATABASE_NAME);
(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");
} }
@VisibleForTesting @VisibleForTesting
public TagDBHelper(Context context, String dbFile) { TagDBHelper(Context context, String dbFile) {
super(context, dbFile, null, DATABASE_VERSION); super(context, dbFile, null, DATABASE_VERSION);
} }
@Override @Override
public void onCreate(SQLiteDatabase db) { 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 // A fake message containing 1 URL
NdefMessage msg1 = new NdefMessage(new NdefRecord[] { 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 // A fake message containing 2 URLs
NdefMessage msg2 = new NdefMessage(new NdefRecord[] { NdefMessage msg2 = new NdefMessage(new NdefRecord[] {
NdefUtil.toUriRecord(URI.create("http://www.youtube.com")), NdefUtil.toUriRecord(Uri.parse("http://www.youtube.com")),
NdefUtil.toUriRecord(URI.create("http://www.android.com")) NdefUtil.toUriRecord(Uri.parse("http://www.android.com"))
}); });
insert(db, msg1, false); insertNdefMessage(db, msg1, false);
insert(db, msg2, true); insertNdefMessage(db, msg2, true);
try { try {
// A real message obtained from an NFC Forum Type 4 tag. // insert some real messages we found in the field.
NdefMessage msg3 = new NdefMessage(REAL_NFC_MSG); for (byte[] msg : MockNdefMessages.ALL_MOCK_MESSAGES) {
insert(db, msg3, false); NdefMessage msg3 = new NdefMessage(msg);
insertNdefMessage(db, msg3, false);
}
} catch (FormatException e) { } catch (FormatException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
} }
private void insert(SQLiteDatabase db, NdefMessage msg, boolean isSaved) { public void insertNdefMessage(SQLiteDatabase db, NdefMessage msg, boolean isSaved) {
SQLiteStatement stmt = db.compileStatement(INSERT); SQLiteStatement stmt = null;
stmt.bindString(1, new String(msg.toByteArray())); // TODO: This should be a blob 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(2, System.currentTimeMillis());
String isSavedStr = isSaved ? "1" : "0"; stmt.bindLong(3, isSaved ? 1 : 0);
stmt.bindString(3, isSavedStr);
stmt.executeInsert(); stmt.executeInsert();
stmt.close(); } finally {
if (stmt != null) stmt.close();
} }
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
} }
} }

View File

@@ -16,42 +16,47 @@
package com.android.apps.tag; package com.android.apps.tag;
import com.android.apps.tag.TagDBHelper.NdefMessagesTable;
import android.app.Activity;
import android.app.AlertDialog; import android.app.AlertDialog;
import android.app.Dialog; import android.app.Dialog;
import android.app.ListActivity; import android.app.ListActivity;
import android.content.DialogInterface; import android.content.DialogInterface;
import android.content.Intent;
import android.database.Cursor; import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteDatabase;
import android.nfc.FormatException;
import android.nfc.NdefMessage;
import android.os.AsyncTask;
import android.os.Bundle; import android.os.Bundle;
import android.util.Log;
import android.view.Menu; import android.view.Menu;
import android.view.View; import android.view.View;
import android.widget.ListView; 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 { public class TagList extends ListActivity implements DialogInterface.OnClickListener {
private SQLiteDatabase db; static final String TAG = "TagList";
private Cursor cursor;
static final String SHOW_SAVED_ONLY = "show_saved_only"; static final String EXTRA_SHOW_SAVED_ONLY = "show_saved_only";
SQLiteDatabase mDatabase;
TagAdapter mAdapter;
@Override @Override
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
boolean showSavedOnly = getIntent().getBooleanExtra(SHOW_SAVED_ONLY, false); boolean showSavedOnly = getIntent().getBooleanExtra(EXTRA_SHOW_SAVED_ONLY, false);
db = new TagDBHelper(getBaseContext()).getReadableDatabase(); mDatabase = TagDBHelper.getInstance(this).getReadableDatabase();
String selection = showSavedOnly ? "saved=1" : null; String selection = showSavedOnly ? NdefMessagesTable.SAVED + "=1" : null;
// TODO: Use an AsyncQueryHandler so that DB queries are not done on UI thread. new TagLoaderTask().execute(selection);
cursor = db.query( mAdapter = new TagAdapter(this);
"NdefMessage", setListAdapter(mAdapter);
new String[] { "_id", "bytes", "date" },
selection,
null, null, null, null);
setListAdapter(new TagCursorAdapter(this, cursor));
registerForContextMenu(getListView()); registerForContextMenu(getListView());
} }
@@ -75,22 +80,53 @@ public class TagList extends ListActivity implements DialogInterface.OnClickList
@Override @Override
protected void onDestroy() { protected void onDestroy() {
if (cursor != null) { if (mAdapter != null) {
cursor.close(); mAdapter.changeCursor(null);
}
if (db != null) {
db.close();
} }
super.onDestroy(); super.onDestroy();
} }
@Override @Override
protected void onListItemClick(ListView l, View v, int position, long id) { protected void onListItemClick(ListView l, View v, int position, long id) {
showDialog(1); Cursor cursor = mAdapter.getCursor();
super.onListItemClick(l, v, position, id); 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 @Override
public void onClick(DialogInterface dialog, int which) { public void onClick(DialogInterface dialog, int which) {
} }
final class TagLoaderTask extends AsyncTask<String, Void, Cursor> {
@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);
}
}
} }

View File

@@ -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<ParsedNdefRecord> 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<NdefMessage, Void, Void> {
@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;
}
}
}

View File

@@ -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();
}

View File

@@ -14,22 +14,24 @@
* limitations under the License. * 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.base.Preconditions;
import com.google.common.collect.Iterables; import com.google.common.collect.Iterables;
import android.nfc.FormatException;
import android.nfc.NdefMessage; import android.nfc.NdefMessage;
import android.nfc.NdefRecord; import android.nfc.NdefRecord;
import android.nfc.FormatException;
import java.util.Arrays;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import java.net.URI;
import java.util.Arrays;
/** /**
* A representation of an NFC Forum "Smart Poster". * 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. * NFC Forum Smart Poster Record Type Definition section 3.2.1.
@@ -39,7 +41,7 @@ public class SmartPoster {
* This record is optional." * This record is optional."
*/ */
private final String titleRecord; private final TextRecord mTitleRecord;
/** /**
* NFC Forum Smart Poster Record Type Definition section 3.2.1. * 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 * records are just metadata about this record. There MUST be one URI
* record and there MUST NOT be more than one." * record and there MUST NOT be more than one."
*/ */
private final URI uriRecord; private final UriRecord mUriRecord;
private SmartPoster(URI uri, @Nullable String title) { private SmartPoster(UriRecord uri, @Nullable TextRecord title) {
uriRecord = Preconditions.checkNotNull(uri); mUriRecord = Preconditions.checkNotNull(uri);
titleRecord = title; mTitleRecord = title;
} }
public URI getURI() { public UriRecord getUriRecord() {
return uriRecord; 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() { public TextRecord getTitle() {
return titleRecord; return mTitleRecord;
} }
public static SmartPoster from(NdefRecord record) { public static SmartPoster parse(NdefRecord record) {
Preconditions.checkArgument(record.getTnf() == NdefRecord.TNF_WELL_KNOWN); Preconditions.checkArgument(record.getTnf() == NdefRecord.TNF_WELL_KNOWN);
Preconditions.checkArgument(Arrays.equals(record.getType(), NdefRecord.RTD_SMART_POSTER)); Preconditions.checkArgument(Arrays.equals(record.getType(), NdefRecord.RTD_SMART_POSTER));
try { try {
NdefMessage subRecords = new NdefMessage(record.getPayload()); NdefMessage subRecords = new NdefMessage(record.getPayload());
URI uri = Iterables.getOnlyElement(NdefUtil.getURIs(subRecords)); UriRecord uri = Iterables.getOnlyElement(NdefUtil.getUris(subRecords));
Iterable<String> textFields = NdefUtil.getTextFields(subRecords); Iterable<TextRecord> textFields = NdefUtil.getTextFields(subRecords);
String title = null; TextRecord title = null;
if (!Iterables.isEmpty(textFields)) { if (!Iterables.isEmpty(textFields)) {
title = Iterables.get(textFields, 0); title = Iterables.get(textFields, 0);
} }
@@ -86,10 +88,15 @@ public class SmartPoster {
public static boolean isPoster(NdefRecord record) { public static boolean isPoster(NdefRecord record) {
try { try {
from(record); parse(record);
return true; return true;
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
return false; return false;
} }
} }
@Override
public String getRecordType() {
return "SmartPoster";
}
} }

View File

@@ -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;
}
}
}

View File

@@ -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<Byte, String> URI_PREFIX_MAP = ImmutableBiMap.<Byte, String>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;
}
}
}

View File

@@ -17,6 +17,7 @@
package com.android.apps.tag; package com.android.apps.tag;
import android.test.AndroidTestCase; import android.test.AndroidTestCase;
import com.android.apps.tag.record.TextRecord;
import com.google.common.primitives.Bytes; import com.google.common.primitives.Bytes;
import android.nfc.NdefRecord; 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); 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());
} }
} }

View File

@@ -18,16 +18,17 @@ package com.android.apps.tag;
import android.test.AndroidTestCase; import android.test.AndroidTestCase;
import android.nfc.NdefMessage; import android.nfc.NdefMessage;
import com.android.apps.tag.record.SmartPoster;
/** /**
* Tests for {@link SmartPoster}. * Tests for {@link SmartPoster}.
*/ */
public class SmartPosterTest extends AndroidTestCase { public class SmartPosterTest extends AndroidTestCase {
public void testSmartPoster() throws Exception { 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]); SmartPoster poster = SmartPoster.parse(msg.getRecords()[0]);
assertEquals("NFC Forum Type 4 Tag", poster.getTitle()); assertEquals("NFC Forum Type 4 Tag", poster.getTitle().getText());
assertEquals("http://www.nxp.com/nfc", poster.getURI().toString()); assertEquals("http://www.nxp.com/nfc", poster.getUriRecord().getUri().toString());
} }
} }

View File

@@ -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/apkbuilder/etc/apkbuilder.bat $TOOLS/
cp $V ${TOPDIR}sdk/ddms/app/etc/ddms.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/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/layoutopt/app/etc/layoutopt.bat $TOOLS/
cp $V ${TOPDIR}sdk/draw9patch/etc/draw9patch.bat $TOOLS/ cp $V ${TOPDIR}sdk/draw9patch/etc/draw9patch.bat $TOOLS/
cp $V ${TOPDIR}sdk/sdkmanager/app/etc/android.bat $TOOLS/ cp $V ${TOPDIR}sdk/sdkmanager/app/etc/android.bat $TOOLS/