1 /* 2 * Copyright (C) 2018 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.tv.util; 18 19 import static java.lang.Boolean.TRUE; 20 21 import android.content.Context; 22 import android.media.tv.TvContract; 23 import android.net.Uri; 24 import android.os.Build; 25 import android.os.Bundle; 26 import android.support.annotation.StringDef; 27 import android.support.annotation.VisibleForTesting; 28 import android.support.annotation.WorkerThread; 29 import android.util.Log; 30 import com.android.tv.data.api.BaseProgram; 31 import com.android.tv.features.PartnerFeatures; 32 import java.lang.annotation.Retention; 33 import java.lang.annotation.RetentionPolicy; 34 import java.util.ArrayList; 35 import java.util.Arrays; 36 import java.util.Collections; 37 import java.util.HashSet; 38 import java.util.List; 39 import java.util.Set; 40 41 /** A utility class related to TvProvider. */ 42 public final class TvProviderUtils { 43 private static final String TAG = "TvProviderUtils"; 44 45 public static final String EXTRA_PROGRAM_COLUMN_SERIES_ID = BaseProgram.COLUMN_SERIES_ID; 46 public static final String EXTRA_PROGRAM_COLUMN_STATE = BaseProgram.COLUMN_STATE; 47 48 /** Possible extra columns in TV provider. */ 49 @Retention(RetentionPolicy.SOURCE) 50 @StringDef({EXTRA_PROGRAM_COLUMN_SERIES_ID, EXTRA_PROGRAM_COLUMN_STATE}) 51 public @interface TvProviderExtraColumn {} 52 53 private static boolean sProgramHasSeriesIdColumn; 54 private static boolean sRecordedProgramHasSeriesIdColumn; 55 private static boolean sRecordedProgramHasStateColumn; 56 57 /** 58 * Checks whether a table contains a series ID column. 59 * 60 * <p>This method is different from {@link #getProgramHasSeriesIdColumn()} and {@link 61 * #getRecordedProgramHasSeriesIdColumn()} because it may access to database, so it should be 62 * run in worker thread. 63 * 64 * @return {@code true} if the corresponding table contains a series ID column; {@code false} 65 * otherwise. 66 */ 67 @WorkerThread checkSeriesIdColumn(Context context, Uri uri)68 public static synchronized boolean checkSeriesIdColumn(Context context, Uri uri) { 69 boolean canCreateColumn = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O); 70 canCreateColumn = 71 (canCreateColumn 72 || PartnerFeatures.TVPROVIDER_ALLOWS_COLUMN_CREATION.isEnabled(context)); 73 if (!canCreateColumn) { 74 return false; 75 } 76 return (Utils.isRecordedProgramsUri(uri) 77 && checkRecordedProgramTableSeriesIdColumn(context, uri)) 78 || (Utils.isProgramsUri(uri) && checkProgramTableSeriesIdColumn(context, uri)); 79 } 80 81 @WorkerThread checkProgramTableSeriesIdColumn(Context context, Uri uri)82 private static synchronized boolean checkProgramTableSeriesIdColumn(Context context, Uri uri) { 83 if (!sProgramHasSeriesIdColumn) { 84 if (getExistingColumns(context, uri).contains(EXTRA_PROGRAM_COLUMN_SERIES_ID)) { 85 sProgramHasSeriesIdColumn = true; 86 } else if (addColumnToTable(context, uri, EXTRA_PROGRAM_COLUMN_SERIES_ID)) { 87 sProgramHasSeriesIdColumn = true; 88 } 89 } 90 return sProgramHasSeriesIdColumn; 91 } 92 93 @WorkerThread checkRecordedProgramTableSeriesIdColumn( Context context, Uri uri)94 private static synchronized boolean checkRecordedProgramTableSeriesIdColumn( 95 Context context, Uri uri) { 96 if (!sRecordedProgramHasSeriesIdColumn) { 97 if (getExistingColumns(context, uri).contains(EXTRA_PROGRAM_COLUMN_SERIES_ID)) { 98 sRecordedProgramHasSeriesIdColumn = true; 99 } else if (addColumnToTable(context, uri, EXTRA_PROGRAM_COLUMN_SERIES_ID)) { 100 sRecordedProgramHasSeriesIdColumn = true; 101 } 102 } 103 return sRecordedProgramHasSeriesIdColumn; 104 } 105 106 /** 107 * Checks whether a table contains a state column. 108 * 109 * <p>This method is different from {@link #getRecordedProgramHasStateColumn()} because it may 110 * access to database, so it should be run in worker thread. 111 * 112 * @return {@code true} if the corresponding table contains a state column; {@code false} 113 * otherwise. 114 */ 115 @WorkerThread checkStateColumn(Context context, Uri uri)116 public static synchronized boolean checkStateColumn(Context context, Uri uri) { 117 boolean canCreateColumn = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O); 118 canCreateColumn = 119 (canCreateColumn 120 || PartnerFeatures.TVPROVIDER_ALLOWS_COLUMN_CREATION.isEnabled(context)); 121 if (!canCreateColumn) { 122 return false; 123 } 124 return (Utils.isRecordedProgramsUri(uri) 125 && checkRecordedProgramTableStateColumn(context, uri)); 126 } 127 128 @WorkerThread checkRecordedProgramTableStateColumn( Context context, Uri uri)129 private static synchronized boolean checkRecordedProgramTableStateColumn( 130 Context context, Uri uri) { 131 if (!sRecordedProgramHasStateColumn) { 132 if (getExistingColumns(context, uri).contains(EXTRA_PROGRAM_COLUMN_STATE)) { 133 sRecordedProgramHasStateColumn = true; 134 } else if (addColumnToTable(context, uri, EXTRA_PROGRAM_COLUMN_STATE)) { 135 sRecordedProgramHasStateColumn = true; 136 } 137 } 138 return sRecordedProgramHasStateColumn; 139 } 140 getProgramHasSeriesIdColumn()141 public static synchronized boolean getProgramHasSeriesIdColumn() { 142 return TRUE.equals(sProgramHasSeriesIdColumn); 143 } 144 getRecordedProgramHasSeriesIdColumn()145 public static synchronized boolean getRecordedProgramHasSeriesIdColumn() { 146 return TRUE.equals(sRecordedProgramHasSeriesIdColumn); 147 } 148 getRecordedProgramHasStateColumn()149 public static synchronized boolean getRecordedProgramHasStateColumn() { 150 return TRUE.equals(sRecordedProgramHasStateColumn); 151 } 152 addExtraColumnsToProjection( String[] projection, @TvProviderExtraColumn String column)153 public static String[] addExtraColumnsToProjection( 154 String[] projection, @TvProviderExtraColumn String column) { 155 List<String> projectionList = new ArrayList<>(Arrays.asList(projection)); 156 if (!projectionList.contains(column)) { 157 projectionList.add(column); 158 } 159 projection = projectionList.toArray(projection); 160 return projection; 161 } 162 163 /** 164 * Gets column names of a table 165 * 166 * @param uri the corresponding URI of the table 167 */ 168 @VisibleForTesting getExistingColumns(Context context, Uri uri)169 static Set<String> getExistingColumns(Context context, Uri uri) { 170 Bundle result = null; 171 try { 172 result = 173 context.getContentResolver() 174 .call(uri, TvContract.METHOD_GET_COLUMNS, uri.toString(), null); 175 } catch (Exception e) { 176 Log.e(TAG, "Error trying to get existing columns.", e); 177 } 178 if (result != null) { 179 String[] columns = result.getStringArray(TvContract.EXTRA_EXISTING_COLUMN_NAMES); 180 if (columns != null) { 181 return new HashSet<>(Arrays.asList(columns)); 182 } 183 } 184 Log.e(TAG, "Query existing column names from " + uri + " returned null"); 185 return Collections.emptySet(); 186 } 187 188 /** 189 * Add a column to the table 190 * 191 * @return {@code true} if the column is added successfully; {@code false} otherwise. 192 */ addColumnToTable(Context context, Uri contentUri, String columnName)193 private static boolean addColumnToTable(Context context, Uri contentUri, String columnName) { 194 Bundle extra = new Bundle(); 195 extra.putCharSequence(TvContract.EXTRA_COLUMN_NAME, columnName); 196 extra.putCharSequence(TvContract.EXTRA_DATA_TYPE, "TEXT"); 197 // If the add operation fails, the following just returns null without crashing. 198 Bundle allColumns = null; 199 try { 200 allColumns = 201 context.getContentResolver() 202 .call( 203 contentUri, 204 TvContract.METHOD_ADD_COLUMN, 205 contentUri.toString(), 206 extra); 207 } catch (Exception e) { 208 Log.e(TAG, "Error trying to add column.", e); 209 } 210 if (allColumns == null) { 211 Log.w(TAG, "Adding new column failed. Uri=" + contentUri); 212 } 213 return allColumns != null; 214 } 215 TvProviderUtils()216 private TvProviderUtils() {} 217 } 218