Home | History | Annotate | Download | only in sampletvinput
      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