diff options
Diffstat (limited to 'src/com/android/tv/data/ChannelDataManager.java')
-rw-r--r-- | src/com/android/tv/data/ChannelDataManager.java | 200 |
1 files changed, 147 insertions, 53 deletions
diff --git a/src/com/android/tv/data/ChannelDataManager.java b/src/com/android/tv/data/ChannelDataManager.java index 6f9ea6d7..6f93fbd1 100644 --- a/src/com/android/tv/data/ChannelDataManager.java +++ b/src/com/android/tv/data/ChannelDataManager.java @@ -21,13 +21,17 @@ import android.content.ContentValues; import android.content.Context; import android.content.SharedPreferences; import android.content.SharedPreferences.Editor; +import android.content.res.AssetFileDescriptor; import android.database.ContentObserver; +import android.database.sqlite.SQLiteException; import android.media.tv.TvContract; import android.media.tv.TvContract.Channels; import android.media.tv.TvInputManager.TvInputCallback; +import android.os.AsyncTask; import android.os.Handler; import android.os.Looper; import android.os.Message; +import android.support.annotation.AnyThread; import android.support.annotation.MainThread; import android.support.annotation.NonNull; import android.support.annotation.VisibleForTesting; @@ -43,6 +47,7 @@ import com.android.tv.util.PermissionUtils; import com.android.tv.util.TvInputManagerHelper; import com.android.tv.util.Utils; +import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -59,7 +64,7 @@ import java.util.concurrent.CopyOnWriteArraySet; * This class is not thread-safe and under an assumption that its public methods are called in * only the main thread. */ -@MainThread +@AnyThread public class ChannelDataManager { private static final String TAG = "ChannelDataManager"; private static final boolean DEBUG = false; @@ -74,10 +79,10 @@ public class ChannelDataManager { private final List<Runnable> mPostRunnablesAfterChannelUpdate = new ArrayList<>(); private final Set<Listener> mListeners = new CopyOnWriteArraySet<>(); - private final Map<Long, ChannelWrapper> mChannelWrapperMap = new HashMap<>(); - private final Map<String, MutableInt> mChannelCountMap = new HashMap<>(); + // Use container class to support multi-thread safety. This value can be set only on the main + // thread. + volatile private UnmodifiableChannelData mData = new UnmodifiableChannelData(); private final Channel.DefaultComparator mChannelComparator; - private final List<Channel> mChannels = new ArrayList<>(); private final Handler mHandler; private final Set<Long> mBrowsableUpdateChannelIds = new HashSet<>(); @@ -92,15 +97,17 @@ public class ChannelDataManager { @Override public void onInputAdded(String inputId) { boolean channelAdded = false; - for (ChannelWrapper channel : mChannelWrapperMap.values()) { + ChannelData data = new ChannelData(mData); + for (ChannelWrapper channel : mData.channelWrapperMap.values()) { if (channel.mChannel.getInputId().equals(inputId)) { channel.mInputRemoved = false; - addChannel(channel.mChannel); + addChannel(data, channel.mChannel); channelAdded = true; } } if (channelAdded) { - Collections.sort(mChannels, mChannelComparator); + Collections.sort(data.channels, mChannelComparator); + mData = new UnmodifiableChannelData(data); notifyChannelListUpdated(); } } @@ -109,7 +116,7 @@ public class ChannelDataManager { public void onInputRemoved(String inputId) { boolean channelRemoved = false; ArrayList<ChannelWrapper> removedChannels = new ArrayList<>(); - for (ChannelWrapper channel : mChannelWrapperMap.values()) { + for (ChannelWrapper channel : mData.channelWrapperMap.values()) { if (channel.mChannel.getInputId().equals(inputId)) { channel.mInputRemoved = true; channelRemoved = true; @@ -117,13 +124,15 @@ public class ChannelDataManager { } } if (channelRemoved) { - clearChannels(); - for (ChannelWrapper channelWrapper : mChannelWrapperMap.values()) { + ChannelData data = new ChannelData(); + data.channelWrapperMap.putAll(mData.channelWrapperMap); + for (ChannelWrapper channelWrapper : data.channelWrapperMap.values()) { if (!channelWrapper.mInputRemoved) { - addChannel(channelWrapper.mChannel); + addChannel(data, channelWrapper.mChannel); } } - Collections.sort(mChannels, mChannelComparator); + Collections.sort(data.channels, mChannelComparator); + mData = new UnmodifiableChannelData(data); notifyChannelListUpdated(); for (ChannelWrapper channel : removedChannels) { channel.notifyChannelRemoved(); @@ -132,10 +141,12 @@ public class ChannelDataManager { } }; + @MainThread public ChannelDataManager(Context context, TvInputManagerHelper inputManager) { this(context, inputManager, context.getContentResolver()); } + @MainThread @VisibleForTesting ChannelDataManager(Context context, TvInputManagerHelper inputManager, ContentResolver contentResolver) { @@ -167,6 +178,7 @@ public class ChannelDataManager { /** * Starts the manager. If data is ready, {@link Listener#onLoadFinished()} will be called. */ + @MainThread public void start() { if (mStarted) { return; @@ -184,6 +196,7 @@ public class ChannelDataManager { * Stops the manager. It clears manager states and runs pending DB operations. Added listeners * aren't automatically removed by this method. */ + @MainThread @VisibleForTesting public void stop() { if (!mStarted) { @@ -192,12 +205,10 @@ public class ChannelDataManager { mStarted = false; mDbLoadFinished = false; - ChannelLogoFetcher.stopFetchingChannelLogos(); mInputManager.removeCallback(mTvInputCallback); mContentResolver.unregisterContentObserver(mChannelObserver); mHandler.removeCallbacksAndMessages(null); - mChannelWrapperMap.clear(); clearChannels(); mPostRunnablesAfterChannelUpdate.clear(); if (mChannelsUpdateTask != null) { @@ -233,7 +244,7 @@ public class ChannelDataManager { * Adds a {@link ChannelListener} for a specific channel with the channel ID {@code channelId}. */ public void addChannelListener(Long channelId, ChannelListener listener) { - ChannelWrapper channelWrapper = mChannelWrapperMap.get(channelId); + ChannelWrapper channelWrapper = mData.channelWrapperMap.get(channelId); if (channelWrapper == null) { return; } @@ -245,7 +256,7 @@ public class ChannelDataManager { * {@code channelId}. */ public void removeChannelListener(Long channelId, ChannelListener listener) { - ChannelWrapper channelWrapper = mChannelWrapperMap.get(channelId); + ChannelWrapper channelWrapper = mData.channelWrapperMap.get(channelId); if (channelWrapper == null) { return; } @@ -263,14 +274,14 @@ public class ChannelDataManager { * Returns the number of channels. */ public int getChannelCount() { - return mChannels.size(); + return mData.channels.size(); } /** * Returns a list of channels. */ public List<Channel> getChannelList() { - return Collections.unmodifiableList(mChannels); + return new ArrayList<>(mData.channels); } /** @@ -278,7 +289,7 @@ public class ChannelDataManager { */ public List<Channel> getBrowsableChannelList() { List<Channel> channels = new ArrayList<>(); - for (Channel channel : mChannels) { + for (Channel channel : mData.channels) { if (channel.isBrowsable()) { channels.add(channel); } @@ -292,7 +303,7 @@ public class ChannelDataManager { * @param inputId The ID of the input. */ public int getChannelCountForInput(String inputId) { - MutableInt count = mChannelCountMap.get(inputId); + MutableInt count = mData.channelCountMap.get(inputId); return count == null ? 0 : count.value; } @@ -303,17 +314,14 @@ public class ChannelDataManager { * In that case this method is used to check if the channel exists in the DB. */ public boolean doesChannelExistInDb(long channelId) { - return mChannelWrapperMap.get(channelId) != null; + return mData.channelWrapperMap.get(channelId) != null; } /** * Returns true if and only if there exists at least one channel and all channels are hidden. */ public boolean areAllChannelsHidden() { - if (mChannels.isEmpty()) { - return false; - } - for (Channel channel : mChannels) { + for (Channel channel : mData.channels) { if (channel.isBrowsable()) { return false; } @@ -325,7 +333,7 @@ public class ChannelDataManager { * Gets the channel with the channel ID {@code channelId}. */ public Channel getChannel(Long channelId) { - ChannelWrapper channelWrapper = mChannelWrapperMap.get(channelId); + ChannelWrapper channelWrapper = mData.channelWrapperMap.get(channelId); if (channelWrapper == null || channelWrapper.mInputRemoved) { return null; } @@ -349,7 +357,7 @@ public class ChannelDataManager { */ public void updateBrowsable(Long channelId, boolean browsable, boolean skipNotifyChannelBrowsableChanged) { - ChannelWrapper channelWrapper = mChannelWrapperMap.get(channelId); + ChannelWrapper channelWrapper = mData.channelWrapperMap.get(channelId); if (channelWrapper == null) { return; } @@ -407,7 +415,7 @@ public class ChannelDataManager { * The value change will be applied to DB when applyPendingDbOperation is called. */ public void updateLocked(Long channelId, boolean locked) { - ChannelWrapper channelWrapper = mChannelWrapperMap.get(channelId); + ChannelWrapper channelWrapper = mData.channelWrapperMap.get(channelId); if (channelWrapper == null) { return; } @@ -427,10 +435,11 @@ public class ChannelDataManager { * to DB. */ public void applyUpdatedValuesToDb() { + ChannelData data = mData; ArrayList<Long> browsableIds = new ArrayList<>(); ArrayList<Long> unbrowsableIds = new ArrayList<>(); for (Long id : mBrowsableUpdateChannelIds) { - ChannelWrapper channelWrapper = mChannelWrapperMap.get(id); + ChannelWrapper channelWrapper = data.channelWrapperMap.get(id); if (channelWrapper == null) { continue; } @@ -452,10 +461,10 @@ public class ChannelDataManager { } editor.apply(); } else { - if (browsableIds.size() != 0) { + if (!browsableIds.isEmpty()) { updateOneColumnValue(column, 1, browsableIds); } - if (unbrowsableIds.size() != 0) { + if (!unbrowsableIds.isEmpty()) { updateOneColumnValue(column, 0, unbrowsableIds); } } @@ -464,7 +473,7 @@ public class ChannelDataManager { ArrayList<Long> lockedIds = new ArrayList<>(); ArrayList<Long> unlockedIds = new ArrayList<>(); for (Long id : mLockedUpdateChannelIds) { - ChannelWrapper channelWrapper = mChannelWrapperMap.get(id); + ChannelWrapper channelWrapper = data.channelWrapperMap.get(id); if (channelWrapper == null) { continue; } @@ -476,10 +485,10 @@ public class ChannelDataManager { channelWrapper.mLockedInDb = channelWrapper.mChannel.isLocked(); } column = TvContract.Channels.COLUMN_LOCKED; - if (lockedIds.size() != 0) { + if (!lockedIds.isEmpty()) { updateOneColumnValue(column, 1, lockedIds); } - if (unlockedIds.size() != 0) { + if (!unlockedIds.isEmpty()) { updateOneColumnValue(column, 0, unlockedIds); } mLockedUpdateChannelIds.clear(); @@ -492,22 +501,24 @@ public class ChannelDataManager { } } - private void addChannel(Channel channel) { - mChannels.add(channel); + @MainThread + private void addChannel(ChannelData data, Channel channel) { + data.channels.add(channel); String inputId = channel.getInputId(); - MutableInt count = mChannelCountMap.get(inputId); + MutableInt count = data.channelCountMap.get(inputId); if (count == null) { - mChannelCountMap.put(inputId, new MutableInt(1)); + data.channelCountMap.put(inputId, new MutableInt(1)); } else { count.value++; } } + @MainThread private void clearChannels() { - mChannels.clear(); - mChannelCountMap.clear(); + mData = new UnmodifiableChannelData(); } + @MainThread private void handleUpdateChannels() { if (mChannelsUpdateTask != null) { mChannelsUpdateTask.cancel(true); @@ -525,6 +536,9 @@ public class ChannelDataManager { } } + /** + * A listener for ChannelDataManager. The callbacks are called on the main thread. + */ public interface Listener { /** * Called when data load is finished. @@ -543,6 +557,9 @@ public class ChannelDataManager { void onChannelBrowsableChanged(); } + /** + * A listener for individual channel change. The callbacks are called on the main thread. + */ public interface ChannelListener { /** * Called when the channel has been removed in DB. @@ -590,9 +607,36 @@ public class ChannelDataManager { } } + private class CheckChannelLogoExistTask extends AsyncTask<Void, Void, Boolean> { + private final Channel mChannel; + + CheckChannelLogoExistTask(Channel channel) { + mChannel = channel; + } + + @Override + protected Boolean doInBackground(Void... params) { + try (AssetFileDescriptor f = mContext.getContentResolver().openAssetFileDescriptor( + TvContract.buildChannelLogoUri(mChannel.getId()), "r")) { + return true; + } catch (SQLiteException | IOException | NullPointerException e) { + // File not found or asset file not found. + } + return false; + } + + @Override + protected void onPostExecute(Boolean result) { + ChannelWrapper wrapper = mData.channelWrapperMap.get(mChannel.getId()); + if (wrapper != null) { + wrapper.mChannel.setChannelLogoExist(result); + } + } + } + private final class QueryAllChannelsTask extends AsyncDbTask.AsyncChannelQueryTask { - public QueryAllChannelsTask(ContentResolver contentResolver) { + QueryAllChannelsTask(ContentResolver contentResolver) { super(contentResolver); } @@ -603,7 +647,9 @@ public class ChannelDataManager { if (DEBUG) Log.e(TAG, "onPostExecute with null channels"); return; } - Set<Long> removedChannelIds = new HashSet<>(mChannelWrapperMap.keySet()); + ChannelData data = new ChannelData(); + data.channelWrapperMap.putAll(mData.channelWrapperMap); + Set<Long> removedChannelIds = new HashSet<>(data.channelWrapperMap.keySet()); List<ChannelWrapper> removedChannelWrappers = new ArrayList<>(); List<ChannelWrapper> updatedChannelWrappers = new ArrayList<>(); @@ -625,13 +671,15 @@ public class ChannelDataManager { boolean newlyAdded = !removedChannelIds.remove(channelId); ChannelWrapper channelWrapper; if (newlyAdded) { + new CheckChannelLogoExistTask(channel) + .executeOnExecutor(AsyncTask.SERIAL_EXECUTOR); channelWrapper = new ChannelWrapper(channel); - mChannelWrapperMap.put(channel.getId(), channelWrapper); + data.channelWrapperMap.put(channel.getId(), channelWrapper); if (!channelWrapper.mInputRemoved) { channelAdded = true; } } else { - channelWrapper = mChannelWrapperMap.get(channelId); + channelWrapper = data.channelWrapperMap.get(channelId); if (!channelWrapper.mChannel.hasSameReadOnlyInfo(channel)) { // Channel data updated Channel oldChannel = channelWrapper.mChannel; @@ -640,9 +688,9 @@ public class ChannelDataManager { // {@link #applyUpdatedValuesToDb} is called. Therefore, the value // between DB and ChannelDataManager could be different for a while. // Therefore, we'll keep the values in ChannelDataManager. - channelWrapper.mChannel.copyFrom(channel); channel.setBrowsable(oldChannel.isBrowsable()); channel.setLocked(oldChannel.isLocked()); + channelWrapper.mChannel.copyFrom(channel); if (!channelWrapper.mInputRemoved) { channelUpdated = true; updatedChannelWrappers.add(channelWrapper); @@ -663,19 +711,19 @@ public class ChannelDataManager { } for (long id : removedChannelIds) { - ChannelWrapper channelWrapper = mChannelWrapperMap.remove(id); + ChannelWrapper channelWrapper = data.channelWrapperMap.remove(id); if (!channelWrapper.mInputRemoved) { channelRemoved = true; removedChannelWrappers.add(channelWrapper); } } - clearChannels(); - for (ChannelWrapper channelWrapper : mChannelWrapperMap.values()) { + for (ChannelWrapper channelWrapper : data.channelWrapperMap.values()) { if (!channelWrapper.mInputRemoved) { - addChannel(channelWrapper.mChannel); + addChannel(data, channelWrapper.mChannel); } } - Collections.sort(mChannels, mChannelComparator); + Collections.sort(data.channels, mChannelComparator); + mData = new UnmodifiableChannelData(data); if (!mDbLoadFinished) { mDbLoadFinished = true; @@ -693,7 +741,6 @@ public class ChannelDataManager { r.run(); } mPostRunnablesAfterChannelUpdate.clear(); - ChannelLogoFetcher.startFetchingChannelLogos(mContext); } } @@ -705,10 +752,9 @@ public class ChannelDataManager { private void updateOneColumnValue( final String columnName, final int columnValue, final List<Long> ids) { if (!PermissionUtils.hasAccessAllEpg(mContext)) { - // TODO: support this feature for non-system LC app. b/23939816 return; } - AsyncDbTask.execute(new Runnable() { + AsyncDbTask.executeOnDbThread(new Runnable() { @Override public void run() { String selection = Utils.buildSelectionForIds(Channels._ID, ids); @@ -723,6 +769,7 @@ public class ChannelDataManager { return channel.getInputId() + "|" + channel.getId(); } + @MainThread private static class ChannelDataManagerHandler extends WeakHandler<ChannelDataManager> { public ChannelDataManagerHandler(ChannelDataManager channelDataManager) { super(Looper.getMainLooper(), channelDataManager); @@ -735,4 +782,51 @@ public class ChannelDataManager { } } } + + /** + * Container class which includes channel data that needs to be synced. This class is + * modifiable and used for changing channel data. + * e.g. TvInputCallback, or AsyncDbTask.onPostExecute. + */ + @MainThread + private static class ChannelData { + final Map<Long, ChannelWrapper> channelWrapperMap; + final Map<String, MutableInt> channelCountMap; + final List<Channel> channels; + + ChannelData() { + channelWrapperMap = new HashMap<>(); + channelCountMap = new HashMap<>(); + channels = new ArrayList<>(); + } + + ChannelData(ChannelData data) { + channelWrapperMap = new HashMap<>(data.channelWrapperMap); + channelCountMap = new HashMap<>(data.channelCountMap); + channels = new ArrayList<>(data.channels); + } + + ChannelData(Map<Long, ChannelWrapper> channelWrapperMap, + Map<String, MutableInt> channelCountMap, List<Channel> channels) { + this.channelWrapperMap = channelWrapperMap; + this.channelCountMap = channelCountMap; + this.channels = channels; + } + } + + /** Unmodifiable channel data. */ + @MainThread + private static class UnmodifiableChannelData extends ChannelData { + UnmodifiableChannelData() { + super(Collections.unmodifiableMap(new HashMap<>()), + Collections.unmodifiableMap(new HashMap<>()), + Collections.unmodifiableList(new ArrayList<>())); + } + + UnmodifiableChannelData(ChannelData data) { + super(Collections.unmodifiableMap(data.channelWrapperMap), + Collections.unmodifiableMap(data.channelCountMap), + Collections.unmodifiableList(data.channels)); + } + } } |