/******************************************************************************* * Copyright (c) 2011 Adam Shanks (ChainsDD) * * 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.noshufou.android.su.provider; import com.noshufou.android.su.util.Util; import android.content.ContentProvider; import android.content.ContentUris; import android.content.ContentValues; import android.content.Context; import android.content.UriMatcher; import android.database.Cursor; import android.database.SQLException; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteException; import android.database.sqlite.SQLiteOpenHelper; import android.database.sqlite.SQLiteQueryBuilder; import android.net.Uri; import android.text.TextUtils; import android.util.Log; import java.util.HashMap; public class PermissionsProvider extends ContentProvider { private static final String TAG = "Su.PermissionsProvider"; public static final String AUTHORITY = "com.noshufou.android.su.provider"; public static class Apps { public static final String CREATE = "CREATE TABLE IF NOT EXISTS " + Apps.TABLE_NAME + " (_id INTEGER PRIMARY KEY AUTOINCREMENT, uid INTEGER, package TEXT, name TEXT, " + "exec_uid INTEGER, exec_cmd TEXT, allow INTEGER, notifications INTEGER, " + "logging INTEGER, dirty INTEGER, UNIQUE (uid,exec_uid,exec_cmd));"; public static final Uri CONTENT_URI = Uri.parse("content://com.noshufou.android.su.provider/apps"); public static final Uri COUNT_CONTENT_URI = Uri.parse("content://com.noshufou.android.su.provider/apps/count"); public static final String TABLE_NAME = "apps"; public static final String APPS_LOGS_JOIN = "apps LEFT OUTER JOIN logs ON apps._id=logs.app_id"; public static final String _ID = "_id"; public static final String UID = "uid"; public static final String PACKAGE = "package"; public static final String NAME = "name"; public static final String EXEC_UID = "exec_uid"; public static final String EXEC_CMD = "exec_cmd"; public static final String ALLOW = "allow"; public static final String LAST_ACCESS = Logs.DATE; public static final String LAST_ACCESS_TYPE = Logs.TYPE; public static final String NOTIFICATIONS = "notifications"; public static final String LOGGING = "logging"; public static final String DIRTY = "dirty"; public static final class AllowType { public static final int TO_DELETE = -2; public static final int ASK = -1; public static final int DENY = 0; public static final int ALLOW = 1; } public static final String[] DEFAULT_PROJECTION = new String[] { _ID, UID, PACKAGE, NAME, EXEC_UID, EXEC_CMD, ALLOW, Logs.DATE, Logs.TYPE, NOTIFICATIONS, LOGGING }; public static final String DEFAULT_SORT_ORDER = "apps.allow DESC, apps.name ASC"; } public static class Logs { public static final String CREATE = "CREATE TABLE IF NOT EXISTS " + Logs.TABLE_NAME + " (_id INTEGER PRIMARY KEY AUTOINCREMENT, app_id INTEGER, date INTEGER, " + "type INTEGER);"; public static final Uri CONTENT_URI = Uri.parse("content://com.noshufou.android.su.provider/logs"); public static final String TABLE_NAME = "logs"; public static final String LOGS_APPS_JOIN = "logs LEFT OUTER JOIN apps ON logs.app_id=apps._id"; public static final String _ID = "_id"; public static final String APP_ID = "app_id"; public static final String UID = Apps.UID; public static final String NAME = Apps.NAME; public static final String PACKAGE = Apps.PACKAGE; public static final String DATE = "date"; public static final String TYPE = "type"; public static final class LogType { public static final int DENY = 0; public static final int ALLOW = 1; public static final int CREATE = 2; public static final int TOGGLE = 3; } public static final String[] DEFAULT_PROJECTION = new String[] { _ID, APP_ID, UID, NAME, PACKAGE, DATE, TYPE }; public static final String DEFAULT_SORT_ORDER = "logs.date DESC"; } private static final int APPS = 100; private static final int APP_ID = 101; private static final int APP_CLEAN = 107; private static final int APP_ID_LOGS = 102; private static final int APP_UID = 103; private static final int APP_UID_LOGS = 104; private static final int APP_COUNT = 105; private static final int APP_COUNT_TYPE = 106; private static final int LOGS = 200; private static final int LOGS_APP_ID = 202; private static final int LOGS_TYPE = 203; private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH); static { sUriMatcher.addURI(AUTHORITY, "apps", APPS); sUriMatcher.addURI(AUTHORITY, "apps/#", APP_ID); sUriMatcher.addURI(AUTHORITY, "apps/clean", APP_CLEAN); sUriMatcher.addURI(AUTHORITY, "apps/#/logs", APP_ID_LOGS); sUriMatcher.addURI(AUTHORITY, "apps/uid/#", APP_UID); sUriMatcher.addURI(AUTHORITY, "apps/uid/#/logs", APP_UID_LOGS); sUriMatcher.addURI(AUTHORITY, "apps/count", APP_COUNT); sUriMatcher.addURI(AUTHORITY, "apps/count/#", APP_COUNT_TYPE); sUriMatcher.addURI(AUTHORITY, "logs", LOGS); sUriMatcher.addURI(AUTHORITY, "logs/#", LOGS_APP_ID); sUriMatcher.addURI(AUTHORITY, "logs/type/#", LOGS_TYPE); } private static final HashMap<String, String> sAppsProjectionMap; static { sAppsProjectionMap = new HashMap<String, String>(); sAppsProjectionMap.put(Apps._ID, Apps.TABLE_NAME + "." + Apps._ID + " AS _id"); sAppsProjectionMap.put(Apps.UID, Apps.TABLE_NAME + "." + Apps.UID); sAppsProjectionMap.put(Apps.PACKAGE, Apps.TABLE_NAME + "." + Apps.PACKAGE); sAppsProjectionMap.put(Apps.NAME, Apps.TABLE_NAME + "." + Apps.NAME); sAppsProjectionMap.put(Apps.EXEC_UID, Apps.TABLE_NAME + "." + Apps.EXEC_UID); sAppsProjectionMap.put(Apps.EXEC_CMD, Apps.TABLE_NAME + "." + Apps.EXEC_CMD); sAppsProjectionMap.put(Apps.ALLOW, Apps.TABLE_NAME + "." + Apps.ALLOW); sAppsProjectionMap.put(Apps.LAST_ACCESS, Logs.TABLE_NAME + "." + Logs.DATE); sAppsProjectionMap.put(Apps.LAST_ACCESS_TYPE, Logs.TABLE_NAME + "." + Logs.TYPE); sAppsProjectionMap.put(Apps.NOTIFICATIONS, Apps.TABLE_NAME + "." + Apps.NOTIFICATIONS); sAppsProjectionMap.put(Apps.LOGGING, Apps.TABLE_NAME + "." + Apps.LOGGING); sAppsProjectionMap.put(Apps.DIRTY, Apps.TABLE_NAME + "." + Apps.DIRTY); } private static final HashMap<String, String> sLogsProjectionMap; static { sLogsProjectionMap = new HashMap<String, String>(); sLogsProjectionMap.put(Logs._ID, Logs.TABLE_NAME + "." + Logs._ID + " AS _id"); sLogsProjectionMap.put(Logs.APP_ID, Logs.TABLE_NAME + "." + Logs.APP_ID); sLogsProjectionMap.put(Logs.UID, Apps.TABLE_NAME + "." + Apps.UID); sLogsProjectionMap.put(Logs.NAME, Apps.TABLE_NAME + "." + Apps.NAME); sLogsProjectionMap.put(Logs.PACKAGE, Apps.TABLE_NAME + "." + Apps.PACKAGE); sLogsProjectionMap.put(Logs.DATE, Logs.TABLE_NAME + "." + Logs.DATE); sLogsProjectionMap.put(Logs.TYPE, Logs.TABLE_NAME + "." + Logs.TYPE); } private Context mContext; private SuDbOpenHelper mDbHelper = null; private SQLiteDatabase mDb = null; @Override public boolean onCreate() { mContext = getContext(); mDbHelper = new SuDbOpenHelper(mContext); return (mDbHelper == null)?false:true; } @Override public String getType(Uri uri) { switch (sUriMatcher.match(uri)) { case APPS: return "vnd.android.cursor.dir/vnd.noshufou.superuser.apps "; case APP_ID: case APP_UID: case APP_COUNT: case APP_COUNT_TYPE: return "vnd.android.cursor.item/vnd.noshufou.superuser.apps "; case APP_ID_LOGS: case APP_UID_LOGS: case LOGS: case LOGS_APP_ID: case LOGS_TYPE: return "vnd.android.cursor.dir/vnd.noshufou.superuser.logs "; default: throw new IllegalArgumentException("Unsupported URI: " + uri); } } @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { if (!ensureDb()) return null; SQLiteQueryBuilder qBuilder = new SQLiteQueryBuilder(); String[] defaultProjection = null; String groupBy = null; int uriMatch = sUriMatcher.match(uri); // Set up table and default projection switch (uriMatch) { case APPS: case APP_ID: case APP_UID: qBuilder.setTables(Apps.APPS_LOGS_JOIN); qBuilder.setProjectionMap(sAppsProjectionMap); defaultProjection = Apps.DEFAULT_PROJECTION; // qBuilder.appendWhere("apps.allow!=-1"); // Leave out apps only there for Log purposes groupBy = Apps.TABLE_NAME + "." + Apps._ID; sortOrder = sortOrder==null?Apps.DEFAULT_SORT_ORDER:sortOrder; sortOrder = sortOrder + (!TextUtils.isEmpty(sortOrder)?", ":""); sortOrder = sortOrder + Logs.DATE + " DESC"; break; case APP_ID_LOGS: case APP_UID_LOGS: case LOGS: case LOGS_APP_ID: case LOGS_TYPE: qBuilder.setTables(Logs.LOGS_APPS_JOIN); qBuilder.setProjectionMap(sLogsProjectionMap); defaultProjection = Logs.DEFAULT_PROJECTION; sortOrder = sortOrder==null?Logs.DEFAULT_SORT_ORDER:sortOrder; break; case APP_COUNT: case APP_COUNT_TYPE: qBuilder.setTables(Apps.TABLE_NAME); defaultProjection = new String[] { "COUNT() as rows" }; break; default: throw new IllegalArgumentException("Unsupported URI: " + uri); } // Append a particular item if necessary switch (uriMatch) { case APP_ID: qBuilder.appendWhere(" apps._id=" + uri.getPathSegments().get(1)); break; case APP_ID_LOGS: case LOGS_APP_ID: qBuilder.appendWhere("apps._id=" + uri.getPathSegments().get(1)); break; case APP_UID: qBuilder.appendWhere(" apps.uid=" + uri.getPathSegments().get(2)); break; case APP_UID_LOGS: qBuilder.appendWhere("apps.uid=" + uri.getPathSegments().get(2)); break; case LOGS_TYPE: qBuilder.appendWhere("logs.type=" + uri.getPathSegments().get(2)); break; case APP_COUNT_TYPE: qBuilder.appendWhere("apps.allow=" + uri.getPathSegments().get(2)); } // TODO: Check columns in incoming projection to make sure they're valid projection = projection==null?defaultProjection:projection; Cursor c = null; try { c = qBuilder.query(mDb, projection, selection, selectionArgs, groupBy, null, sortOrder); } catch (SQLiteException e) { Log.e(TAG, "Query failed, returning null cursor.", e); } c.setNotificationUri(mContext.getContentResolver(), uri); return c; } @Override public Uri insert(Uri uri, ContentValues values) { if (!ensureDb()) return null; long rowId = 0; Uri returnUri = null; switch (sUriMatcher.match(uri)) { case APPS: // TODO: Check validity of incoming data before inserting it values.put(Apps.DIRTY, "1"); try { rowId = mDb.insertOrThrow(Apps.TABLE_NAME, null, values); } catch (SQLException e) { String where = Apps.UID + "=? AND " + Apps.EXEC_UID + "=? AND " + Apps.EXEC_CMD + "=?"; String[] whereArgs = new String[] { values.getAsString(Apps.UID), values.getAsString(Apps.EXEC_UID), values.getAsString(Apps.EXEC_CMD)}; mDb.update(Apps.TABLE_NAME, values, where, whereArgs); Cursor c = mDb.query(Apps.TABLE_NAME, new String[] { Apps._ID }, where, whereArgs, null, null, null); if (c.moveToFirst()) { rowId = c.getLong(0); } c.close(); } if (values.getAsInteger(Apps.ALLOW) != Apps.AllowType.ASK) { ContentValues logValues = new ContentValues(); logValues.put(Logs.APP_ID, rowId); logValues.put(Logs.DATE, System.currentTimeMillis()); logValues.put(Logs.TYPE, Logs.LogType.CREATE); mDb.insert(Logs.TABLE_NAME, null, logValues); } Util.updatePermissionsDb(mContext); returnUri = ContentUris.withAppendedId(Apps.CONTENT_URI, rowId); break; case APP_ID_LOGS: case LOGS_APP_ID: // TODO: Check validity of incoming data before inserting it values.put(Logs.APP_ID, uri.getPathSegments().get(1)); rowId = mDb.insert(Logs.TABLE_NAME, null, values); returnUri = ContentUris.withAppendedId(Logs.CONTENT_URI, rowId); // Logs are special, they should also notify of a change to the uri // logs/app_id, and all apps uri getContext().getContentResolver().notifyChange( ContentUris.withAppendedId(Logs.CONTENT_URI, Long.parseLong(uri.getPathSegments().get(1))), null); getContext().getContentResolver().notifyChange(Apps.CONTENT_URI, null); break; default: throw new IllegalArgumentException("Unsupported URI: " + uri); } if (rowId > -1) { getContext().getContentResolver().notifyChange(returnUri, null); return returnUri; } throw new SQLException("Failed to insert row into " + uri); } @Override public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { if (!ensureDb()) return -1; int count = 0; boolean updatePermissionsDb = false; if (!values.containsKey(Apps.DIRTY)) { Log.d(TAG, "Row dirty, update permissions.sqlite"); values.put(Apps.DIRTY, "1"); updatePermissionsDb = true; } else { Log.d(TAG, "Row not dirty, don't update permissions.sqlite"); } switch (sUriMatcher.match(uri)) { case APPS: count = mDb.update(Apps.TABLE_NAME, values, selection, selectionArgs); break; case APP_ID: count = mDb.update(Apps.TABLE_NAME, values, Apps._ID + "=" + uri.getPathSegments().get(1) + (!TextUtils.isEmpty(selection)? " AND (" + selection + ")":""), selectionArgs); break; case APP_UID: count = mDb.update(Apps.TABLE_NAME, values, Apps.UID + "=" + uri.getPathSegments().get(2) + (!TextUtils.isEmpty(selection)? " AND (" + selection + ")":""), selectionArgs); break; default: throw new IllegalArgumentException("Unsupported URI: " + uri); } if (updatePermissionsDb) { Util.updatePermissionsDb(mContext); } getContext().getContentResolver().notifyChange(uri, null); return count; } @Override public int delete(Uri uri, String selection, String[] selectionArgs) { if (!ensureDb()) return -1; int count = 0; ContentValues deleteAppValues = new ContentValues(); deleteAppValues.put(Apps.ALLOW, Apps.AllowType.TO_DELETE); deleteAppValues.put(Apps.DIRTY, 1); switch (sUriMatcher.match(uri)) { case APPS: // Don't delete the app here, set it's allow column to -1 and // mark it as dirty. The PermissionsDbService will delete it count = mDb.update(Apps.TABLE_NAME, deleteAppValues, selection, selectionArgs); // Delete from the other DB too Util.updatePermissionsDb(mContext); break; case APP_ID: count = mDb.update(Apps.TABLE_NAME, deleteAppValues, Apps._ID + "=" + uri.getPathSegments().get(1) + (!TextUtils.isEmpty(selection)? " AND (" + selection + ")":""), selectionArgs); Util.updatePermissionsDb(mContext); // No break here so we can fall through and delete associated logs case APP_ID_LOGS: case LOGS_APP_ID: count += mDb.delete(Logs.TABLE_NAME, Logs.APP_ID + "=" + uri.getPathSegments().get(1) + (!TextUtils.isEmpty(selection)? " AND (" + selection + ")":""), selectionArgs); break; case APP_UID: // May remove this, I don't think I'm going to want to use it count = mDb.update(Apps.TABLE_NAME, deleteAppValues, Apps.UID + "=" + uri.getPathSegments().get(2) + (!TextUtils.isEmpty(selection)? " AND (" + selection + ")":""), selectionArgs); Util.updatePermissionsDb(mContext); break; case APP_CLEAN: count = mDb.delete(Apps.TABLE_NAME, selection, selectionArgs); break; case LOGS: count = mDb.delete(Logs.TABLE_NAME, selection, selectionArgs); break; default: throw new IllegalArgumentException("Unsupported URI: " + uri); } getContext().getContentResolver().notifyChange(uri, null); getContext().getContentResolver().notifyChange(Apps.CONTENT_URI, null); return count; } private boolean ensureDb() { if (mDb == null) { mDb = mDbHelper.getWritableDatabase(); if (mDb == null) return false; } return true; } private class SuDbOpenHelper extends SQLiteOpenHelper { private static final String DATABASE_NAME = "su.db"; private static final int DATABASE_VERSION = 4; SuDbOpenHelper(Context context) { super(context, DATABASE_NAME, null, DATABASE_VERSION); } @Override public void onCreate(SQLiteDatabase db) { db.execSQL(Apps.CREATE); db.execSQL(Logs.CREATE); } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { // Pattern for upgrade blocks // // if (upgradeVersion == [the DATABASE_VERSION you set] - 1) { // .. your upgrade logic .. // upgradeVersion = [the DATABASE_VERSION you set] // } int upgradeVersion = oldVersion; if (upgradeVersion == 1) { // Don't do anything here anymore, since we don't want to // open the permissions.sqlite database upgradeVersion = 2; } if (upgradeVersion == 2) { try { db.execSQL("ALTER TABLE apps ADD COLUMN dirty INTEGER"); } catch (SQLiteException e) { // We're getting this exception because the column already exists // for some reason... Log.e(TAG, "dirty column already exists... wut?", e); } // Set everything to dirty ContentValues values = new ContentValues(); values.put(Apps.DIRTY, "1"); db.update(Apps.TABLE_NAME, values, null, null); Util.updatePermissionsDb(mContext); upgradeVersion = 3; } if (upgradeVersion == 3) { Cursor c = db.query(Apps.TABLE_NAME, new String[] { Apps._ID, Apps.UID, Apps.NAME }, null, null, null, null, null); while (c.moveToNext()) { if (c.getString(2).equalsIgnoreCase("unknown")) { ContentValues values = new ContentValues(); values.put(Apps.NAME, Util.getAppName(mContext, c.getInt(1), false)); values.put(Apps.PACKAGE, Util.getAppPackage(mContext, c.getInt(1))); db.update(Apps.TABLE_NAME, values, Apps._ID + "=?", new String[] { String.valueOf(c.getLong(0)) }); } } c.close(); upgradeVersion = 4; } } } }