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 
updateChannels( Context context, String inputId, List<ChannelInfo> channels)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 
getChannelCount(ContentResolver resolver, String inputId)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 
getVideoFormat(int videoHeight)149     private static String getVideoFormat(int videoHeight) {
150         return VIDEO_HEIGHT_TO_FORMAT_MAP.get(videoHeight);
151     }
152 
buildChannelMap(ContentResolver resolver, String inputId, List<ChannelInfo> channels)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 
getLastProgramEndTimeMillis(ContentResolver resolver, Uri channelUri)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 
getProgramPlaybackInfo( ContentResolver resolver, Uri channelUri, long startTimeMs, long endTimeMs, int maxProgramInReturn)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 
convertVideoInfoToInternalProviderData(int videotype, String videoUrl)243     public static String convertVideoInfoToInternalProviderData(int videotype, String videoUrl) {
244         return videotype + "," + videoUrl;
245     }
246 
parseInternalProviderData(String internalData)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 
insertUrl(Context context, Uri contentUri, URL sourceUrl)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 
copy(InputStream is, OutputStream os)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 
getServiceNameFromInputId(Context context, String inputId)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 
stringToContentRatings(String commaSeparatedRatings)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 
contentRatingsToString(TvContentRating[] contentRatings)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 
getChannelByNumber(String channelNumber, List<ChannelInfo> channels)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 
TvContractUtils()338     private TvContractUtils() {}
339 
340     public static class InsertLogosTask extends AsyncTask<Map<Uri, String>, Void, Void> {
341         private final Context mContext;
342 
InsertLogosTask(Context context)343         InsertLogosTask(Context context) {
344             mContext = context;
345         }
346 
347         @Override
doInBackground(Map<Uri, String>.... logosList)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