1 /* 2 * Copyright 2015 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.example.android.sampletvinput; 18 19 import android.content.ContentResolver; 20 import android.content.ContentValues; 21 import android.content.Context; 22 import android.database.Cursor; 23 import android.media.tv.TvContentRating; 24 import android.media.tv.TvContract; 25 import android.media.tv.TvContract.Channels; 26 import android.media.tv.TvContract.Programs; 27 import android.media.tv.TvInputInfo; 28 import android.media.tv.TvInputManager; 29 import android.net.Uri; 30 import android.os.AsyncTask; 31 import android.text.TextUtils; 32 import android.util.Log; 33 import android.util.LongSparseArray; 34 import android.util.Pair; 35 import android.util.SparseArray; 36 37 import com.example.android.sampletvinput.rich.RichTvInputService.ChannelInfo; 38 import com.example.android.sampletvinput.rich.RichTvInputService.PlaybackInfo; 39 40 import java.io.IOException; 41 import java.io.InputStream; 42 import java.io.OutputStream; 43 import java.net.MalformedURLException; 44 import java.net.URL; 45 import java.util.ArrayList; 46 import java.util.HashMap; 47 import java.util.List; 48 import java.util.Map; 49 50 /** 51 * Static helper methods for working with {@link android.media.tv.TvContract}. 52 */ 53 public class TvContractUtils { 54 private static final String TAG = "TvContractUtils"; 55 private static final boolean DEBUG = true; 56 57 private static final SparseArray<String> VIDEO_HEIGHT_TO_FORMAT_MAP = 58 new SparseArray<String>(); 59 60 static { 61 VIDEO_HEIGHT_TO_FORMAT_MAP.put(480, TvContract.Channels.VIDEO_FORMAT_480P); 62 VIDEO_HEIGHT_TO_FORMAT_MAP.put(576, TvContract.Channels.VIDEO_FORMAT_576P); 63 VIDEO_HEIGHT_TO_FORMAT_MAP.put(720, TvContract.Channels.VIDEO_FORMAT_720P); 64 VIDEO_HEIGHT_TO_FORMAT_MAP.put(1080, TvContract.Channels.VIDEO_FORMAT_1080P); 65 VIDEO_HEIGHT_TO_FORMAT_MAP.put(2160, TvContract.Channels.VIDEO_FORMAT_2160P); 66 VIDEO_HEIGHT_TO_FORMAT_MAP.put(4320, TvContract.Channels.VIDEO_FORMAT_4320P); 67 } 68 69 public static void updateChannels( 70 Context context, String inputId, List<ChannelInfo> channels) { 71 // Create a map from original network ID to channel row ID for existing channels. 72 SparseArray<Long> mExistingChannelsMap = new SparseArray<Long>(); 73 Uri channelsUri = TvContract.buildChannelsUriForInput(inputId); 74 String[] projection = {Channels._ID, Channels.COLUMN_ORIGINAL_NETWORK_ID}; 75 Cursor cursor = null; 76 ContentResolver resolver = context.getContentResolver(); 77 try { 78 cursor = resolver.query(channelsUri, projection, null, null, null); 79 while (cursor != null && cursor.moveToNext()) { 80 long rowId = cursor.getLong(0); 81 int originalNetworkId = cursor.getInt(1); 82 mExistingChannelsMap.put(originalNetworkId, rowId); 83 } 84 } finally { 85 if (cursor != null) { 86 cursor.close(); 87 } 88 } 89 90 // If a channel exists, update it. If not, insert a new one. 91 ContentValues values = new ContentValues(); 92 values.put(Channels.COLUMN_INPUT_ID, inputId); 93 Map<Uri, String> logos = new HashMap<Uri, String>(); 94 for (ChannelInfo channel : channels) { 95 values.put(Channels.COLUMN_DISPLAY_NUMBER, channel.number); 96 values.put(Channels.COLUMN_DISPLAY_NAME, channel.name); 97 values.put(Channels.COLUMN_ORIGINAL_NETWORK_ID, channel.originalNetworkId); 98 values.put(Channels.COLUMN_TRANSPORT_STREAM_ID, channel.transportStreamId); 99 values.put(Channels.COLUMN_SERVICE_ID, channel.serviceId); 100 String videoFormat = getVideoFormat(channel.videoHeight); 101 if (videoFormat != null) { 102 values.put(Channels.COLUMN_VIDEO_FORMAT, videoFormat); 103 } else { 104 values.putNull(Channels.COLUMN_VIDEO_FORMAT); 105 } 106 Long rowId = mExistingChannelsMap.get(channel.originalNetworkId); 107 Uri uri; 108 if (rowId == null) { 109 uri = resolver.insert(TvContract.Channels.CONTENT_URI, values); 110 } else { 111 uri = TvContract.buildChannelUri(rowId); 112 resolver.update(uri, values, null, null); 113 mExistingChannelsMap.remove(channel.originalNetworkId); 114 } 115 if (!TextUtils.isEmpty(channel.logoUrl)) { 116 logos.put(TvContract.buildChannelLogoUri(uri), channel.logoUrl); 117 } 118 } 119 if (!logos.isEmpty()) { 120 new InsertLogosTask(context).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, logos); 121 } 122 123 // Deletes channels which don't exist in the new feed. 124 int size = mExistingChannelsMap.size(); 125 for(int i = 0; i < size; ++i) { 126 Long rowId = mExistingChannelsMap.valueAt(i); 127 resolver.delete(TvContract.buildChannelUri(rowId), null, null); 128 } 129 } 130 131 public static int getChannelCount(ContentResolver resolver, String inputId) { 132 Uri uri = TvContract.buildChannelsUriForInput(inputId); 133 String[] projection = { TvContract.Channels._ID }; 134 135 Cursor cursor = null; 136 try { 137 cursor = resolver.query(uri, projection, null, null, null); 138 if (cursor != null) { 139 return cursor.getCount(); 140 } 141 } finally { 142 if (cursor != null) { 143 cursor.close(); 144 } 145 } 146 return 0; 147 } 148 149 private static String getVideoFormat(int videoHeight) { 150 return VIDEO_HEIGHT_TO_FORMAT_MAP.get(videoHeight); 151 } 152 153 public static LongSparseArray<ChannelInfo> buildChannelMap(ContentResolver resolver, 154 String inputId, List<ChannelInfo> channels) { 155 Uri uri = TvContract.buildChannelsUriForInput(inputId); 156 String[] projection = { 157 TvContract.Channels._ID, 158 TvContract.Channels.COLUMN_DISPLAY_NUMBER 159 }; 160 161 LongSparseArray<ChannelInfo> channelMap = new LongSparseArray<>(); 162 Cursor cursor = null; 163 try { 164 cursor = resolver.query(uri, projection, null, null, null); 165 if (cursor == null || cursor.getCount() == 0) { 166 return null; 167 } 168 169 while (cursor.moveToNext()) { 170 long channelId = cursor.getLong(0); 171 String channelNumber = cursor.getString(1); 172 channelMap.put(channelId, getChannelByNumber(channelNumber, channels)); 173 } 174 } catch (Exception e) { 175 Log.d(TAG, "Content provider query: " + e.getStackTrace()); 176 return null; 177 } finally { 178 if (cursor != null) { 179 cursor.close(); 180 } 181 } 182 return channelMap; 183 } 184 185 public static long getLastProgramEndTimeMillis(ContentResolver resolver, Uri channelUri) { 186 Uri uri = TvContract.buildProgramsUriForChannel(channelUri); 187 String[] projection = {Programs.COLUMN_END_TIME_UTC_MILLIS}; 188 Cursor cursor = null; 189 try { 190 // TvProvider returns programs chronological order by default. 191 cursor = resolver.query(uri, projection, null, null, null); 192 if (cursor == null || cursor.getCount() == 0) { 193 return 0; 194 } 195 cursor.moveToLast(); 196 return cursor.getLong(0); 197 } catch (Exception e) { 198 Log.w(TAG, "Unable to get last program end time for " + channelUri, e); 199 } finally { 200 if (cursor != null) { 201 cursor.close(); 202 } 203 } 204 return 0; 205 } 206 207 public static List<PlaybackInfo> getProgramPlaybackInfo( 208 ContentResolver resolver, Uri channelUri, long startTimeMs, long endTimeMs, 209 int maxProgramInReturn) { 210 Uri uri = TvContract.buildProgramsUriForChannel(channelUri, startTimeMs, 211 endTimeMs); 212 String[] projection = { Programs.COLUMN_START_TIME_UTC_MILLIS, 213 Programs.COLUMN_END_TIME_UTC_MILLIS, 214 Programs.COLUMN_CONTENT_RATING, 215 Programs.COLUMN_INTERNAL_PROVIDER_DATA }; 216 Cursor cursor = null; 217 List<PlaybackInfo> list = new ArrayList<>(); 218 try { 219 cursor = resolver.query(uri, projection, null, null, null); 220 while (cursor.moveToNext()) { 221 long startMs = cursor.getLong(0); 222 long endMs = cursor.getLong(1); 223 TvContentRating[] ratings = stringToContentRatings(cursor.getString(2)); 224 Pair<Integer, String> values = parseInternalProviderData(cursor.getString(3)); 225 int videoType = values.first; 226 String videoUrl = values.second; 227 list.add(new PlaybackInfo(startMs, endMs, videoUrl, videoType, 228 ratings)); 229 if (list.size() > maxProgramInReturn) { 230 break; 231 } 232 } 233 } catch (Exception e) { 234 Log.e(TAG, "Failed to get program playback info from TvProvider.", e); 235 } finally { 236 if (cursor != null) { 237 cursor.close(); 238 } 239 } 240 return list; 241 } 242 243 public static String convertVideoInfoToInternalProviderData(int videotype, String videoUrl) { 244 return videotype + "," + videoUrl; 245 } 246 247 public static Pair<Integer, String> parseInternalProviderData(String internalData) { 248 String[] values = internalData.split(",", 2); 249 if (values.length != 2) { 250 throw new IllegalArgumentException(internalData); 251 } 252 return new Pair<>(Integer.parseInt(values[0]), values[1]); 253 } 254 255 public static void insertUrl(Context context, Uri contentUri, URL sourceUrl) { 256 if (DEBUG) { 257 Log.d(TAG, "Inserting " + sourceUrl + " to " + contentUri); 258 } 259 InputStream is = null; 260 OutputStream os = null; 261 try { 262 is = sourceUrl.openStream(); 263 os = context.getContentResolver().openOutputStream(contentUri); 264 copy(is, os); 265 } catch (IOException ioe) { 266 Log.e(TAG, "Failed to write " + sourceUrl + " to " + contentUri, ioe); 267 } finally { 268 if (is != null) { 269 try { 270 is.close(); 271 } catch (IOException e) { 272 // Ignore exception. 273 } 274 } 275 if (os != null) { 276 try { 277 os.close(); 278 } catch (IOException e) { 279 // Ignore exception. 280 } 281 } 282 } 283 } 284 285 public static void copy(InputStream is, OutputStream os) throws IOException { 286 byte[] buffer = new byte[1024]; 287 int len; 288 while ((len = is.read(buffer)) != -1) { 289 os.write(buffer, 0, len); 290 } 291 } 292 293 public static String getServiceNameFromInputId(Context context, String inputId) { 294 TvInputManager tim = (TvInputManager) context.getSystemService(Context.TV_INPUT_SERVICE); 295 for (TvInputInfo info : tim.getTvInputList()) { 296 if (info.getId().equals(inputId)) { 297 return info.getServiceInfo().name; 298 } 299 } 300 return null; 301 } 302 303 public static TvContentRating[] stringToContentRatings(String commaSeparatedRatings) { 304 if (TextUtils.isEmpty(commaSeparatedRatings)) { 305 return null; 306 } 307 String[] ratings = commaSeparatedRatings.split("\\s*,\\s*"); 308 TvContentRating[] contentRatings = new TvContentRating[ratings.length]; 309 for (int i = 0; i < contentRatings.length; ++i) { 310 contentRatings[i] = TvContentRating.unflattenFromString(ratings[i]); 311 } 312 return contentRatings; 313 } 314 315 public static String contentRatingsToString(TvContentRating[] contentRatings) { 316 if (contentRatings == null || contentRatings.length == 0) { 317 return null; 318 } 319 final String DELIMITER = ","; 320 StringBuilder ratings = new StringBuilder(contentRatings[0].flattenToString()); 321 for (int i = 1; i < contentRatings.length; ++i) { 322 ratings.append(DELIMITER); 323 ratings.append(contentRatings[i].flattenToString()); 324 } 325 return ratings.toString(); 326 } 327 328 private static ChannelInfo getChannelByNumber(String channelNumber, 329 List<ChannelInfo> channels) { 330 for (ChannelInfo info : channels) { 331 if (info.number.equals(channelNumber)) { 332 return info; 333 } 334 } 335 throw new IllegalArgumentException("Unknown channel: " + channelNumber); 336 } 337 338 private TvContractUtils() {} 339 340 public static class InsertLogosTask extends AsyncTask<Map<Uri, String>, Void, Void> { 341 private final Context mContext; 342 343 InsertLogosTask(Context context) { 344 mContext = context; 345 } 346 347 @Override 348 public Void doInBackground(Map<Uri, String>... logosList) { 349 for (Map<Uri, String> logos : logosList) { 350 for (Uri uri : logos.keySet()) { 351 try { 352 insertUrl(mContext, uri, new URL(logos.get(uri))); 353 } catch (MalformedURLException e) { 354 Log.e(TAG, "Can't load " + logos.get(uri), e); 355 } 356 } 357 } 358 return null; 359 } 360 } 361 } 362