package org.fdroid.fdroid.data; import android.content.ContentValues; import android.content.Context; import android.content.UriMatcher; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.res.Resources; import android.database.Cursor; import android.net.Uri; import android.support.annotation.Nullable; import android.util.Log; import org.fdroid.fdroid.R; import org.fdroid.fdroid.Utils; import org.fdroid.fdroid.data.Schema.InstalledAppTable; import org.fdroid.fdroid.data.Schema.InstalledAppTable.Cols; import java.util.HashMap; import java.util.Map; public class InstalledAppProvider extends FDroidProvider { private static final String TAG = "InstalledAppProvider"; public static class Helper { /** * @return The keys are the package names, and their corresponding values are * the {@link PackageInfo#lastUpdateTime last update time} in milliseconds. */ public static Map<String, Long> all(Context context) { Map<String, Long> cachedInfo = new HashMap<>(); final Uri uri = InstalledAppProvider.getContentUri(); final String[] projection = Cols.ALL; Cursor cursor = context.getContentResolver().query(uri, projection, null, null, null); if (cursor != null) { if (cursor.getCount() > 0) { cursor.moveToFirst(); while (!cursor.isAfterLast()) { cachedInfo.put( cursor.getString(cursor.getColumnIndex(Cols.PACKAGE_NAME)), cursor.getLong(cursor.getColumnIndex(Cols.LAST_UPDATE_TIME)) ); cursor.moveToNext(); } } cursor.close(); } return cachedInfo; } @Nullable public static InstalledApp findByPackageName(Context context, String packageName) { Cursor cursor = context.getContentResolver().query(getAppUri(packageName), Cols.ALL, null, null, null); if (cursor == null) { return null; } try { if (cursor.getCount() == 0) { return null; } cursor.moveToFirst(); return new InstalledApp(cursor); } finally { cursor.close(); } } } private static final String PROVIDER_NAME = "InstalledAppProvider"; private static final String PATH_SEARCH = "search"; private static final int CODE_SEARCH = CODE_SINGLE + 1; private static final UriMatcher MATCHER = new UriMatcher(-1); static { MATCHER.addURI(getAuthority(), null, CODE_LIST); MATCHER.addURI(getAuthority(), PATH_SEARCH + "/*", CODE_SEARCH); MATCHER.addURI(getAuthority(), "*", CODE_SINGLE); } public static Uri getContentUri() { return Uri.parse("content://" + getAuthority()); } /** * @return the {@link Uri} that points to a specific installed app */ public static Uri getAppUri(String packageName) { return Uri.withAppendedPath(getContentUri(), packageName); } public static Uri getSearchUri(String keywords) { return getContentUri().buildUpon() .appendPath(PATH_SEARCH) .appendPath(keywords) .build(); } public static String getApplicationLabel(Context context, String packageName) { PackageManager pm = context.getPackageManager(); ApplicationInfo appInfo; try { appInfo = pm.getApplicationInfo(packageName, PackageManager.GET_META_DATA); return appInfo.loadLabel(pm).toString(); } catch (PackageManager.NameNotFoundException | Resources.NotFoundException e) { Utils.debugLog(TAG, "Could not get application label", e); } return packageName; // all else fails, return packageName } @Override protected String getTableName() { return InstalledAppTable.NAME; } @Override protected String getProviderName() { return "InstalledAppProvider"; } public static String getAuthority() { return AUTHORITY + "." + PROVIDER_NAME; } @Override protected UriMatcher getMatcher() { return MATCHER; } private QuerySelection queryApp(String packageName) { return new QuerySelection(Cols.PACKAGE_NAME + " = ?", new String[]{packageName}); } private QuerySelection querySearch(String query) { return new QuerySelection(Cols.APPLICATION_LABEL + " LIKE ?", new String[]{"%" + query + "%"}); } @Override public Cursor query(Uri uri, String[] projection, String customSelection, String[] selectionArgs, String sortOrder) { if (sortOrder == null) { sortOrder = Cols.APPLICATION_LABEL; } QuerySelection selection = new QuerySelection(customSelection, selectionArgs); switch (MATCHER.match(uri)) { case CODE_LIST: break; case CODE_SINGLE: selection = selection.add(queryApp(uri.getLastPathSegment())); break; case CODE_SEARCH: selection = selection.add(querySearch(uri.getLastPathSegment())); break; default: String message = "Invalid URI for installed app content provider: " + uri; Log.e(TAG, message); throw new UnsupportedOperationException(message); } Cursor cursor = db().query(getTableName(), projection, selection.getSelection(), selection.getArgs(), null, null, sortOrder); cursor.setNotificationUri(getContext().getContentResolver(), uri); return cursor; } @Override public int delete(Uri uri, String where, String[] whereArgs) { if (MATCHER.match(uri) != CODE_SINGLE) { throw new UnsupportedOperationException("Delete not supported for " + uri + "."); } QuerySelection query = new QuerySelection(where, whereArgs); query = query.add(queryApp(uri.getLastPathSegment())); return db().delete(getTableName(), query.getSelection(), query.getArgs()); } @Override public Uri insert(Uri uri, ContentValues values) { if (MATCHER.match(uri) != CODE_LIST) { throw new UnsupportedOperationException("Insert not supported for " + uri + "."); } verifyVersionNameNotNull(values); db().replaceOrThrow(getTableName(), null, values); return getAppUri(values.getAsString(Cols.PACKAGE_NAME)); } /** * Update is not supported for {@code InstalledAppProvider}. Instead, use * {@link #insert(Uri, ContentValues)}, and it will overwrite the relevant * row, if one exists. This just throws {@link UnsupportedOperationException} */ @Override public int update(Uri uri, ContentValues values, String where, String[] whereArgs) { throw new UnsupportedOperationException("\"Update' not supported for installed appp provider. Instead, you should insert, and it will overwrite the relevant rows if one exists."); } /** * During development, I stumbled across one (out of over 300) installed apps which had a versionName * of null. As such, I figured we may as well store it as "Unknown". The alternative is to allow the * column to accept NULL values in the database, and then deal with the potential of a null everywhere * "versionName" is used. */ private void verifyVersionNameNotNull(ContentValues values) { if (values.containsKey(Cols.VERSION_NAME) && values.getAsString(Cols.VERSION_NAME) == null) { values.put(Cols.VERSION_NAME, getContext().getString(R.string.unknown)); } } }