aboutsummaryrefslogtreecommitdiff
path: root/src/com/android/tv/data/ChannelLogoFetcher.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/android/tv/data/ChannelLogoFetcher.java')
-rw-r--r--src/com/android/tv/data/ChannelLogoFetcher.java307
1 files changed, 227 insertions, 80 deletions
diff --git a/src/com/android/tv/data/ChannelLogoFetcher.java b/src/com/android/tv/data/ChannelLogoFetcher.java
index 256ecdb2..5a549f83 100644
--- a/src/com/android/tv/data/ChannelLogoFetcher.java
+++ b/src/com/android/tv/data/ChannelLogoFetcher.java
@@ -16,68 +16,155 @@
package com.android.tv.data;
-import android.content.ContentProviderOperation;
import android.content.Context;
-import android.content.OperationApplicationException;
-import android.content.SharedPreferences;
+import android.database.Cursor;
import android.graphics.Bitmap.CompressFormat;
import android.media.tv.TvContract;
+import android.media.tv.TvContract.Channels;
import android.net.Uri;
import android.os.AsyncTask;
-import android.os.RemoteException;
-import android.support.annotation.AnyThread;
+import android.support.annotation.WorkerThread;
import android.text.TextUtils;
import android.util.Log;
-import com.android.tv.common.SharedPreferencesUtils;
+import com.android.tv.util.AsyncDbTask;
import com.android.tv.util.BitmapUtils;
import com.android.tv.util.BitmapUtils.ScaledBitmapInfo;
import com.android.tv.util.PermissionUtils;
+import java.io.BufferedReader;
import java.io.IOException;
+import java.io.InputStreamReader;
import java.io.OutputStream;
import java.util.ArrayList;
-import java.util.Map;
+import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
/**
- * Fetches channel logos from the cloud into the database. It's for the channels which have no logos
- * or need update logos. This class is thread safe.
+ * Utility class for TMS data.
+ * This class is thread safe.
*/
public class ChannelLogoFetcher {
private static final String TAG = "ChannelLogoFetcher";
private static final boolean DEBUG = false;
- private static final String PREF_KEY_IS_FIRST_TIME_FETCH_CHANNEL_LOGO =
- "is_first_time_fetch_channel_logo";
+ /**
+ * The name of the file which contains the TMS data.
+ * The file has multiple records and each of them is a string separated by '|' like
+ * STATION_NAME|SHORT_NAME|CALL_SIGN|LOGO_URI.
+ */
+ private static final String TMS_US_TABLE_FILE = "tms_us.table";
+ private static final String TMS_KR_TABLE_FILE = "tms_kr.table";
+ private static final String FIELD_SEPARATOR = "\\|";
+ private static final String NAME_SEPARATOR_FOR_TMS = "\\(|\\)|\\{|\\}|\\[|\\]";
+ private static final String NAME_SEPARATOR_FOR_DB = "\\W";
+ private static final int INDEX_NAME = 0;
+ private static final int INDEX_SHORT_NAME = 1;
+ private static final int INDEX_CALL_SIGN = 2;
+ private static final int INDEX_LOGO_URI = 3;
+
+ private static final String COLUMN_CHANNEL_LOGO = "logo";
+ private static final Object sLock = new Object();
+ private static final Set<Long> sChannelIdBlackListSet = new HashSet<>();
+ private static LoadChannelTask sQueryTask;
private static FetchLogoTask sFetchTask;
/**
- * Fetches the channel logos from the cloud data and insert them into TvProvider.
+ * Fetch the channel logos from TMS data and insert them into TvProvider.
* The previous task is canceled and a new task starts.
*/
- @AnyThread
- public static synchronized void startFetchingChannelLogos(
- Context context, List<Channel> channels) {
+ public static void startFetchingChannelLogos(Context context) {
if (!PermissionUtils.hasAccessAllEpg(context)) {
// TODO: support this feature for non-system LC app. b/23939816
return;
}
- if (sFetchTask != null) {
- sFetchTask.cancel(true);
+ synchronized (sLock) {
+ stopFetchingChannelLogos();
+ if (DEBUG) Log.d(TAG, "Request to start fetching logos.");
+ sQueryTask = new LoadChannelTask(context);
+ sQueryTask.executeOnDbThread();
}
- if (DEBUG) Log.d(TAG, "Request to start fetching logos.");
- if (channels == null || channels.isEmpty()) {
- return;
+ }
+
+ /**
+ * Stops the current fetching tasks. This can be called when the Activity pauses.
+ */
+ public static void stopFetchingChannelLogos() {
+ synchronized (sLock) {
+ if (DEBUG) Log.d(TAG, "Request to stop fetching logos.");
+ if (sQueryTask != null) {
+ sQueryTask.cancel(true);
+ sQueryTask = null;
+ }
+ if (sFetchTask != null) {
+ sFetchTask.cancel(true);
+ sFetchTask = null;
+ }
}
- sFetchTask = new FetchLogoTask(context, channels);
- sFetchTask.execute();
}
private ChannelLogoFetcher() {
}
+ private static final class LoadChannelTask extends AsyncDbTask<Void, Void, List<Channel>> {
+ private final Context mContext;
+
+ public LoadChannelTask(Context context) {
+ mContext = context;
+ }
+
+ @Override
+ protected List<Channel> doInBackground(Void... arg) {
+ // Load channels which doesn't have channel logos.
+ if (DEBUG) Log.d(TAG, "Starts loading the channels from DB");
+ String[] projection =
+ new String[] { Channels._ID, Channels.COLUMN_DISPLAY_NAME };
+ String selection = COLUMN_CHANNEL_LOGO + " IS NULL AND "
+ + Channels.COLUMN_PACKAGE_NAME + "=?";
+ String[] selectionArgs = new String[] { mContext.getPackageName() };
+ try (Cursor c = mContext.getContentResolver().query(Channels.CONTENT_URI,
+ projection, selection, selectionArgs, null)) {
+ if (c == null) {
+ Log.e(TAG, "Query returns null cursor", new RuntimeException());
+ return null;
+ }
+ List<Channel> channels = new ArrayList<>();
+ while (!isCancelled() && c.moveToNext()) {
+ long channelId = c.getLong(0);
+ if (sChannelIdBlackListSet.contains(channelId)) {
+ continue;
+ }
+ channels.add(new Channel.Builder().setId(c.getLong(0))
+ .setDisplayName(c.getString(1).toUpperCase(Locale.getDefault()))
+ .build());
+ }
+ return channels;
+ }
+ }
+
+ @Override
+ protected void onPostExecute(List<Channel> channels) {
+ synchronized (sLock) {
+ if (DEBUG) {
+ int count = channels == null ? 0 : channels.size();
+ Log.d(TAG, count + " channels are loaded");
+ }
+ if (sQueryTask == this) {
+ sQueryTask = null;
+ if (channels != null && !channels.isEmpty()) {
+ sFetchTask = new FetchLogoTask(mContext, channels);
+ sFetchTask.execute();
+ }
+ }
+ }
+ }
+ }
+
private static final class FetchLogoTask extends AsyncTask<Void, Void, Void> {
private final Context mContext;
private final List<Channel> mChannels;
@@ -93,53 +180,83 @@ public class ChannelLogoFetcher {
if (DEBUG) Log.d(TAG, "Fetching the channel logos has been canceled");
return null;
}
- List<Channel> channelsToUpdate = new ArrayList<>();
- List<Channel> channelsToRemove = new ArrayList<>();
- // Updates or removes the logo by comparing the logo uri which is got from the cloud
- // and the stored one. And we assume that the data got form the cloud is 100%
- // correct and completed.
- SharedPreferences sharedPreferences =
- mContext.getSharedPreferences(
- SharedPreferencesUtils.SHARED_PREF_CHANNEL_LOGO_URIS,
- Context.MODE_PRIVATE);
- SharedPreferences.Editor sharedPreferencesEditor = sharedPreferences.edit();
- Map<String, ?> uncheckedChannels = sharedPreferences.getAll();
- boolean isFirstTimeFetchChannelLogo = sharedPreferences.getBoolean(
- PREF_KEY_IS_FIRST_TIME_FETCH_CHANNEL_LOGO, true);
- // Iterating channels.
- for (Channel channel : mChannels) {
- String channelIdString = Long.toString(channel.getId());
- String storedChannelLogoUri = (String) uncheckedChannels.remove(channelIdString);
- if (!TextUtils.isEmpty(channel.getLogoUri())
- && !TextUtils.equals(storedChannelLogoUri, channel.getLogoUri())) {
- channelsToUpdate.add(channel);
- sharedPreferencesEditor.putString(channelIdString, channel.getLogoUri());
- } else if (TextUtils.isEmpty(channel.getLogoUri())
- && (!TextUtils.isEmpty(storedChannelLogoUri)
- || isFirstTimeFetchChannelLogo)) {
- channelsToRemove.add(channel);
- sharedPreferencesEditor.remove(channelIdString);
+ // Load the TMS table data.
+ if (DEBUG) Log.d(TAG, "Loads TMS data");
+ Map<String, String> channelNameLogoUriMap = new HashMap<>();
+ try {
+ channelNameLogoUriMap.putAll(readTmsFile(mContext, TMS_US_TABLE_FILE));
+ if (isCancelled()) {
+ if (DEBUG) Log.d(TAG, "Fetching the channel logos has been canceled");
+ return null;
}
+ channelNameLogoUriMap.putAll(readTmsFile(mContext, TMS_KR_TABLE_FILE));
+ } catch (IOException e) {
+ Log.e(TAG, "Loading TMS data failed.", e);
+ return null;
}
-
- // Removes non existing channels from SharedPreferences.
- for (String channelId : uncheckedChannels.keySet()) {
- sharedPreferencesEditor.remove(channelId);
+ if (isCancelled()) {
+ if (DEBUG) Log.d(TAG, "Fetching the channel logos has been canceled");
+ return null;
}
- // Updates channel logos.
- for (Channel channel : channelsToUpdate) {
+ // Iterating channels.
+ for (Channel channel : mChannels) {
if (isCancelled()) {
if (DEBUG) Log.d(TAG, "Fetching the channel logos has been canceled");
return null;
}
- // Downloads the channel logo.
- String logoUri = channel.getLogoUri();
+ // Download the channel logo.
+ if (TextUtils.isEmpty(channel.getDisplayName())) {
+ if (DEBUG) {
+ Log.d(TAG, "The channel with ID (" + channel.getId()
+ + ") doesn't have the display name.");
+ }
+ sChannelIdBlackListSet.add(channel.getId());
+ continue;
+ }
+ String channelName = channel.getDisplayName().trim();
+ String logoUri = channelNameLogoUriMap.get(channelName);
+ if (TextUtils.isEmpty(logoUri)) {
+ if (DEBUG) {
+ Log.d(TAG, "Can't find a logo URI for channel '" + channelName + "'");
+ }
+ // Find the candidate names. If the channel name is CNN-HD, then find CNNHD
+ // and CNN. Or if the channel name is KQED+, then find KQED.
+ String[] splitNames = channelName.split(NAME_SEPARATOR_FOR_DB);
+ if (splitNames.length > 1) {
+ StringBuilder sb = new StringBuilder();
+ for (String splitName : splitNames) {
+ sb.append(splitName);
+ }
+ logoUri = channelNameLogoUriMap.get(sb.toString());
+ if (DEBUG) {
+ if (TextUtils.isEmpty(logoUri)) {
+ Log.d(TAG, "Can't find a logo URI for channel '" + sb.toString()
+ + "'");
+ }
+ }
+ }
+ if (TextUtils.isEmpty(logoUri)
+ && splitNames[0].length() != channelName.length()) {
+ logoUri = channelNameLogoUriMap.get(splitNames[0]);
+ if (DEBUG) {
+ if (TextUtils.isEmpty(logoUri)) {
+ Log.d(TAG, "Can't find a logo URI for channel '" + splitNames[0]
+ + "'");
+ }
+ }
+ }
+ }
+ if (TextUtils.isEmpty(logoUri)) {
+ sChannelIdBlackListSet.add(channel.getId());
+ continue;
+ }
ScaledBitmapInfo bitmapInfo = BitmapUtils.decodeSampledBitmapFromUriString(
mContext, logoUri, Integer.MAX_VALUE, Integer.MAX_VALUE);
if (bitmapInfo == null) {
Log.e(TAG, "Failed to load bitmap. {channelName=" + channel.getDisplayName()
+ ", " + "logoUri=" + logoUri + "}");
+ sChannelIdBlackListSet.add(channel.getId());
continue;
}
if (isCancelled()) {
@@ -147,15 +264,12 @@ public class ChannelLogoFetcher {
return null;
}
- // Inserts the logo to DB.
+ // Insert the logo to DB.
Uri dstLogoUri = TvContract.buildChannelLogoUri(channel.getId());
try (OutputStream os = mContext.getContentResolver().openOutputStream(dstLogoUri)) {
bitmapInfo.bitmap.compress(CompressFormat.PNG, 100, os);
} catch (IOException e) {
Log.e(TAG, "Failed to write " + logoUri + " to " + dstLogoUri, e);
- // Removes it from the shared preference for the failed channels to make it
- // retry next time.
- sharedPreferencesEditor.remove(Long.toString(channel.getId()));
continue;
}
if (DEBUG) {
@@ -163,30 +277,63 @@ public class ChannelLogoFetcher {
+ dstLogoUri + "}");
}
}
+ if (DEBUG) Log.d(TAG, "Fetching logos has been finished successfully.");
+ return null;
+ }
- // Removes the logos for the channels that have logos before but now
- // their logo uris are null.
- boolean deleteChannelLogoFailed = false;
- if (!channelsToRemove.isEmpty()) {
- ArrayList<ContentProviderOperation> ops = new ArrayList<>();
- for (Channel channel : channelsToRemove) {
- ops.add(ContentProviderOperation.newDelete(
- TvContract.buildChannelLogoUri(channel.getId())).build());
- }
- try {
- mContext.getContentResolver().applyBatch(TvContract.AUTHORITY, ops);
- } catch (RemoteException | OperationApplicationException e) {
- deleteChannelLogoFailed = true;
- Log.e(TAG, "Error deleting obsolete channels", e);
+ @WorkerThread
+ private Map<String, String> readTmsFile(Context context, String fileName)
+ throws IOException {
+ try (BufferedReader reader = new BufferedReader(new InputStreamReader(
+ context.getAssets().open(fileName)))) {
+ Map<String, String> channelNameLogoUriMap = new HashMap<>();
+ String line;
+ while ((line = reader.readLine()) != null && !isCancelled()) {
+ String[] data = line.split(FIELD_SEPARATOR);
+ if (data.length != INDEX_LOGO_URI + 1) {
+ if (DEBUG) Log.d(TAG, "Invalid or comment row: " + line);
+ continue;
+ }
+ addChannelNames(channelNameLogoUriMap,
+ data[INDEX_NAME].toUpperCase(Locale.getDefault()),
+ data[INDEX_LOGO_URI]);
+ addChannelNames(channelNameLogoUriMap,
+ data[INDEX_SHORT_NAME].toUpperCase(Locale.getDefault()),
+ data[INDEX_LOGO_URI]);
+ addChannelNames(channelNameLogoUriMap,
+ data[INDEX_CALL_SIGN].toUpperCase(Locale.getDefault()),
+ data[INDEX_LOGO_URI]);
}
+ return channelNameLogoUriMap;
}
- if (isFirstTimeFetchChannelLogo && !deleteChannelLogoFailed) {
- sharedPreferencesEditor.putBoolean(
- PREF_KEY_IS_FIRST_TIME_FETCH_CHANNEL_LOGO, false);
+ }
+
+ private void addChannelNames(Map<String, String> channelNameLogoUriMap, String channelName,
+ String logoUri) {
+ if (!TextUtils.isEmpty(channelName)) {
+ channelNameLogoUriMap.put(channelName, logoUri);
+ // Find the candidate names.
+ // If the name is like "W05AAD (W05AA-D)", then split the names into "W05AAD" and
+ // "W05AA-D"
+ String[] splitNames = channelName.split(NAME_SEPARATOR_FOR_TMS);
+ if (splitNames.length > 1) {
+ for (String name : splitNames) {
+ name = name.trim();
+ if (channelNameLogoUriMap.get(name) == null) {
+ channelNameLogoUriMap.put(name, logoUri);
+ }
+ }
+ }
+ }
+ }
+
+ @Override
+ protected void onPostExecute(Void result) {
+ synchronized (sLock) {
+ if (sFetchTask == this) {
+ sFetchTask = null;
+ }
}
- sharedPreferencesEditor.commit();
- if (DEBUG) Log.d(TAG, "Fetching logos has been finished successfully.");
- return null;
}
}
}