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