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
17package com.android.apps.tag.record;
18
19import com.android.apps.tag.R;
20import com.google.common.annotations.VisibleForTesting;
21import com.google.common.base.Preconditions;
22import com.google.common.collect.BiMap;
23import com.google.common.collect.ImmutableBiMap;
24import com.google.common.primitives.Bytes;
25
26import android.app.Activity;
27import android.content.ActivityNotFoundException;
28import android.content.Context;
29import android.content.Intent;
30import android.content.pm.PackageManager;
31import android.content.pm.ResolveInfo;
32import android.net.Uri;
33import android.nfc.NdefRecord;
34import android.os.Parcel;
35import android.os.Parcelable;
36import android.telephony.PhoneNumberUtils;
37import android.text.TextUtils;
38import android.util.Log;
39import android.view.LayoutInflater;
40import android.view.View;
41import android.view.View.OnClickListener;
42import android.view.ViewGroup;
43import android.widget.ImageView;
44import android.widget.TextView;
45
46import java.nio.charset.Charset;
47import java.util.Arrays;
48import java.util.List;
49import java.util.Locale;
50
51/**
52 * A parsed record containing a Uri.
53 */
54public class UriRecord extends ParsedNdefRecord implements OnClickListener {
55    private static final String TAG = "UriRecord";
56
57    public static final String RECORD_TYPE = "UriRecord";
58
59    /**
60     * NFC Forum "URI Record Type Definition"
61     *
62     * This is a mapping of "URI Identifier Codes" to URI string prefixes,
63     * per section 3.2.2 of the NFC Forum URI Record Type Definition document.
64     */
65    private static final BiMap<Byte, String> URI_PREFIX_MAP = ImmutableBiMap.<Byte, String>builder()
66            .put((byte) 0x00, "")
67            .put((byte) 0x01, "http://www.")
68            .put((byte) 0x02, "https://www.")
69            .put((byte) 0x03, "http://")
70            .put((byte) 0x04, "https://")
71            .put((byte) 0x05, "tel:")
72            .put((byte) 0x06, "mailto:")
73            .put((byte) 0x07, "ftp://anonymous:anonymous@")
74            .put((byte) 0x08, "ftp://ftp.")
75            .put((byte) 0x09, "ftps://")
76            .put((byte) 0x0A, "sftp://")
77            .put((byte) 0x0B, "smb://")
78            .put((byte) 0x0C, "nfs://")
79            .put((byte) 0x0D, "ftp://")
80            .put((byte) 0x0E, "dav://")
81            .put((byte) 0x0F, "news:")
82            .put((byte) 0x10, "telnet://")
83            .put((byte) 0x11, "imap:")
84            .put((byte) 0x12, "rtsp://")
85            .put((byte) 0x13, "urn:")
86            .put((byte) 0x14, "pop:")
87            .put((byte) 0x15, "sip:")
88            .put((byte) 0x16, "sips:")
89            .put((byte) 0x17, "tftp:")
90            .put((byte) 0x18, "btspp://")
91            .put((byte) 0x19, "btl2cap://")
92            .put((byte) 0x1A, "btgoep://")
93            .put((byte) 0x1B, "tcpobex://")
94            .put((byte) 0x1C, "irdaobex://")
95            .put((byte) 0x1D, "file://")
96            .put((byte) 0x1E, "urn:epc:id:")
97            .put((byte) 0x1F, "urn:epc:tag:")
98            .put((byte) 0x20, "urn:epc:pat:")
99            .put((byte) 0x21, "urn:epc:raw:")
100            .put((byte) 0x22, "urn:epc:")
101            .put((byte) 0x23, "urn:nfc:")
102            .build();
103
104    private final Uri mUri;
105
106    private UriRecord(Uri uri) {
107        this.mUri = Preconditions.checkNotNull(uri);
108    }
109
110    public Intent getIntentForUri() {
111        String scheme = mUri.getScheme();
112        if ("tel".equals(scheme)) {
113            return new Intent(Intent.ACTION_CALL, mUri);
114        } else if ("sms".equals(scheme) || "smsto".equals(scheme)) {
115            return new Intent(Intent.ACTION_SENDTO, mUri);
116        } else {
117            return new Intent(Intent.ACTION_VIEW, mUri);
118        }
119    }
120
121    public String getPrettyUriString(Context context) {
122        String scheme = mUri.getScheme();
123        boolean tel = "tel".equals(scheme);
124        boolean sms = "sms".equals(scheme) || "smsto".equals(scheme);
125        if (tel || sms) {
126            String ssp = mUri.getSchemeSpecificPart();
127            int offset = ssp.indexOf('?');
128            if (offset >= 0) {
129                ssp = ssp.substring(0, offset);
130            }
131            if (tel) {
132                return context.getString(R.string.action_call, PhoneNumberUtils.formatNumber(ssp));
133            } else {
134                return context.getString(R.string.action_text, PhoneNumberUtils.formatNumber(ssp));
135            }
136        } else {
137            return mUri.toString();
138        }
139    }
140
141    @Override
142    public View getView(Activity activity, LayoutInflater inflater, ViewGroup parent, int offset) {
143        return RecordUtils.getViewsForIntent(activity, inflater, parent, this, getIntentForUri(),
144                getPrettyUriString(activity));
145    }
146
147    @Override
148    public String getSnippet(Context context, Locale locale) {
149        return getPrettyUriString(context);
150    }
151
152    @Override
153    public void onClick(View view) {
154        RecordUtils.ClickInfo info = (RecordUtils.ClickInfo) view.getTag();
155        try {
156            info.activity.startActivity(info.intent);
157            info.activity.finish();
158        } catch (ActivityNotFoundException e) {
159            // The activity wansn't found for some reason. Don't crash, but don't do anything.
160            Log.e(TAG, "Failed to launch activity for intent " + info.intent, e);
161        }
162    }
163
164    @VisibleForTesting
165    public Uri getUri() {
166        return mUri;
167    }
168
169    /**
170     * Convert {@link android.nfc.NdefRecord} into a {@link android.net.Uri}. This will handle
171     * both TNF_WELL_KNOWN / RTD_URI and TNF_ABSOLUTE_URI.
172     *
173     * @throws IllegalArgumentException if the NdefRecord is not a
174     *     record containing a URI.
175     */
176    public static UriRecord parse(NdefRecord record) {
177        short tnf = record.getTnf();
178        if (tnf == NdefRecord.TNF_WELL_KNOWN) {
179            return parseWellKnown(record);
180        } else if (tnf == NdefRecord.TNF_ABSOLUTE_URI) {
181            return parseAbsolute(record);
182        }
183        throw new IllegalArgumentException("Unknown TNF " + tnf);
184    }
185
186    /** Parse and absolute URI record */
187    private static UriRecord parseAbsolute(NdefRecord record) {
188        byte[] payload = record.getPayload();
189        Uri uri = Uri.parse(new String(payload, Charset.forName("UTF-8")));
190        return new UriRecord(uri);
191    }
192
193    /** Parse an well known URI record */
194    private static UriRecord parseWellKnown(NdefRecord record) {
195        Preconditions.checkArgument(Arrays.equals(record.getType(), NdefRecord.RTD_URI));
196
197        byte[] payload = record.getPayload();
198        Preconditions.checkArgument(payload.length > 0);
199
200        /*
201         * payload[0] contains the URI Identifier Code, per the
202         * NFC Forum "URI Record Type Definition" section 3.2.2.
203         *
204         * payload[1]...payload[payload.length - 1] contains the rest of
205         * the URI.
206         */
207
208        String prefix = URI_PREFIX_MAP.get(payload[0]);
209        Preconditions.checkArgument(prefix != null);
210
211        byte[] fullUri = Bytes.concat(
212                prefix.getBytes(Charset.forName("UTF-8")),
213                Arrays.copyOfRange(payload, 1, payload.length));
214
215        Uri uri = Uri.parse(new String(fullUri, Charset.forName("UTF-8")));
216        return new UriRecord(uri);
217    }
218
219    public static boolean isUri(NdefRecord record) {
220        try {
221            parse(record);
222            return true;
223        } catch (IllegalArgumentException e) {
224            return false;
225        }
226    }
227
228    private static final byte[] EMPTY = new byte[0];
229
230    /**
231     * Convert a {@link Uri} to an {@link NdefRecord}
232     */
233    public static NdefRecord newUriRecord(Uri uri) {
234        byte[] uriBytes = uri.toString().getBytes(Charset.forName("UTF-8"));
235
236        /*
237         * We prepend 0x00 to the bytes of the URI to indicate that this
238         * is the entire URI, and we are not taking advantage of the
239         * URI shortening rules in the NFC Forum URI spec section 3.2.2.
240         * This produces a NdefRecord which is slightly larger than
241         * necessary.
242         *
243         * In the future, we should use the URI shortening rules in 3.2.2
244         * to create a smaller NdefRecord.
245         */
246        byte[] payload = Bytes.concat(new byte[] { 0x00 }, uriBytes);
247
248        return new NdefRecord(NdefRecord.TNF_WELL_KNOWN,
249                NdefRecord.RTD_URI, EMPTY, payload);
250    }
251}
252