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.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 if (!canCreateColumn) { 71 return false; 72 } 73 return (Utils.isRecordedProgramsUri(uri) 74 && checkRecordedProgramTableSeriesIdColumn(context, uri)) 75 || (Utils.isProgramsUri(uri) && checkProgramTableSeriesIdColumn(context, uri)); 76 } 77 78 @WorkerThread checkProgramTableSeriesIdColumn(Context context, Uri uri)79 private static synchronized boolean checkProgramTableSeriesIdColumn(Context context, Uri uri) { 80 if (!sProgramHasSeriesIdColumn) { 81 if (getExistingColumns(context, uri).contains(EXTRA_PROGRAM_COLUMN_SERIES_ID)) { 82 sProgramHasSeriesIdColumn = true; 83 } else if (addColumnToTable(context, uri, EXTRA_PROGRAM_COLUMN_SERIES_ID)) { 84 sProgramHasSeriesIdColumn = true; 85 } 86 } 87 return sProgramHasSeriesIdColumn; 88 } 89 90 @WorkerThread checkRecordedProgramTableSeriesIdColumn( Context context, Uri uri)91 private static synchronized boolean checkRecordedProgramTableSeriesIdColumn( 92 Context context, Uri uri) { 93 if (!sRecordedProgramHasSeriesIdColumn) { 94 if (getExistingColumns(context, uri).contains(EXTRA_PROGRAM_COLUMN_SERIES_ID)) { 95 sRecordedProgramHasSeriesIdColumn = true; 96 } else if (addColumnToTable(context, uri, EXTRA_PROGRAM_COLUMN_SERIES_ID)) { 97 sRecordedProgramHasSeriesIdColumn = true; 98 } 99 } 100 return sRecordedProgramHasSeriesIdColumn; 101 } 102 103 /** 104 * Checks whether a table contains a state column. 105 * 106 * <p>This method is different from {@link #getRecordedProgramHasStateColumn()} because it may 107 * access to database, so it should be run in worker thread. 108 * 109 * @return {@code true} if the corresponding table contains a state column; {@code false} 110 * otherwise. 111 */ 112 @WorkerThread checkStateColumn(Context context, Uri uri)113 public static synchronized boolean checkStateColumn(Context context, Uri uri) { 114 boolean canCreateColumn = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O); 115 if (!canCreateColumn) { 116 return false; 117 } 118 return (Utils.isRecordedProgramsUri(uri) 119 && checkRecordedProgramTableStateColumn(context, uri)); 120 } 121 122 @WorkerThread checkRecordedProgramTableStateColumn( Context context, Uri uri)123 private static synchronized boolean checkRecordedProgramTableStateColumn( 124 Context context, Uri uri) { 125 if (!sRecordedProgramHasStateColumn) { 126 if (getExistingColumns(context, uri).contains(EXTRA_PROGRAM_COLUMN_STATE)) { 127 sRecordedProgramHasStateColumn = true; 128 } else if (addColumnToTable(context, uri, EXTRA_PROGRAM_COLUMN_STATE)) { 129 sRecordedProgramHasStateColumn = true; 130 } 131 } 132 return sRecordedProgramHasStateColumn; 133 } 134 getProgramHasSeriesIdColumn()135 public static synchronized boolean getProgramHasSeriesIdColumn() { 136 return TRUE.equals(sProgramHasSeriesIdColumn); 137 } 138 getRecordedProgramHasSeriesIdColumn()139 public static synchronized boolean getRecordedProgramHasSeriesIdColumn() { 140 return TRUE.equals(sRecordedProgramHasSeriesIdColumn); 141 } 142 getRecordedProgramHasStateColumn()143 public static synchronized boolean getRecordedProgramHasStateColumn() { 144 return TRUE.equals(sRecordedProgramHasStateColumn); 145 } 146 addExtraColumnsToProjection(String[] projection, @TvProviderExtraColumn String column)147 public static String[] addExtraColumnsToProjection(String[] projection, 148 @TvProviderExtraColumn String column) { 149 List<String> projectionList = new ArrayList<>(Arrays.asList(projection)); 150 if (!projectionList.contains(column)) { 151 projectionList.add(column); 152 } 153 projection = projectionList.toArray(projection); 154 return projection; 155 } 156 157 /** 158 * Gets column names of a table 159 * 160 * @param uri the corresponding URI of the table 161 */ 162 @VisibleForTesting getExistingColumns(Context context, Uri uri)163 static Set<String> getExistingColumns(Context context, Uri uri) { 164 Bundle result = null; 165 try { 166 result = 167 context.getContentResolver() 168 .call(uri, TvContract.METHOD_GET_COLUMNS, uri.toString(), null); 169 } catch (Exception e) { 170 Log.e(TAG, "Error trying to get existing columns.", e); 171 } 172 if (result != null) { 173 String[] columns = result.getStringArray(TvContract.EXTRA_EXISTING_COLUMN_NAMES); 174 if (columns != null) { 175 return new HashSet<>(Arrays.asList(columns)); 176 } 177 } 178 Log.e(TAG, "Query existing column names from " + uri + " returned null"); 179 return Collections.emptySet(); 180 } 181 182 /** 183 * Add a column to the table 184 * 185 * @return {@code true} if the column is added successfully; {@code false} otherwise. 186 */ addColumnToTable(Context context, Uri contentUri, String columnName)187 private static boolean addColumnToTable(Context context, Uri contentUri, String columnName) { 188 Bundle extra = new Bundle(); 189 extra.putCharSequence(TvContract.EXTRA_COLUMN_NAME, columnName); 190 extra.putCharSequence(TvContract.EXTRA_DATA_TYPE, "TEXT"); 191 // If the add operation fails, the following just returns null without crashing. 192 Bundle allColumns = null; 193 try { 194 allColumns = 195 context.getContentResolver() 196 .call( 197 contentUri, 198 TvContract.METHOD_ADD_COLUMN, 199 contentUri.toString(), 200 extra); 201 } catch (Exception e) { 202 Log.e(TAG, "Error trying to add column.", e); 203 } 204 if (allColumns == null) { 205 Log.w(TAG, "Adding new column failed. Uri=" + contentUri); 206 } 207 return allColumns != null; 208 } 209 TvProviderUtils()210 private TvProviderUtils() {} 211 } 212