/* * Copyright (C) 2018 The Android Open Source Project * * 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.android.tv.util; import static java.lang.Boolean.TRUE; import android.content.Context; import android.media.tv.TvContract; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.support.annotation.StringDef; import android.support.annotation.VisibleForTesting; import android.support.annotation.WorkerThread; import android.util.Log; import com.android.tv.data.api.BaseProgram; import com.android.tv.features.PartnerFeatures; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; /** A utility class related to TvProvider. */ public final class TvProviderUtils { private static final String TAG = "TvProviderUtils"; public static final String EXTRA_PROGRAM_COLUMN_SERIES_ID = BaseProgram.COLUMN_SERIES_ID; public static final String EXTRA_PROGRAM_COLUMN_STATE = BaseProgram.COLUMN_STATE; /** Possible extra columns in TV provider. */ @Retention(RetentionPolicy.SOURCE) @StringDef({EXTRA_PROGRAM_COLUMN_SERIES_ID, EXTRA_PROGRAM_COLUMN_STATE}) public @interface TvProviderExtraColumn {} private static boolean sProgramHasSeriesIdColumn; private static boolean sRecordedProgramHasSeriesIdColumn; private static boolean sRecordedProgramHasStateColumn; /** * Checks whether a table contains a series ID column. * *

This method is different from {@link #getProgramHasSeriesIdColumn()} and {@link * #getRecordedProgramHasSeriesIdColumn()} because it may access to database, so it should be * run in worker thread. * * @return {@code true} if the corresponding table contains a series ID column; {@code false} * otherwise. */ @WorkerThread public static synchronized boolean checkSeriesIdColumn(Context context, Uri uri) { boolean canCreateColumn = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O); canCreateColumn = (canCreateColumn || PartnerFeatures.TVPROVIDER_ALLOWS_COLUMN_CREATION.isEnabled(context)); if (!canCreateColumn) { return false; } return (Utils.isRecordedProgramsUri(uri) && checkRecordedProgramTableSeriesIdColumn(context, uri)) || (Utils.isProgramsUri(uri) && checkProgramTableSeriesIdColumn(context, uri)); } @WorkerThread private static synchronized boolean checkProgramTableSeriesIdColumn(Context context, Uri uri) { if (!sProgramHasSeriesIdColumn) { if (getExistingColumns(context, uri).contains(EXTRA_PROGRAM_COLUMN_SERIES_ID)) { sProgramHasSeriesIdColumn = true; } else if (addColumnToTable(context, uri, EXTRA_PROGRAM_COLUMN_SERIES_ID)) { sProgramHasSeriesIdColumn = true; } } return sProgramHasSeriesIdColumn; } @WorkerThread private static synchronized boolean checkRecordedProgramTableSeriesIdColumn( Context context, Uri uri) { if (!sRecordedProgramHasSeriesIdColumn) { if (getExistingColumns(context, uri).contains(EXTRA_PROGRAM_COLUMN_SERIES_ID)) { sRecordedProgramHasSeriesIdColumn = true; } else if (addColumnToTable(context, uri, EXTRA_PROGRAM_COLUMN_SERIES_ID)) { sRecordedProgramHasSeriesIdColumn = true; } } return sRecordedProgramHasSeriesIdColumn; } /** * Checks whether a table contains a state column. * *

This method is different from {@link #getRecordedProgramHasStateColumn()} because it may * access to database, so it should be run in worker thread. * * @return {@code true} if the corresponding table contains a state column; {@code false} * otherwise. */ @WorkerThread public static synchronized boolean checkStateColumn(Context context, Uri uri) { boolean canCreateColumn = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O); canCreateColumn = (canCreateColumn || PartnerFeatures.TVPROVIDER_ALLOWS_COLUMN_CREATION.isEnabled(context)); if (!canCreateColumn) { return false; } return (Utils.isRecordedProgramsUri(uri) && checkRecordedProgramTableStateColumn(context, uri)); } @WorkerThread private static synchronized boolean checkRecordedProgramTableStateColumn( Context context, Uri uri) { if (!sRecordedProgramHasStateColumn) { if (getExistingColumns(context, uri).contains(EXTRA_PROGRAM_COLUMN_STATE)) { sRecordedProgramHasStateColumn = true; } else if (addColumnToTable(context, uri, EXTRA_PROGRAM_COLUMN_STATE)) { sRecordedProgramHasStateColumn = true; } } return sRecordedProgramHasStateColumn; } public static synchronized boolean getProgramHasSeriesIdColumn() { return TRUE.equals(sProgramHasSeriesIdColumn); } public static synchronized boolean getRecordedProgramHasSeriesIdColumn() { return TRUE.equals(sRecordedProgramHasSeriesIdColumn); } public static synchronized boolean getRecordedProgramHasStateColumn() { return TRUE.equals(sRecordedProgramHasStateColumn); } public static String[] addExtraColumnsToProjection( String[] projection, @TvProviderExtraColumn String column) { List projectionList = new ArrayList<>(Arrays.asList(projection)); if (!projectionList.contains(column)) { projectionList.add(column); } projection = projectionList.toArray(projection); return projection; } /** * Gets column names of a table * * @param uri the corresponding URI of the table */ @VisibleForTesting static Set getExistingColumns(Context context, Uri uri) { Bundle result = null; try { result = context.getContentResolver() .call(uri, TvContract.METHOD_GET_COLUMNS, uri.toString(), null); } catch (Exception e) { Log.e(TAG, "Error trying to get existing columns.", e); } if (result != null) { String[] columns = result.getStringArray(TvContract.EXTRA_EXISTING_COLUMN_NAMES); if (columns != null) { return new HashSet<>(Arrays.asList(columns)); } } Log.e(TAG, "Query existing column names from " + uri + " returned null"); return Collections.emptySet(); } /** * Add a column to the table * * @return {@code true} if the column is added successfully; {@code false} otherwise. */ private static boolean addColumnToTable(Context context, Uri contentUri, String columnName) { Bundle extra = new Bundle(); extra.putCharSequence(TvContract.EXTRA_COLUMN_NAME, columnName); extra.putCharSequence(TvContract.EXTRA_DATA_TYPE, "TEXT"); // If the add operation fails, the following just returns null without crashing. Bundle allColumns = null; try { allColumns = context.getContentResolver() .call( contentUri, TvContract.METHOD_ADD_COLUMN, contentUri.toString(), extra); } catch (Exception e) { Log.e(TAG, "Error trying to add column.", e); } if (allColumns == null) { Log.w(TAG, "Adding new column failed. Uri=" + contentUri); } return allColumns != null; } private TvProviderUtils() {} }