Home | History | Annotate | Download | only in data
      1 /*
      2  * Copyright (C) 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.android.tv.data;
     18 
     19 import android.content.Context;
     20 import android.database.Cursor;
     21 import android.graphics.Bitmap.CompressFormat;
     22 import android.media.tv.TvContract;
     23 import android.media.tv.TvContract.Channels;
     24 import android.net.Uri;
     25 import android.os.AsyncTask;
     26 import android.support.annotation.WorkerThread;
     27 import android.text.TextUtils;
     28 import android.util.Log;
     29 
     30 import com.android.tv.util.AsyncDbTask;
     31 import com.android.tv.util.BitmapUtils;
     32 import com.android.tv.util.BitmapUtils.ScaledBitmapInfo;
     33 import com.android.tv.util.PermissionUtils;
     34 
     35 import java.io.BufferedReader;
     36 import java.io.IOException;
     37 import java.io.InputStreamReader;
     38 import java.io.OutputStream;
     39 import java.util.ArrayList;
     40 import java.util.HashMap;
     41 import java.util.HashSet;
     42 import java.util.List;
     43 import java.util.Locale;
     44 import java.util.Map;
     45 import java.util.Set;
     46 
     47 /**
     48  * Utility class for TMS data.
     49  * This class is thread safe.
     50  */
     51 public class ChannelLogoFetcher {
     52     private static final String TAG = "ChannelLogoFetcher";
     53     private static final boolean DEBUG = false;
     54 
     55     /**
     56      * The name of the file which contains the TMS data.
     57      * The file has multiple records and each of them is a string separated by '|' like
     58      * STATION_NAME|SHORT_NAME|CALL_SIGN|LOGO_URI.
     59      */
     60     private static final String TMS_US_TABLE_FILE = "tms_us.table";
     61     private static final String TMS_KR_TABLE_FILE = "tms_kr.table";
     62     private static final String FIELD_SEPARATOR = "\\|";
     63     private static final String NAME_SEPARATOR_FOR_TMS = "\\(|\\)|\\{|\\}|\\[|\\]";
     64     private static final String NAME_SEPARATOR_FOR_DB = "\\W";
     65     private static final int INDEX_NAME = 0;
     66     private static final int INDEX_SHORT_NAME = 1;
     67     private static final int INDEX_CALL_SIGN = 2;
     68     private static final int INDEX_LOGO_URI = 3;
     69 
     70     private static final String COLUMN_CHANNEL_LOGO = "logo";
     71 
     72     private static final Object sLock = new Object();
     73     private static final Set<Long> sChannelIdBlackListSet = new HashSet<>();
     74     private static LoadChannelTask sQueryTask;
     75     private static FetchLogoTask sFetchTask;
     76 
     77     /**
     78      * Fetch the channel logos from TMS data and insert them into TvProvider.
     79      * The previous task is canceled and a new task starts.
     80      */
     81     public static void startFetchingChannelLogos(Context context) {
     82         if (!PermissionUtils.hasAccessAllEpg(context)) {
     83             // TODO: support this feature for non-system LC app. b/23939816
     84             return;
     85         }
     86         synchronized (sLock) {
     87             stopFetchingChannelLogos();
     88             if (DEBUG) Log.d(TAG, "Request to start fetching logos.");
     89             sQueryTask = new LoadChannelTask(context);
     90             sQueryTask.executeOnDbThread();
     91         }
     92     }
     93 
     94     /**
     95      * Stops the current fetching tasks. This can be called when the Activity pauses.
     96      */
     97     public static void stopFetchingChannelLogos() {
     98         synchronized (sLock) {
     99             if (DEBUG) Log.d(TAG, "Request to stop fetching logos.");
    100             if (sQueryTask != null) {
    101                 sQueryTask.cancel(true);
    102                 sQueryTask = null;
    103             }
    104             if (sFetchTask != null) {
    105                 sFetchTask.cancel(true);
    106                 sFetchTask = null;
    107             }
    108         }
    109     }
    110 
    111     private ChannelLogoFetcher() {
    112     }
    113 
    114     private static final class LoadChannelTask extends AsyncDbTask<Void, Void, List<Channel>> {
    115         private final Context mContext;
    116 
    117         public LoadChannelTask(Context context) {
    118             mContext = context;
    119         }
    120 
    121         @Override
    122         protected List<Channel> doInBackground(Void... arg) {
    123             // Load channels which doesn't have channel logos.
    124             if (DEBUG) Log.d(TAG, "Starts loading the channels from DB");
    125             String[] projection =
    126                     new String[] { Channels._ID, Channels.COLUMN_DISPLAY_NAME };
    127             String selection = COLUMN_CHANNEL_LOGO + " IS NULL AND "
    128                     + Channels.COLUMN_PACKAGE_NAME + "=?";
    129             String[] selectionArgs = new String[] { mContext.getPackageName() };
    130             try (Cursor c = mContext.getContentResolver().query(Channels.CONTENT_URI,
    131                     projection, selection, selectionArgs, null)) {
    132                 if (c == null) {
    133                     Log.e(TAG, "Query returns null cursor", new RuntimeException());
    134                     return null;
    135                 }
    136                 List<Channel> channels = new ArrayList<>();
    137                 while (!isCancelled() && c.moveToNext()) {
    138                     long channelId = c.getLong(0);
    139                     if (sChannelIdBlackListSet.contains(channelId)) {
    140                         continue;
    141                     }
    142                     channels.add(new Channel.Builder().setId(c.getLong(0))
    143                             .setDisplayName(c.getString(1).toUpperCase(Locale.getDefault()))
    144                             .build());
    145                 }
    146                 return channels;
    147             }
    148         }
    149 
    150         @Override
    151         protected void onPostExecute(List<Channel> channels) {
    152             synchronized (sLock) {
    153                 if (DEBUG) {
    154                     int count = channels == null ? 0 : channels.size();
    155                     Log.d(TAG, count + " channels are loaded");
    156                 }
    157                 if (sQueryTask == this) {
    158                     sQueryTask = null;
    159                     if (channels != null && !channels.isEmpty()) {
    160                         sFetchTask = new FetchLogoTask(mContext, channels);
    161                         sFetchTask.execute();
    162                     }
    163                 }
    164             }
    165         }
    166     }
    167 
    168     private static final class FetchLogoTask extends AsyncTask<Void, Void, Void> {
    169         private final Context mContext;
    170         private final List<Channel> mChannels;
    171 
    172         public FetchLogoTask(Context context, List<Channel> channels) {
    173             mContext = context;
    174             mChannels = channels;
    175         }
    176 
    177         @Override
    178         protected Void doInBackground(Void... arg) {
    179             if (isCancelled()) {
    180                 if (DEBUG) Log.d(TAG, "Fetching the channel logos has been canceled");
    181                 return null;
    182             }
    183             // Load the TMS table data.
    184             if (DEBUG) Log.d(TAG, "Loads TMS data");
    185             Map<String, String> channelNameLogoUriMap = new HashMap<>();
    186             try {
    187                 channelNameLogoUriMap.putAll(readTmsFile(mContext, TMS_US_TABLE_FILE));
    188                 if (isCancelled()) {
    189                     if (DEBUG) Log.d(TAG, "Fetching the channel logos has been canceled");
    190                     return null;
    191                 }
    192                 channelNameLogoUriMap.putAll(readTmsFile(mContext, TMS_KR_TABLE_FILE));
    193             } catch (IOException e) {
    194                 Log.e(TAG, "Loading TMS data failed.", e);
    195                 return null;
    196             }
    197             if (isCancelled()) {
    198                 if (DEBUG) Log.d(TAG, "Fetching the channel logos has been canceled");
    199                 return null;
    200             }
    201 
    202             // Iterating channels.
    203             for (Channel channel : mChannels) {
    204                 if (isCancelled()) {
    205                     if (DEBUG) Log.d(TAG, "Fetching the channel logos has been canceled");
    206                     return null;
    207                 }
    208                 // Download the channel logo.
    209                 if (TextUtils.isEmpty(channel.getDisplayName())) {
    210                     if (DEBUG) {
    211                         Log.d(TAG, "The channel with ID (" + channel.getId()
    212                                 + ") doesn't have the display name.");
    213                     }
    214                     sChannelIdBlackListSet.add(channel.getId());
    215                     continue;
    216                 }
    217                 String channelName = channel.getDisplayName().trim();
    218                 String logoUri = channelNameLogoUriMap.get(channelName);
    219                 if (TextUtils.isEmpty(logoUri)) {
    220                     if (DEBUG) {
    221                         Log.d(TAG, "Can't find a logo URI for channel '" + channelName + "'");
    222                     }
    223                     // Find the candidate names. If the channel name is CNN-HD, then find CNNHD
    224                     // and CNN. Or if the channel name is KQED+, then find KQED.
    225                     String[] splitNames = channelName.split(NAME_SEPARATOR_FOR_DB);
    226                     if (splitNames.length > 1) {
    227                         StringBuilder sb = new StringBuilder();
    228                         for (String splitName : splitNames) {
    229                             sb.append(splitName);
    230                         }
    231                         logoUri = channelNameLogoUriMap.get(sb.toString());
    232                         if (DEBUG) {
    233                             if (TextUtils.isEmpty(logoUri)) {
    234                                 Log.d(TAG, "Can't find a logo URI for channel '" + sb.toString()
    235                                         + "'");
    236                             }
    237                         }
    238                     }
    239                     if (TextUtils.isEmpty(logoUri)
    240                             && splitNames[0].length() != channelName.length()) {
    241                         logoUri = channelNameLogoUriMap.get(splitNames[0]);
    242                         if (DEBUG) {
    243                             if (TextUtils.isEmpty(logoUri)) {
    244                                 Log.d(TAG, "Can't find a logo URI for channel '" + splitNames[0]
    245                                         + "'");
    246                             }
    247                         }
    248                     }
    249                 }
    250                 if (TextUtils.isEmpty(logoUri)) {
    251                     sChannelIdBlackListSet.add(channel.getId());
    252                     continue;
    253                 }
    254                 ScaledBitmapInfo bitmapInfo = BitmapUtils.decodeSampledBitmapFromUriString(
    255                         mContext, logoUri, Integer.MAX_VALUE, Integer.MAX_VALUE);
    256                 if (bitmapInfo == null) {
    257                     Log.e(TAG, "Failed to load bitmap. {channelName=" + channel.getDisplayName()
    258                             + ", " + "logoUri=" + logoUri + "}");
    259                     sChannelIdBlackListSet.add(channel.getId());
    260                     continue;
    261                 }
    262                 if (isCancelled()) {
    263                     if (DEBUG) Log.d(TAG, "Fetching the channel logos has been canceled");
    264                     return null;
    265                 }
    266 
    267                 // Insert the logo to DB.
    268                 Uri dstLogoUri = TvContract.buildChannelLogoUri(channel.getId());
    269                 try (OutputStream os = mContext.getContentResolver().openOutputStream(dstLogoUri)) {
    270                     bitmapInfo.bitmap.compress(CompressFormat.PNG, 100, os);
    271                 } catch (IOException e) {
    272                     Log.e(TAG, "Failed to write " + logoUri + "  to " + dstLogoUri, e);
    273                     continue;
    274                 }
    275                 if (DEBUG) {
    276                     Log.d(TAG, "Inserting logo file to DB succeeded. {from=" + logoUri + ", to="
    277                             + dstLogoUri + "}");
    278                 }
    279             }
    280             if (DEBUG) Log.d(TAG, "Fetching logos has been finished successfully.");
    281             return null;
    282         }
    283 
    284         @WorkerThread
    285         private Map<String, String> readTmsFile(Context context, String fileName)
    286                 throws IOException {
    287             try (BufferedReader reader = new BufferedReader(new InputStreamReader(
    288                     context.getAssets().open(fileName)))) {
    289                 Map<String, String> channelNameLogoUriMap = new HashMap<>();
    290                 String line;
    291                 while ((line = reader.readLine()) != null && !isCancelled()) {
    292                     String[] data = line.split(FIELD_SEPARATOR);
    293                     if (data.length != INDEX_LOGO_URI + 1) {
    294                         if (DEBUG) Log.d(TAG, "Invalid or comment row: " + line);
    295                         continue;
    296                     }
    297                     addChannelNames(channelNameLogoUriMap,
    298                             data[INDEX_NAME].toUpperCase(Locale.getDefault()),
    299                             data[INDEX_LOGO_URI]);
    300                     addChannelNames(channelNameLogoUriMap,
    301                             data[INDEX_SHORT_NAME].toUpperCase(Locale.getDefault()),
    302                             data[INDEX_LOGO_URI]);
    303                     addChannelNames(channelNameLogoUriMap,
    304                             data[INDEX_CALL_SIGN].toUpperCase(Locale.getDefault()),
    305                             data[INDEX_LOGO_URI]);
    306                 }
    307                 return channelNameLogoUriMap;
    308             }
    309         }
    310 
    311         private void addChannelNames(Map<String, String> channelNameLogoUriMap, String channelName,
    312                 String logoUri) {
    313             if (!TextUtils.isEmpty(channelName)) {
    314                 channelNameLogoUriMap.put(channelName, logoUri);
    315                 // Find the candidate names.
    316                 // If the name is like "W05AAD (W05AA-D)", then split the names into "W05AAD" and
    317                 // "W05AA-D"
    318                 String[] splitNames = channelName.split(NAME_SEPARATOR_FOR_TMS);
    319                 if (splitNames.length > 1) {
    320                     for (String name : splitNames) {
    321                         name = name.trim();
    322                         if (channelNameLogoUriMap.get(name) == null) {
    323                             channelNameLogoUriMap.put(name, logoUri);
    324                         }
    325                     }
    326                 }
    327             }
    328         }
    329 
    330         @Override
    331         protected void onPostExecute(Void result) {
    332             synchronized (sLock) {
    333                 if (sFetchTask == this) {
    334                     sFetchTask = null;
    335                 }
    336             }
    337         }
    338     }
    339 }
    340