1 /* 2 * Copyright (C) 2010 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.example.android.nfc.record; 17 18 import android.app.Activity; 19 import android.nfc.FormatException; 20 import android.nfc.NdefMessage; 21 import android.nfc.NdefRecord; 22 import android.view.LayoutInflater; 23 import android.view.View; 24 import android.view.ViewGroup; 25 import android.view.ViewGroup.LayoutParams; 26 import android.widget.LinearLayout; 27 28 import com.google.common.base.Charsets; 29 import com.google.common.base.Preconditions; 30 import com.google.common.collect.ImmutableMap; 31 import com.google.common.collect.Iterables; 32 33 import com.example.android.nfc.NdefMessageParser; 34 import com.example.android.nfc.R; 35 36 import java.util.Arrays; 37 import java.util.NoSuchElementException; 38 39 /** 40 * A representation of an NFC Forum "Smart Poster". 41 */ 42 public class SmartPoster implements ParsedNdefRecord { 43 44 /** 45 * NFC Forum Smart Poster Record Type Definition section 3.2.1. 46 * 47 * "The Title record for the service (there can be many of these in 48 * different languages, but a language MUST NOT be repeated). This record is 49 * optional." 50 */ 51 private final TextRecord mTitleRecord; 52 53 /** 54 * NFC Forum Smart Poster Record Type Definition section 3.2.1. 55 * 56 * "The URI record. This is the core of the Smart Poster, and all other 57 * records are just metadata about this record. There MUST be one URI record 58 * and there MUST NOT be more than one." 59 */ 60 private final UriRecord mUriRecord; 61 62 /** 63 * NFC Forum Smart Poster Record Type Definition section 3.2.1. 64 * 65 * "The Action record. This record describes how the service should be 66 * treated. For example, the action may indicate that the device should save 67 * the URI as a bookmark or open a browser. The Action record is optional. 68 * If it does not exist, the device may decide what to do with the service. 69 * If the action record exists, it should be treated as a strong suggestion; 70 * the UI designer may ignore it, but doing so will induce a different user 71 * experience from device to device." 72 */ 73 private final RecommendedAction mAction; 74 75 /** 76 * NFC Forum Smart Poster Record Type Definition section 3.2.1. 77 * 78 * "The Type record. If the URI references an external entity (e.g., via a 79 * URL), the Type record may be used to declare the MIME type of the entity. 80 * This can be used to tell the mobile device what kind of an object it can 81 * expect before it opens the connection. The Type record is optional." 82 */ 83 private final String mType; 84 85 private SmartPoster(UriRecord uri, TextRecord title, RecommendedAction action, String type) { 86 mUriRecord = Preconditions.checkNotNull(uri); 87 mTitleRecord = title; 88 mAction = Preconditions.checkNotNull(action); 89 mType = type; 90 } 91 92 public UriRecord getUriRecord() { 93 return mUriRecord; 94 } 95 96 /** 97 * Returns the title of the smart poster. This may be {@code null}. 98 */ 99 public TextRecord getTitle() { 100 return mTitleRecord; 101 } 102 103 public static SmartPoster parse(NdefRecord record) { 104 Preconditions.checkArgument(record.getTnf() == NdefRecord.TNF_WELL_KNOWN); 105 Preconditions.checkArgument(Arrays.equals(record.getType(), NdefRecord.RTD_SMART_POSTER)); 106 try { 107 NdefMessage subRecords = new NdefMessage(record.getPayload()); 108 return parse(subRecords.getRecords()); 109 } catch (FormatException e) { 110 throw new IllegalArgumentException(e); 111 } 112 } 113 114 public static SmartPoster parse(NdefRecord[] recordsRaw) { 115 try { 116 Iterable<ParsedNdefRecord> records = NdefMessageParser.getRecords(recordsRaw); 117 UriRecord uri = Iterables.getOnlyElement(Iterables.filter(records, UriRecord.class)); 118 TextRecord title = getFirstIfExists(records, TextRecord.class); 119 RecommendedAction action = parseRecommendedAction(recordsRaw); 120 String type = parseType(recordsRaw); 121 return new SmartPoster(uri, title, action, type); 122 } catch (NoSuchElementException e) { 123 throw new IllegalArgumentException(e); 124 } 125 } 126 127 public static boolean isPoster(NdefRecord record) { 128 try { 129 parse(record); 130 return true; 131 } catch (IllegalArgumentException e) { 132 return false; 133 } 134 } 135 136 public View getView(Activity activity, LayoutInflater inflater, ViewGroup parent, int offset) { 137 if (mTitleRecord != null) { 138 // Build a container to hold the title and the URI 139 LinearLayout container = new LinearLayout(activity); 140 container.setOrientation(LinearLayout.VERTICAL); 141 container.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, 142 LayoutParams.WRAP_CONTENT)); 143 container.addView(mTitleRecord.getView(activity, inflater, container, offset)); 144 inflater.inflate(R.layout.tag_divider, container); 145 container.addView(mUriRecord.getView(activity, inflater, container, offset)); 146 return container; 147 } else { 148 // Just a URI, return a view for it directly 149 return mUriRecord.getView(activity, inflater, parent, offset); 150 } 151 } 152 153 /** 154 * Returns the first element of {@code elements} which is an instance of 155 * {@code type}, or {@code null} if no such element exists. 156 */ 157 private static <T> T getFirstIfExists(Iterable<?> elements, Class<T> type) { 158 Iterable<T> filtered = Iterables.filter(elements, type); 159 T instance = null; 160 if (!Iterables.isEmpty(filtered)) { 161 instance = Iterables.get(filtered, 0); 162 } 163 return instance; 164 } 165 166 private enum RecommendedAction { 167 UNKNOWN((byte) -1), DO_ACTION((byte) 0), SAVE_FOR_LATER((byte) 1), OPEN_FOR_EDITING( 168 (byte) 2); 169 170 private static final ImmutableMap<Byte, RecommendedAction> LOOKUP; 171 static { 172 ImmutableMap.Builder<Byte, RecommendedAction> builder = ImmutableMap.builder(); 173 for (RecommendedAction action : RecommendedAction.values()) { 174 builder.put(action.getByte(), action); 175 } 176 LOOKUP = builder.build(); 177 } 178 179 private final byte mAction; 180 181 private RecommendedAction(byte val) { 182 this.mAction = val; 183 } 184 185 private byte getByte() { 186 return mAction; 187 } 188 } 189 190 private static NdefRecord getByType(byte[] type, NdefRecord[] records) { 191 for (NdefRecord record : records) { 192 if (Arrays.equals(type, record.getType())) { 193 return record; 194 } 195 } 196 return null; 197 } 198 199 private static final byte[] ACTION_RECORD_TYPE = new byte[] {'a', 'c', 't'}; 200 201 private static RecommendedAction parseRecommendedAction(NdefRecord[] records) { 202 NdefRecord record = getByType(ACTION_RECORD_TYPE, records); 203 if (record == null) { 204 return RecommendedAction.UNKNOWN; 205 } 206 byte action = record.getPayload()[0]; 207 if (RecommendedAction.LOOKUP.containsKey(action)) { 208 return RecommendedAction.LOOKUP.get(action); 209 } 210 return RecommendedAction.UNKNOWN; 211 } 212 213 private static final byte[] TYPE_TYPE = new byte[] {'t'}; 214 215 private static String parseType(NdefRecord[] records) { 216 NdefRecord type = getByType(TYPE_TYPE, records); 217 if (type == null) { 218 return null; 219 } 220 return new String(type.getPayload(), Charsets.UTF_8); 221 } 222 } 223