| /* |
| * 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. |
| * |
| * <p>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. |
| * |
| * <p>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<String> 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<String> 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() {} |
| } |