/* * Copyright (C) 2010 mAPPn.Inc * * 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.lan.nicehair.common.download; import java.io.File; import java.util.ArrayList; import java.util.List; import android.content.ContentProvider; import android.content.ContentUris; import android.content.ContentValues; import android.content.Context; import android.content.Intent; import android.content.UriMatcher; import android.database.CrossProcessCursor; import android.database.Cursor; import android.database.CursorWindow; import android.database.CursorWrapper; import android.database.SQLException; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import android.net.Uri; import android.text.TextUtils; import com.lan.nicehair.utils.AppLog; import com.lan.nicehair.utils.Utils; /** * Allows application to interact with the download manager. */ public final class DownloadProvider extends ContentProvider { private final String TAG="DownloadProvider"; /** Database filename */ private static final String DB_NAME = "downloads.db"; /** Current database version */ private static final int DB_VERSION = 108; /** Name of table in the database */ private static final String DB_TABLE = "downloads"; /** MIME type for the entire download list */ private static final String DOWNLOAD_LIST_TYPE = "vnd.android.cursor.dir/download"; /** MIME type for an individual download */ private static final String DOWNLOAD_TYPE = "vnd.android.cursor.item/download"; /** URI matcher used to recognize URIs sent by applications */ private static final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH); /** URI matcher constant for the URI of all downloads belonging to the calling UID */ private static final int MY_DOWNLOADS = 1; /** URI matcher constant for the URI of an individual download belonging to the calling UID */ private static final int MY_DOWNLOADS_ID = 2; static { sURIMatcher.addURI("gfan_downloads", "my_downloads", MY_DOWNLOADS); sURIMatcher.addURI("gfan_downloads", "my_downloads/#", MY_DOWNLOADS_ID); } /** The database that lies underneath this content provider */ private SQLiteOpenHelper mOpenHelper = null; /** * This class encapsulates a SQL where clause and its parameters. It makes it possible for * shared methods (like {@link DownloadProvider#getWhereClause(Uri, String, String[], int)}) * to return both pieces of information, and provides some utility logic to ease piece-by-piece * construction of selections. */ private static class SqlSelection { public StringBuilder mWhereClause = new StringBuilder(); public List<String> mParameters = new ArrayList<String>(); public <T> void appendClause(String newClause, final T... parameters) { if (TextUtils.isEmpty(newClause)) { return; } if (mWhereClause.length() != 0) { mWhereClause.append(" AND "); } mWhereClause.append("("); mWhereClause.append(newClause); mWhereClause.append(")"); if (parameters != null) { for (Object parameter : parameters) { mParameters.add(parameter.toString()); } } } public String getSelection() { return mWhereClause.toString(); } public String[] getParameters() { String[] array = new String[mParameters.size()]; return mParameters.toArray(array); } } /** * Creates and updated database on demand when opening it. * Helper class to create database the first time the provider is * initialized and upgrade it when a new version of the provider needs * an updated version of the database. */ private final class DatabaseHelper extends SQLiteOpenHelper { public DatabaseHelper(final Context context) { super(context, DB_NAME, null, DB_VERSION); } /** * Creates database the first time we try to open it. */ @Override public void onCreate(final SQLiteDatabase db) { AppLog.d(TAG,"populating new database"); onUpgrade(db, 0, DB_VERSION); } /** * Updates the database format when a content provider is used * with a database that was created with a different format. * * Note: to support downgrades, creating a table should always drop it first if it already * exists. */ @Override public void onUpgrade(final SQLiteDatabase db, int oldV, final int newV) { createDownloadsTable(db); } /** * Creates the table that'll hold the download information. */ private void createDownloadsTable(SQLiteDatabase db) { try { db.execSQL("DROP TABLE IF EXISTS " + DB_TABLE); db.execSQL("CREATE TABLE " + DB_TABLE + "(" + DownloadManager.Impl._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + DownloadManager.Impl.COLUMN_URI + " TEXT, " + DownloadManager.Impl.COLUMN_RETRY_AFTER_REDIRECT_COUNT + " INTEGER, " + DownloadManager.Impl.COLUMN_APP_DATA + " TEXT, " + DownloadManager.Impl.COLUMN_FILE_NAME_HINT + " TEXT, " + DownloadManager.Impl.COLUMN_DATA + " TEXT, " + DownloadManager.Impl.COLUMN_MIME_TYPE + " TEXT, " + DownloadManager.Impl.COLUMN_DESTINATION + " INTEGER, " + DownloadManager.Impl.COLUMN_VISIBILITY + " INTEGER, " + DownloadManager.Impl.COLUMN_CONTROL + " INTEGER, " + DownloadManager.Impl.COLUMN_STATUS + " INTEGER, " + DownloadManager.Impl.COLUMN_FAILED_CONNECTIONS + " INTEGER, " + DownloadManager.Impl.COLUMN_LAST_MODIFICATION + " BIGINT, " + DownloadManager.Impl.COLUMN_NOTIFICATION_PACKAGE + " TEXT, " + DownloadManager.Impl.COLUMN_NOTIFICATION_CLASS + " TEXT, " + DownloadManager.Impl.COLUMN_NOTIFICATION_EXTRAS + " TEXT, " + DownloadManager.Impl.COLUMN_TOTAL_BYTES + " INTEGER DEFAULT -1, " + DownloadManager.Impl.COLUMN_CURRENT_BYTES + " INTEGER DEFAULT 0, " + DownloadManager.Impl.COLUMN_ETAG + " TEXT, " + DownloadManager.Impl.COLUMN_MD5 + " TEXT, " + DownloadManager.Impl.COLUMN_PACKAGE_NAME + " TEXT, " + DownloadManager.Impl.COLUMN_TITLE + " TEXT, " + DownloadManager.Impl.COLUMN_DESCRIPTION + " TEXT, " + DownloadManager.Impl.COLUMN_DELETED + " BOOLEAN NOT NULL DEFAULT 0, " + DownloadManager.Impl.COLUMN_SOURCE + " INTEGER);"); } catch (SQLException ex) { AppLog.e(TAG,"couldn't create table in downloads database"); throw ex; } } } /** * Initializes the content provider when it is created. */ @Override public boolean onCreate() { mOpenHelper = new DatabaseHelper(getContext()); return true; } /** * Returns the content-provider-style MIME types of the various * types accessible through this content provider. */ @Override public String getType(final Uri uri) { int match = sURIMatcher.match(uri); switch (match) { case MY_DOWNLOADS: { return DOWNLOAD_LIST_TYPE; } case MY_DOWNLOADS_ID: { return DOWNLOAD_TYPE; } default: { AppLog.d(TAG,"calling getType on an unknown URI: " + uri); throw new IllegalArgumentException("Unknown URI: " + uri); } } } /** * Inserts a row in the database */ @Override public Uri insert(final Uri uri, final ContentValues values) { SQLiteDatabase db = mOpenHelper.getWritableDatabase(); ContentValues filteredValues = new ContentValues(); copyString(DownloadManager.Impl.COLUMN_URI, values, filteredValues); copyString(DownloadManager.Impl.COLUMN_APP_DATA, values, filteredValues); copyString(DownloadManager.Impl.COLUMN_FILE_NAME_HINT, values, filteredValues); copyString(DownloadManager.Impl.COLUMN_MIME_TYPE, values, filteredValues); copyString(DownloadManager.Impl.COLUMN_PACKAGE_NAME, values, filteredValues); copyString(DownloadManager.Impl.COLUMN_MD5, values, filteredValues); copyInteger(DownloadManager.Impl.COLUMN_DESTINATION, values, filteredValues); // Integer dest = values.getAsInteger(DownloadManager.Impl.COLUMN_DESTINATION); // if(dest == DownloadManager.Impl.DESTINATION_EXTERNAL) { // filteredValues.put(DownloadManager.Impl.COLUMN_VISIBILITY, // DownloadManager.Impl.VISIBILITY_VISIBLE_NOTIFY_COMPLETED); // } else { // filteredValues.put(DownloadManager.Impl.COLUMN_VISIBILITY, // DownloadManager.Impl.VISIBILITY_HIDDEN); // } copyInteger(DownloadManager.Impl.COLUMN_VISIBILITY, values, filteredValues); copyInteger(DownloadManager.Impl.COLUMN_CONTROL, values, filteredValues); copyInteger(DownloadManager.Impl.COLUMN_SOURCE, values, filteredValues); filteredValues.put(DownloadManager.Impl.COLUMN_STATUS, DownloadManager.Impl.STATUS_PENDING); filteredValues.put(DownloadManager.Impl.COLUMN_LAST_MODIFICATION, System.currentTimeMillis()); String pckg = values.getAsString(DownloadManager.Impl.COLUMN_NOTIFICATION_PACKAGE); String clazz = values.getAsString(DownloadManager.Impl.COLUMN_NOTIFICATION_CLASS); if (pckg != null) { filteredValues.put(DownloadManager.Impl.COLUMN_NOTIFICATION_PACKAGE, pckg); if (clazz != null) { filteredValues.put(DownloadManager.Impl.COLUMN_NOTIFICATION_CLASS, clazz); } } copyString(DownloadManager.Impl.COLUMN_NOTIFICATION_EXTRAS, values, filteredValues); copyStringWithDefault(DownloadManager.Impl.COLUMN_TITLE, values, filteredValues, ""); copyStringWithDefault(DownloadManager.Impl.COLUMN_DESCRIPTION, values, filteredValues, ""); filteredValues.put(DownloadManager.Impl.COLUMN_TOTAL_BYTES, -1); filteredValues.put(DownloadManager.Impl.COLUMN_CURRENT_BYTES, 0); Context context = getContext(); context.startService(new Intent(context, DownloadService.class)); long rowID = db.insert(DB_TABLE, null, filteredValues); if (rowID == -1) { AppLog.d(TAG,"couldn't insert into downloads database"); return null; } context.startService(new Intent(context, DownloadService.class)); notifyContentChanged(uri, sURIMatcher.match(uri)); return ContentUris.withAppendedId(DownloadManager.Impl.CONTENT_URI, rowID); } // /** // * Check that the file URI provided for DESTINATION_FILE_URI is valid. // */ // private void checkFileUriDestination(ContentValues values) { // String fileUri = values.getAsString(DownloadManager.Impl.COLUMN_FILE_NAME_HINT); // if (fileUri == null) { // throw new IllegalArgumentException( // "DESTINATION_FILE_URI must include a file URI under COLUMN_FILE_NAME_HINT"); // } // Uri uri = Uri.parse(fileUri); // String scheme = uri.getScheme(); // if (scheme == null || !scheme.equals("file")) { // throw new IllegalArgumentException("Not a file URI: " + uri); // } // String path = uri.getPath(); // if (path == null) { // throw new IllegalArgumentException("Invalid file URI: " + uri); // } // String externalPath = Environment.getExternalStorageDirectory().getAbsolutePath(); // if (!path.startsWith(externalPath)) { // throw new SecurityException("Destination must be on external storage: " + uri); // } // } /** * Starts a database query */ @Override public Cursor query(final Uri uri, String[] projection, final String selection, final String[] selectionArgs, final String sort) { SQLiteDatabase db = mOpenHelper.getReadableDatabase(); int match = sURIMatcher.match(uri); if (match == -1) { AppLog.d(TAG,"querying unknown URI: " + uri); throw new IllegalArgumentException("Unknown URI: " + uri); } SqlSelection fullSelection = getWhereClause(uri, selection, selectionArgs, match); // print the query info logVerboseQueryInfo(projection, selection, selectionArgs, sort, db); Cursor ret = db.query(DB_TABLE, projection, fullSelection.getSelection(), fullSelection.getParameters(), null, null, sort); if (ret != null) { ret = new ReadOnlyCursorWrapper(ret); } if (ret != null) { ret.setNotificationUri(getContext().getContentResolver(), uri); } else { AppLog.d(TAG,"query failed in downloads database"); } return ret; } /* * print the query info */ private void logVerboseQueryInfo(String[] projection, final String selection, final String[] selectionArgs, final String sort, SQLiteDatabase db) { java.lang.StringBuilder sb = new java.lang.StringBuilder(); sb.append("starting query, database is "); if (db != null) { sb.append("not "); } sb.append("null; "); if (projection == null) { sb.append("projection is null; "); } else if (projection.length == 0) { sb.append("projection is empty; "); } else { for (int i = 0; i < projection.length; ++i) { sb.append("projection["); sb.append(i); sb.append("] is "); sb.append(projection[i]); sb.append("; "); } } sb.append("selection is "); sb.append(selection); sb.append("; "); if (selectionArgs == null) { sb.append("selectionArgs is null; "); } else if (selectionArgs.length == 0) { sb.append("selectionArgs is empty; "); } else { for (int i = 0; i < selectionArgs.length; ++i) { sb.append("selectionArgs["); sb.append(i); sb.append("] is "); sb.append(selectionArgs[i]); sb.append("; "); } } sb.append("sort is "); sb.append(sort); sb.append("."); AppLog.d(TAG,sb.toString()); } private String getDownloadIdFromUri(final Uri uri) { return uri.getPathSegments().get(1); } /** * Updates a row in the database */ @Override public int update(final Uri uri, final ContentValues values, final String where, final String[] whereArgs) { SQLiteDatabase db = mOpenHelper.getWritableDatabase(); int count; boolean startService = false; if (values.containsKey(DownloadManager.Impl.COLUMN_DELETED)) { if (values.getAsInteger(DownloadManager.Impl.COLUMN_DELETED) == 1) { // some rows are to be 'deleted'. need to start DownloadService. startService = true; } } ContentValues filteredValues = values; String filename = values.getAsString(DownloadManager.Impl.COLUMN_DATA); if (filename != null) { // update the download item titile Cursor c = query(uri, new String[] { DownloadManager.Impl.COLUMN_TITLE }, null, null, null); if (!c.moveToFirst() || TextUtils.isEmpty(c.getString(0))) { values.put(DownloadManager.Impl.COLUMN_TITLE, new File(filename).getName()); } c.close(); } Integer status = values.getAsInteger(DownloadManager.Impl.COLUMN_STATUS); boolean isRestart = status != null && status == DownloadManager.Impl.STATUS_PENDING; if (isRestart) { startService = true; } int match = sURIMatcher.match(uri); switch (match) { case MY_DOWNLOADS: case MY_DOWNLOADS_ID: SqlSelection selection = getWhereClause(uri, where, whereArgs, match); if (filteredValues.size() > 0) { AppLog.d(TAG,"update database values : " + filteredValues); count = db.update(DB_TABLE, filteredValues, selection.getSelection(), selection.getParameters()); if(count > 0) { startService = true; } } else { count = 0; } break; default: AppLog.d(TAG,"updating unknown/invalid URI: " + uri); throw new UnsupportedOperationException("Cannot update URI: " + uri); } notifyContentChanged(uri, match); if (startService) { Context context = getContext(); context.startService(new Intent(context, DownloadService.class)); } return count; } /** * Notify of a change through both URIs (/my_downloads and /all_downloads) * @param uri either URI for the changed download(s) * @param uriMatch the match ID from {@link #sURIMatcher} */ private void notifyContentChanged(final Uri uri, int uriMatch) { Long downloadId = null; if (uriMatch == MY_DOWNLOADS_ID) { downloadId = Long.parseLong(getDownloadIdFromUri(uri)); } Uri uriToNotify = DownloadManager.Impl.CONTENT_URI; if (downloadId != null) { uriToNotify = ContentUris.withAppendedId(uriToNotify, downloadId); } getContext().getContentResolver().notifyChange(uriToNotify, null); } private SqlSelection getWhereClause(final Uri uri, final String where, final String[] whereArgs, int uriMatch) { SqlSelection selection = new SqlSelection(); selection.appendClause(where, whereArgs); if (uriMatch == MY_DOWNLOADS_ID) { selection.appendClause(DownloadManager.Impl._ID + " = ?", getDownloadIdFromUri(uri)); } return selection; } /** * Deletes a row in the database */ @Override public int delete(final Uri uri, final String where, final String[] whereArgs) { SQLiteDatabase db = mOpenHelper.getWritableDatabase(); int count; int match = sURIMatcher.match(uri); switch (match) { case MY_DOWNLOADS: case MY_DOWNLOADS_ID: SqlSelection selection = getWhereClause(uri, where, whereArgs, match); count = db.delete(DB_TABLE, selection.getSelection(), selection.getParameters()); break; default: AppLog.d(TAG,"deleting unknown/invalid URI: " + uri); throw new UnsupportedOperationException("Cannot delete URI: " + uri); } notifyContentChanged(uri, match); return count; } private static final void copyInteger(String key, ContentValues from, ContentValues to) { Integer i = from.getAsInteger(key); if (i != null) { to.put(key, i); } } private static final void copyString(String key, ContentValues from, ContentValues to) { String s = from.getAsString(key); if (s != null) { to.put(key, s); } } private static final void copyStringWithDefault(String key, ContentValues from, ContentValues to, String defaultValue) { copyString(key, from, to); if (!to.containsKey(key)) { to.put(key, defaultValue); } } private class ReadOnlyCursorWrapper extends CursorWrapper implements CrossProcessCursor { public ReadOnlyCursorWrapper(Cursor cursor) { super(cursor); mCursor = (CrossProcessCursor) cursor; } // public boolean deleteRow() { // throw new SecurityException("Download manager cursors are read-only"); // } // // public boolean commitUpdates() { // throw new SecurityException("Download manager cursors are read-only"); // } public void fillWindow(int pos, CursorWindow window) { mCursor.fillWindow(pos, window); } public CursorWindow getWindow() { return mCursor.getWindow(); } public boolean onMove(int oldPosition, int newPosition) { return mCursor.onMove(oldPosition, newPosition); } private CrossProcessCursor mCursor; } }