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 17 package com.android.apps.tag.record; 18 19 import com.android.apps.tag.R; 20 import com.android.apps.tag.message.NdefMessageParser; 21 import com.google.common.base.Charsets; 22 import com.google.common.base.Preconditions; 23 import com.google.common.collect.ImmutableMap; 24 import com.google.common.collect.Iterables; 25 26 import android.app.Activity; 27 import android.content.Context; 28 import android.nfc.FormatException; 29 import android.nfc.NdefMessage; 30 import android.nfc.NdefRecord; 31 import android.view.LayoutInflater; 32 import android.view.View; 33 import android.view.ViewGroup; 34 import android.view.ViewGroup.LayoutParams; 35 import android.widget.LinearLayout; 36 37 import java.util.Arrays; 38 import java.util.Locale; 39 import java.util.NoSuchElementException; 40 41 import javax.annotation.Nullable; 42 43 /** 44 * A representation of an NFC Forum "Smart Poster". 45 */ 46 public class SmartPoster extends ParsedNdefRecord { 47 48 /** 49 * NFC Forum Smart Poster Record Type Definition section 3.2.1. 50 * 51 * "The Title record for the service (there can be many of these in 52 * different languages, but a language MUST NOT be repeated). 53 * This record is optional." 54 55 */ 56 private final TextRecord mTitleRecord; 57 58 /** 59 * NFC Forum Smart Poster Record Type Definition section 3.2.1. 60 * 61 * "The URI record. This is the core of the Smart Poster, and all other 62 * records are just metadata about this record. There MUST be one URI 63 * record and there MUST NOT be more than one." 64 */ 65 private final UriRecord mUriRecord; 66 67 /** 68 * NFC Forum Smart Poster Record Type Definition section 3.2.1. 69 * 70 * "The Icon record. A Smart Poster may include an icon by including one 71 * or many MIME-typed image records within the Smart Poster. If the 72 * device supports images, it SHOULD select and display one of these, 73 * depending on the device capabilities. The device SHOULD display only 74 * one. The Icon record is optional." 75 */ 76 private final ImageRecord mImageRecord; 77 78 /** 79 * NFC Forum Smart Poster Record Type Definition section 3.2.1. 80 * 81 * "The Action record. This record describes how the service should be 82 * treated. For example, the action may indicate that the device should 83 * save the URI as a bookmark or open a browser. The Action record is 84 * optional. If it does not exist, the device may decide what to do with 85 * the service. If the action record exists, it should be treated as 86 * a strong suggestion; the UI designer may ignore it, but doing so 87 * will induce a different user experience from device to device." 88 */ 89 private final RecommendedAction mAction; 90 91 /** 92 * NFC Forum Smart Poster Record Type Definition section 3.2.1. 93 * 94 * "The Type record. If the URI references an external entity (e.g., via 95 * a URL), the Type record may be used to declare the MIME type of the 96 * entity. This can be used to tell the mobile device what kind of an 97 * object it can expect before it opens the connection. The Type record 98 * is optional." 99 */ 100 private final String mType; 101 102 SmartPoster(UriRecord uri, @Nullable TextRecord title, @Nullable ImageRecord image, RecommendedAction action, @Nullable String type)103 private SmartPoster(UriRecord uri, @Nullable TextRecord title, 104 @Nullable ImageRecord image, RecommendedAction action, 105 @Nullable String type) { 106 mUriRecord = Preconditions.checkNotNull(uri); 107 mTitleRecord = title; 108 mImageRecord = image; 109 mAction = Preconditions.checkNotNull(action); 110 mType = type; 111 } 112 getUriRecord()113 public UriRecord getUriRecord() { 114 return mUriRecord; 115 } 116 117 /** 118 * Returns the title of the smart poster. This may be {@code null}. 119 */ getTitle()120 public TextRecord getTitle() { 121 return mTitleRecord; 122 } 123 parse(NdefRecord record)124 public static SmartPoster parse(NdefRecord record) { 125 Preconditions.checkArgument(record.getTnf() == NdefRecord.TNF_WELL_KNOWN); 126 Preconditions.checkArgument(Arrays.equals(record.getType(), NdefRecord.RTD_SMART_POSTER)); 127 try { 128 NdefMessage subRecords = new NdefMessage(record.getPayload()); 129 return parse(subRecords.getRecords()); 130 } catch (FormatException e) { 131 throw new IllegalArgumentException(e); 132 } 133 } 134 parse(NdefRecord[] recordsRaw)135 public static SmartPoster parse(NdefRecord[] recordsRaw) { 136 try { 137 Iterable<ParsedNdefRecord> records = NdefMessageParser.getRecords(recordsRaw); 138 UriRecord uri = Iterables.getOnlyElement(Iterables.filter(records, UriRecord.class)); 139 TextRecord title = getFirstIfExists(records, TextRecord.class); 140 ImageRecord image = getFirstIfExists(records, ImageRecord.class); 141 RecommendedAction action = parseRecommendedAction(recordsRaw); 142 String type = parseType(recordsRaw); 143 144 return new SmartPoster(uri, title, image, action, type); 145 } catch (NoSuchElementException e) { 146 throw new IllegalArgumentException(e); 147 } 148 } 149 isPoster(NdefRecord record)150 public static boolean isPoster(NdefRecord record) { 151 try { 152 parse(record); 153 return true; 154 } catch (IllegalArgumentException e) { 155 return false; 156 } 157 } 158 159 @Override getView(Activity activity, LayoutInflater inflater, ViewGroup parent, int offset)160 public View getView(Activity activity, LayoutInflater inflater, ViewGroup parent, int offset) { 161 if (mTitleRecord != null) { 162 // Build a container to hold the title and the URI 163 LinearLayout container = new LinearLayout(activity); 164 container.setOrientation(LinearLayout.VERTICAL); 165 container.setLayoutParams(new LayoutParams( 166 LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); 167 168 container.addView(mTitleRecord.getView(activity, inflater, container, offset)); 169 inflater.inflate(R.layout.tag_divider, container); 170 container.addView(mUriRecord.getView(activity, inflater, container, offset)); 171 return container; 172 } else { 173 // Just a URI, return a view for it directly 174 return mUriRecord.getView(activity, inflater, parent, offset); 175 } 176 } 177 178 @Override getSnippet(Context context, Locale locale)179 public String getSnippet(Context context, Locale locale) { 180 if (mTitleRecord != null) { 181 return mTitleRecord.getText(); 182 } 183 184 return mUriRecord.getPrettyUriString(context); 185 } 186 187 188 /** 189 * Returns the first element of {@code elements} which is an instance 190 * of {@code type}, or {@code null} if no such element exists. 191 */ getFirstIfExists(Iterable<?> elements, Class<T> type)192 private static <T> T getFirstIfExists(Iterable<?> elements, Class<T> type) { 193 Iterable<T> filtered = Iterables.filter(elements, type); 194 T instance = null; 195 if (!Iterables.isEmpty(filtered)) { 196 instance = Iterables.get(filtered, 0); 197 } 198 return instance; 199 } 200 201 private enum RecommendedAction { 202 UNKNOWN((byte) -1), DO_ACTION((byte) 0), 203 SAVE_FOR_LATER((byte) 1), OPEN_FOR_EDITING((byte) 2); 204 205 private static final ImmutableMap<Byte, RecommendedAction> LOOKUP; 206 static { 207 ImmutableMap.Builder<Byte, RecommendedAction> builder = ImmutableMap.builder(); 208 for (RecommendedAction action : RecommendedAction.values()) { action.getByte()209 builder.put(action.getByte(), action); 210 } 211 LOOKUP = builder.build(); 212 } 213 214 private final byte mAction; 215 RecommendedAction(byte val)216 private RecommendedAction(byte val) { 217 this.mAction = val; 218 } getByte()219 private byte getByte() { 220 return mAction; 221 } 222 } 223 getByType(byte[] type, NdefRecord[] records)224 private static NdefRecord getByType(byte[] type, NdefRecord[] records) { 225 for (NdefRecord record : records) { 226 if (Arrays.equals(type, record.getType())) { 227 return record; 228 } 229 } 230 return null; 231 } 232 233 private static final byte[] ACTION_RECORD_TYPE = new byte[] { 'a', 'c', 't' }; 234 parseRecommendedAction(NdefRecord[] records)235 private static RecommendedAction parseRecommendedAction(NdefRecord[] records) { 236 NdefRecord record = getByType(ACTION_RECORD_TYPE, records); 237 if (record == null) { 238 return RecommendedAction.UNKNOWN; 239 } 240 byte action = record.getPayload()[0]; 241 if (RecommendedAction.LOOKUP.containsKey(action)) { 242 return RecommendedAction.LOOKUP.get(action); 243 } 244 return RecommendedAction.UNKNOWN; 245 } 246 247 private static final byte[] TYPE_TYPE = new byte[] { 't' }; 248 parseType(NdefRecord[] records)249 private static String parseType(NdefRecord[] records) { 250 NdefRecord type = getByType(TYPE_TYPE, records); 251 if (type == null) { 252 return null; 253 } 254 return new String(type.getPayload(), Charsets.UTF_8); 255 } 256 } 257