class AppProvider extends FDroidProvider {

    @Override
    public Cursor query(@NonNull Uri uri, String[] projection, String customSelection, String[] selectionArgs, String sortOrder) {
        AppQuerySelection selection = new AppQuerySelection(customSelection, selectionArgs);

        // Queries which are for the main list of apps should not include swap apps.
        boolean includeSwap = true;

        // It is usually the case that we ask for app(s) for which we don't care what repo is
        // responsible for providing them. In that case, we need to populate the metadata with
        // that form the repo with the highest priority.
        // Whenever we know which repo it is coming from, then it is important that we don't
        // delegate to the repo with the highest priority, but rather the specific repo we are
        // querying from.
        boolean repoIsKnown = false;

        int limit = 0;

        List<String> pathSegments = uri.getPathSegments();
        switch (MATCHER.match(uri)) {
            case CALC_PREFERRED_METADATA:
                updatePreferredMetadata();
                return null;

            case CODE_LIST:
                includeSwap = false;
                break;

            case CODE_SINGLE:
                long repoId = Long.parseLong(pathSegments.get(1));
                String packageName = pathSegments.get(2);
                selection = selection.add(querySingle(packageName, repoId));
                repoIsKnown = true;
                break;

            case CAN_UPDATE:
                selection = selection.add(queryCanUpdate());
                includeSwap = false;
                break;

            case REPO:
                selection = selection.add(queryRepo(Long.parseLong(uri.getLastPathSegment())));
                repoIsKnown = true;
                break;

            case INSTALLED:
                selection = selection.add(queryInstalled());
                sortOrder = Cols.NAME;
                includeSwap = false;
                break;

            case SEARCH_TEXT:
                selection = selection.add(querySearch(pathSegments.get(1)));
                includeSwap = false;
                break;

            case SEARCH_TEXT_AND_CATEGORIES:
                selection = selection
                        .add(querySearch(pathSegments.get(1)))
                        .add(queryCategory(pathSegments.get(2)));
                includeSwap = false;
                break;

            case SEARCH_REPO:
                selection = selection
                        .add(querySearch(pathSegments.get(2)))
                        .add(queryRepo(Long.parseLong(pathSegments.get(1))));
                repoIsKnown = true;
                break;

            case CATEGORY:
                selection = selection.add(queryCategory(uri.getLastPathSegment()));
                includeSwap = false;
                break;

            case TOP_FROM_CATEGORY:
                selection = selection.add(queryCategory(pathSegments.get(2)));
                limit = Integer.parseInt(pathSegments.get(1));
                sortOrder = getTableName() + "." + Cols.LAST_UPDATED + " DESC";
                includeSwap = false;
                break;

            case INSTALLED_WITH_KNOWN_VULNS:
                selection = selection.add(queryInstalledWithKnownVulns());
                includeSwap = false;
                break;

            case RECENTLY_UPDATED:
                String table = getTableName();
                String isNew = table + "." + Cols.LAST_UPDATED + " <= " + table + "." + Cols.ADDED + " DESC";
                String hasFeatureGraphic = table + "." + Cols.FEATURE_GRAPHIC + " IS NULL ASC ";
                String lastUpdated = table + "." + Cols.LAST_UPDATED + " DESC";
                sortOrder = lastUpdated + ", " + isNew + ", " + hasFeatureGraphic;

                // There seems no reason to limit the number of apps on the front page, but it helps
                // if it loads quickly, as it is the default view shown every time F-Droid is opened.
                // 200 is an arbitrary number which hopefully gives the user enough to scroll through
                // if they are bored.
                limit = 200;

                includeSwap = false;
                break;

            case HIGHEST_PRIORITY:
                selection = selection.add(queryPackageName(uri.getLastPathSegment()));
                includeSwap = false;
                break;

            default:
                Log.e(TAG, "Invalid URI for app content provider: " + uri);
                throw new UnsupportedOperationException("Invalid URI for app content provider: " + uri);
        }

        if (!repoIsKnown) {
            selection = selection.add(queryHighestPriority());
        }

        return runQuery(uri, selection, projection, includeSwap, sortOrder, limit);
    }

}