Add context menu (long press) on items
[android/platform/packages/apps/Tag.git] / src / com / android / apps / tag / provider / TagProvider.java
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.provider;
18
19 import com.android.apps.tag.R;
20 import com.android.apps.tag.provider.TagContract.NdefMessages;
21 import com.google.common.base.Charsets;
22 import com.google.common.base.Preconditions;
23 import com.google.common.collect.ImmutableMap;
24
25 import android.content.ContentUris;
26 import android.content.ContentValues;
27 import android.content.Context;
28 import android.content.UriMatcher;
29 import android.database.Cursor;
30 import android.database.sqlite.SQLiteDatabase;
31 import android.database.sqlite.SQLiteOpenHelper;
32 import android.database.sqlite.SQLiteQueryBuilder;
33 import android.net.Uri;
34 import android.nfc.FormatException;
35 import android.nfc.NdefMessage;
36 import android.nfc.NdefRecord;
37 import android.os.AsyncTask;
38 import android.os.ParcelFileDescriptor;
39 import android.text.TextUtils;
40 import android.util.Log;
41
42 import java.io.FileNotFoundException;
43 import java.io.FileOutputStream;
44 import java.io.IOException;
45 import java.util.Map;
46
47 /**
48  * Stores NFC tags in a database. The contract is defined in {@link TagContract}.
49  */
50 public class TagProvider extends SQLiteContentProvider implements TagProviderPipeDataWriter {
51     private static final String TAG = "TagProvider";
52
53     private static final int NDEF_MESSAGES = 1000;
54     private static final int NDEF_MESSAGES_ID = 1001;
55
56     private static final int NDEF_MESSAGES_ID_MIME = 2002;
57
58     private static final UriMatcher MATCHER;
59
60
61     private static final Map<String, String> NDEF_MESSAGES_PROJECTION_MAP =
62             ImmutableMap.<String, String>builder()
63                 .put(NdefMessages._ID, NdefMessages._ID)
64                 .put(NdefMessages.TITLE, NdefMessages.TITLE)
65                 .put(NdefMessages.BYTES, NdefMessages.BYTES)
66                 .put(NdefMessages.DATE, NdefMessages.DATE)
67                 .put(NdefMessages.STARRED, NdefMessages.STARRED)
68                 .build();
69
70     private Map<String, String> mNdefRecordsMimeProjectionMap;
71
72     static {
73         MATCHER = new UriMatcher(0);
74         String auth = TagContract.AUTHORITY;
75
76         MATCHER.addURI(auth, "ndef_msgs", NDEF_MESSAGES);
77         MATCHER.addURI(auth, "ndef_msgs/#", NDEF_MESSAGES_ID);
78         MATCHER.addURI(auth, "ndef_msgs/#/#/mime", NDEF_MESSAGES_ID_MIME);
79     }
80
81     @Override
82     public boolean onCreate() {
83         boolean result = super.onCreate();
84
85         // Build the projection map for the MIME records using a localized display name
86         mNdefRecordsMimeProjectionMap = ImmutableMap.<String, String>builder()
87                 .put(NdefMessages.MIME._ID, NdefMessages.MIME._ID)
88                 .put(NdefMessages.MIME.SIZE, NdefMessages.MIME.SIZE)
89                 .put(NdefMessages.MIME.DISPLAY_NAME,
90                         "'" + getContext().getString(R.string.mime_display_name) + "' AS "
91                         + NdefMessages.MIME.DISPLAY_NAME)
92                 .build();
93
94         return result;
95     }
96
97     @Override
98     protected SQLiteOpenHelper getDatabaseHelper(Context context) {
99         return new TagDBHelper(context);
100     }
101
102     /**
103      * Appends one set of selection args to another. This is useful when adding a selection
104      * argument to a user provided set.
105      */
106     public static String[] appendSelectionArgs(String[] originalValues, String[] newValues) {
107         if (originalValues == null || originalValues.length == 0) {
108             return newValues;
109         }
110         String[] result = new String[originalValues.length + newValues.length ];
111         System.arraycopy(originalValues, 0, result, 0, originalValues.length);
112         System.arraycopy(newValues, 0, result, originalValues.length, newValues.length);
113         return result;
114     }
115
116     /**
117      * Concatenates two SQL WHERE clauses, handling empty or null values.
118      */
119     public static String concatenateWhere(String a, String b) {
120         if (TextUtils.isEmpty(a)) {
121             return b;
122         }
123         if (TextUtils.isEmpty(b)) {
124             return a;
125         }
126
127         return "(" + a + ") AND (" + b + ")";
128     }
129
130     @Override
131     public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
132             String sortOrder) {
133         SQLiteDatabase db = getDatabaseHelper().getReadableDatabase();
134         SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
135         int match = MATCHER.match(uri);
136         switch (match) {
137             case NDEF_MESSAGES_ID: {
138                 selection = concatenateWhere(selection,
139                         TagDBHelper.TABLE_NAME_NDEF_MESSAGES + "._id=?");
140                 selectionArgs = appendSelectionArgs(selectionArgs,
141                         new String[] { Long.toString(ContentUris.parseId(uri)) });
142                 // fall through
143             }
144             case NDEF_MESSAGES: {
145                 qb.setTables(TagDBHelper.TABLE_NAME_NDEF_MESSAGES);
146                 qb.setProjectionMap(NDEF_MESSAGES_PROJECTION_MAP);
147                 break;
148             }
149
150             case NDEF_MESSAGES_ID_MIME: {
151                 selection = concatenateWhere(selection,
152                         TagDBHelper.TABLE_NAME_NDEF_MESSAGES + "._id=?");
153                 selectionArgs = appendSelectionArgs(selectionArgs,
154                         new String[] { Long.toString(ContentUris.parseId(uri)) });
155                 qb.setTables(TagDBHelper.TABLE_NAME_NDEF_MESSAGES);
156                 qb.setProjectionMap(mNdefRecordsMimeProjectionMap);
157                 break;
158             }
159
160             default: {
161                 throw new IllegalArgumentException("unkown uri " + uri);
162             }
163         }
164
165         Cursor cursor = qb.query(db, projection, selection, selectionArgs, null, null, sortOrder);
166         if (cursor != null) {
167             cursor.setNotificationUri(getContext().getContentResolver(), TagContract.AUTHORITY_URI);
168         }
169         return cursor;
170     }
171
172     @Override
173     protected Uri insertInTransaction(Uri uri, ContentValues values) {
174         SQLiteDatabase db = getDatabaseHelper().getWritableDatabase();
175         int match = MATCHER.match(uri);
176         long id = -1;
177         switch (match) {
178             case NDEF_MESSAGES: {
179                 id = db.insert(TagDBHelper.TABLE_NAME_NDEF_MESSAGES, NdefMessages.TITLE, values);
180                 break;
181             }
182
183             default: {
184                 throw new IllegalArgumentException("unkown uri " + uri);
185             }
186         }
187
188         if (id >= 0) {
189             return ContentUris.withAppendedId(uri, id);
190         }
191         return null;
192     }
193
194     @Override
195     protected int updateInTransaction(Uri uri, ContentValues values, String selection,
196             String[] selectionArgs) {
197         SQLiteDatabase db = getDatabaseHelper().getWritableDatabase();
198         int match = MATCHER.match(uri);
199         int count = 0;
200         switch (match) {
201             case NDEF_MESSAGES_ID: {
202                 selection = concatenateWhere(selection,
203                         TagDBHelper.TABLE_NAME_NDEF_MESSAGES + "._id=?");
204                 selectionArgs = appendSelectionArgs(selectionArgs,
205                         new String[] { Long.toString(ContentUris.parseId(uri)) });
206                 // fall through
207             }
208             case NDEF_MESSAGES: {
209                 count = db.update(TagDBHelper.TABLE_NAME_NDEF_MESSAGES, values, selection,
210                         selectionArgs);
211                 break;
212             }
213
214             default: {
215                 throw new IllegalArgumentException("unkown uri " + uri);
216             }
217         }
218
219         return count;
220     }
221
222     @Override
223     protected int deleteInTransaction(Uri uri, String selection, String[] selectionArgs) {
224         SQLiteDatabase db = getDatabaseHelper().getWritableDatabase();
225         int match = MATCHER.match(uri);
226         int count = 0;
227         switch (match) {
228             case NDEF_MESSAGES_ID: {
229                 selection = concatenateWhere(selection,
230                         TagDBHelper.TABLE_NAME_NDEF_MESSAGES + "._id=?");
231                 selectionArgs = appendSelectionArgs(selectionArgs,
232                         new String[] { Long.toString(ContentUris.parseId(uri)) });
233                 // fall through
234             }
235             case NDEF_MESSAGES: {
236                 count = db.delete(TagDBHelper.TABLE_NAME_NDEF_MESSAGES, selection, selectionArgs);
237                 break;
238             }
239
240             default: {
241                 throw new IllegalArgumentException("unkown uri " + uri);
242             }
243         }
244
245         return count;
246     }
247
248     private NdefRecord getRecord(Uri uri) {
249         NdefRecord record = null;
250
251         Cursor cursor = null;
252         try {
253             SQLiteDatabase db = getDatabaseHelper().getReadableDatabase();
254             cursor = db.query(TagDBHelper.TABLE_NAME_NDEF_MESSAGES,
255                     new String[] { NdefMessages.BYTES }, "_id=?",
256                     new String[] { uri.getPathSegments().get(1) }, null, null, null, null);
257             if (cursor.moveToFirst()) {
258                 NdefMessage msg = new NdefMessage(cursor.getBlob(0));
259                 NdefRecord[] records = msg.getRecords();
260
261                 int offset = Integer.parseInt(uri.getPathSegments().get(2));
262
263                 if (records != null && offset < records.length) {
264                     record = records[offset];
265                 }
266             }
267         } catch (FormatException e) {
268             Log.e(TAG, "Invalid NdefMessage format", e);
269         } finally {
270             if (cursor != null) cursor.close();
271         }
272
273         return record;
274     }
275
276
277     @Override
278     public String getType(Uri uri) {
279         int match = MATCHER.match(uri);
280         switch (match) {
281
282             case NDEF_MESSAGES_ID: {
283                 return NdefMessages.CONTENT_ITEM_TYPE;
284             }
285             case NDEF_MESSAGES: {
286                 return NdefMessages.CONTENT_TYPE;
287             }
288
289             case NDEF_MESSAGES_ID_MIME: {
290                 NdefRecord record = getRecord(uri);
291                 if (record != null) {
292                     return new String(record.getType(), Charsets.US_ASCII).toLowerCase();
293                 }
294                 return null;
295             }
296
297             default: {
298                 throw new IllegalArgumentException("unknown uri " + uri);
299             }
300         }
301     }
302
303     @Override
304     protected void notifyChange() {
305         getContext().getContentResolver().notifyChange(TagContract.AUTHORITY_URI, null, false);
306     }
307
308     @Override
309     public void writeMimeDataToPipe(ParcelFileDescriptor output, Uri uri) {
310         NdefRecord record = getRecord(uri);
311         if (record == null) return;
312
313         try {
314             byte[] data = record.getPayload();
315             FileOutputStream os = new FileOutputStream(output.getFileDescriptor());
316             os.write(data);
317             os.flush();
318         } catch (IOException e) {
319             Log.e(TAG, "failed to write MIME data to " + uri, e);
320         }
321     }
322
323     /**
324      * A helper function for implementing {@link #openFile}, for
325      * creating a data pipe and background thread allowing you to stream
326      * generated data back to the client.  This function returns a new
327      * ParcelFileDescriptor that should be returned to the caller (the caller
328      * is responsible for closing it).
329      *
330      * @param uri The URI whose data is to be written.
331      * @param func Interface implementing the function that will actually
332      * stream the data.
333      * @return Returns a new ParcelFileDescriptor holding the read side of
334      * the pipe.  This should be returned to the caller for reading; the caller
335      * is responsible for closing it when done.
336      */
337     public ParcelFileDescriptor openMimePipe(final Uri uri,
338             final TagProviderPipeDataWriter func) throws FileNotFoundException {
339         try {
340             final ParcelFileDescriptor[] fds = ParcelFileDescriptor.createPipe();
341
342             AsyncTask<Object, Object, Object> task = new AsyncTask<Object, Object, Object>() {
343                 @Override
344                 protected Object doInBackground(Object... params) {
345                     func.writeMimeDataToPipe(fds[1], uri);
346                     try {
347                         fds[1].close();
348                     } catch (IOException e) {
349                         Log.w(TAG, "Failure closing pipe", e);
350                     }
351                     return null;
352                 }
353             };
354             task.execute((Object[])null);
355
356             return fds[0];
357         } catch (IOException e) {
358             throw new FileNotFoundException("failure making pipe");
359         }
360     }
361
362     @Override
363     public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
364         Preconditions.checkArgument("r".equals(mode));
365         Preconditions.checkArgument(MATCHER.match(uri) == NDEF_MESSAGES_ID_MIME);
366         return openMimePipe(uri, this);
367     }
368 }