diff options
author | Nick Chalko <nchalko@google.com> | 2015-08-03 15:39:56 -0700 |
---|---|---|
committer | Nick Chalko <nchalko@google.com> | 2015-08-03 15:53:37 -0700 |
commit | 816a4be1a0f34f6a48877c8afd3dbbca19eac435 (patch) | |
tree | 4f18dda269764494942f5313acc93db4a35d47db /src/com/android/tv/data/ChannelDataManager.java | |
parent | 6edd2b09e5d16a29c703a5fcbd2e88c5cf5e55b7 (diff) | |
download | TV-816a4be1a0f34f6a48877c8afd3dbbca19eac435.tar.gz |
Migrate Live Channels App Src to AOSP branch
Bug: 21625152
Change-Id: I07e2830b27440556dc757e6340b4f77d1c0cbc66
Diffstat (limited to 'src/com/android/tv/data/ChannelDataManager.java')
-rw-r--r-- | src/com/android/tv/data/ChannelDataManager.java | 644 |
1 files changed, 644 insertions, 0 deletions
diff --git a/src/com/android/tv/data/ChannelDataManager.java b/src/com/android/tv/data/ChannelDataManager.java new file mode 100644 index 00000000..d09d1686 --- /dev/null +++ b/src/com/android/tv/data/ChannelDataManager.java @@ -0,0 +1,644 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tv.data; + +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.database.ContentObserver; +import android.media.tv.TvContract; +import android.media.tv.TvContract.Channels; +import android.media.tv.TvInputManager.TvInputCallback; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.support.annotation.VisibleForTesting; +import android.util.Log; +import android.util.MutableInt; + +import com.android.tv.util.AsyncDbTask; +import com.android.tv.util.TvInputManagerHelper; +import com.android.tv.util.Utils; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * The class to manage channel data. + * Basic features: reading channel list and each channel's current program, and updating + * the values of {@link Channels#COLUMN_BROWSABLE}, {@link Channels#COLUMN_LOCKED}. + * This class is not thread-safe and under an assumption that its public methods are called in + * only the main thread. + */ +public class ChannelDataManager { + private static final String TAG = "ChannelDataManager"; + private static final boolean DEBUG = false; + + private static final int MSG_UPDATE_CHANNELS = 1000; + + private final Context mContext; + private final TvInputManagerHelper mInputManager; + private boolean mStarted; + private boolean mDbLoadFinished; + private QueryAllChannelsTask mChannelsUpdateTask; + private final List<Runnable> mPostRunnablesAfterChannelUpdate = new ArrayList<>(); + + private final Set<Listener> mListeners = new HashSet<>(); + private final Map<Long, ChannelWrapper> mChannelWrapperMap = new HashMap<>(); + private final Map<String, MutableInt> mChannelCountMap = new HashMap<>(); + private final Channel.DefaultComparator mChannelComparator; + private final List<Channel> mChannels = new ArrayList<>(); + + private final Handler mHandler; + private final Set<Long> mBrowsableUpdateChannelIds = new HashSet<>(); + private final Set<Long> mLockedUpdateChannelIds = new HashSet<>(); + + private final ContentResolver mContentResolver; + private final ContentObserver mChannelObserver; + + private final TvInputCallback mTvInputCallback = new TvInputCallback() { + @Override + public void onInputAdded(String inputId) { + boolean channelAdded = false; + for (ChannelWrapper channel : mChannelWrapperMap.values()) { + if (channel.mChannel.getInputId().equals(inputId)) { + channel.mInputRemoved = false; + addChannel(channel.mChannel); + channelAdded = true; + } + } + if (channelAdded) { + Collections.sort(mChannels, mChannelComparator); + for (Listener l : mListeners) { + l.onChannelListUpdated(); + } + } + } + + @Override + public void onInputRemoved(String inputId) { + boolean channelRemoved = false; + ArrayList<ChannelWrapper> removedChannels = new ArrayList<>(); + for (ChannelWrapper channel : mChannelWrapperMap.values()) { + if (channel.mChannel.getInputId().equals(inputId)) { + channel.mInputRemoved = true; + channelRemoved = true; + removedChannels.add(channel); + } + } + if (channelRemoved) { + clearChannels(); + for (ChannelWrapper channelWrapper : mChannelWrapperMap.values()) { + if (!channelWrapper.mInputRemoved) { + addChannel(channelWrapper.mChannel); + } + } + Collections.sort(mChannels, mChannelComparator); + for (Listener l : mListeners) { + l.onChannelListUpdated(); + } + for (ChannelWrapper channel : removedChannels) { + channel.notifyChannelRemoved(); + } + } + } + }; + + public ChannelDataManager(Context context, TvInputManagerHelper inputManager) { + this(context, inputManager, context.getContentResolver(), Looper.myLooper()); + } + + @VisibleForTesting + ChannelDataManager(Context context, TvInputManagerHelper inputManager, + ContentResolver contentResolver, Looper looper) { + mContext = context; + mInputManager = inputManager; + mContentResolver = contentResolver; + mChannelComparator = new Channel.DefaultComparator(context, inputManager); + // Detect duplicate channels while sorting. + mChannelComparator.setDetectDuplicatesEnabled(true); + mHandler = new Handler(looper) { + @Override + public void handleMessage(Message msg) { + if (msg.what == MSG_UPDATE_CHANNELS) { + handleUpdateChannels(); + } + } + }; + mChannelObserver = new ContentObserver(mHandler) { + @Override + public void onChange(boolean selfChange) { + if (!mHandler.hasMessages(MSG_UPDATE_CHANNELS)) { + mHandler.sendEmptyMessage(MSG_UPDATE_CHANNELS); + } + } + }; + } + + @VisibleForTesting + ContentObserver getContentObserver() { + return mChannelObserver; + } + + /** + * Starts the manager. If data is ready, {@link Listener#onLoadFinished()} will be called. + */ + public void start() { + if (mStarted) { + return; + } + mStarted = true; + // Should be called directly instead of posting MSG_UPDATE_CHANNELS message to the handler. + // If not, other DB tasks can be executed before channel loading. + handleUpdateChannels(); + mContentResolver.registerContentObserver( + TvContract.Channels.CONTENT_URI, true, mChannelObserver); + mInputManager.addCallback(mTvInputCallback); + } + + /** + * Stops the manager. It clears manager states and runs pending DB operations. Added listeners + * aren't automatically removed by this method. + */ + public void stop() { + if (!mStarted) { + return; + } + mStarted = false; + mDbLoadFinished = false; + + ChannelLogoFetcher.stopFetchingChannelLogos(); + mInputManager.removeCallback(mTvInputCallback); + mContentResolver.unregisterContentObserver(mChannelObserver); + mHandler.removeCallbacksAndMessages(null); + + mChannelWrapperMap.clear(); + clearChannels(); + mPostRunnablesAfterChannelUpdate.clear(); + if (mChannelsUpdateTask != null) { + mChannelsUpdateTask.cancel(true); + mChannelsUpdateTask = null; + } + applyUpdatedValuesToDb(); + } + + /** + * Adds a {@link Listener}. + */ + public void addListener(Listener listener) { + mListeners.add(listener); + } + + /** + * Removes a {@link Listener}. + */ + public void removeListener(Listener listener) { + mListeners.remove(listener); + } + + /** + * 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); + if (channelWrapper == null) { + return; + } + channelWrapper.addListener(listener); + } + + /** + * Removes a {@link ChannelListener} for a specific channel with the channel ID + * {@code channelId}. + */ + public void removeChannelListener(Long channelId, ChannelListener listener) { + ChannelWrapper channelWrapper = mChannelWrapperMap.get(channelId); + if (channelWrapper == null) { + return; + } + channelWrapper.removeListener(listener); + } + + /** + * Checks whether data is ready. + */ + public boolean isDbLoadFinished() { + return mDbLoadFinished; + } + + /** + * Returns the number of channels. + */ + public int getChannelCount() { + return mChannels.size(); + } + + /** + * Returns a list of channels. + */ + public List<Channel> getChannelList() { + return Collections.unmodifiableList(mChannels); + } + + /** + * Returns a list of browsable channels. + */ + public List<Channel> getBrowsableChannelList() { + List<Channel> channels = new ArrayList<>(); + for (Channel channel : mChannels) { + if (channel.isBrowsable()) { + channels.add(channel); + } + } + return Collections.unmodifiableList(channels); + } + + /** + * Returns the total channel count for a given input. + * + * @param inputId The ID of the input. + */ + public int getChannelCountForInput(String inputId) { + MutableInt count = mChannelCountMap.get(inputId); + return count == null ? 0 : count.value; + } + + /** + * 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) { + if (channel.isBrowsable()) { + return false; + } + } + return true; + } + + /** + * Gets the channel with the channel ID {@code channelId}. + */ + public Channel getChannel(Long channelId) { + ChannelWrapper channelWrapper = mChannelWrapperMap.get(channelId); + if (channelWrapper == null || channelWrapper.mInputRemoved) { + return null; + } + return channelWrapper.mChannel; + } + + /** + * The value change will be applied to DB when applyPendingDbOperation is called. + */ + public void updateBrowsable(Long channelId, boolean browsable) { + updateBrowsable(channelId, browsable, false); + } + + /** + * The value change will be applied to DB when applyPendingDbOperation is called. + * + * @param skipNotifyChannelBrowsableChanged If it's true, {@link Listener + * #onChannelBrowsableChanged()} is not called, when this method is called. + * {@link #notifyChannelBrowsableChanged} should be directly called, once browsable + * update is completed. + */ + public void updateBrowsable(Long channelId, boolean browsable, + boolean skipNotifyChannelBrowsableChanged) { + ChannelWrapper channelWrapper = mChannelWrapperMap.get(channelId); + if (channelWrapper == null) { + return; + } + if (channelWrapper.mChannel.isBrowsable() != browsable) { + channelWrapper.mChannel.setBrowsable(browsable); + if (browsable == channelWrapper.mBrowsableInDb) { + mBrowsableUpdateChannelIds.remove(channelWrapper.mChannel.getId()); + } else { + mBrowsableUpdateChannelIds.add(channelWrapper.mChannel.getId()); + } + channelWrapper.notifyChannelUpdated(); + // When updateBrowsable is called multiple times in a method, we don't need to + // notify Listener.onChannelBrowsableChanged multiple times but only once. So + // we send a message instead of directly calling onChannelBrowsableChanged. + if (!skipNotifyChannelBrowsableChanged) { + notifyChannelBrowsableChanged(); + } + } + } + + public void notifyChannelBrowsableChanged() { + for (Listener l : mListeners) { + l.onChannelBrowsableChanged(); + } + } + + /** + * Updates channels from DB. Once the update is done, {@code postRunnable} will + * be called. + */ + public void updateChannels(Runnable postRunnable) { + if (mChannelsUpdateTask != null) { + mChannelsUpdateTask.cancel(true); + mChannelsUpdateTask = null; + } + mPostRunnablesAfterChannelUpdate.add(postRunnable); + if (!mHandler.hasMessages(MSG_UPDATE_CHANNELS)) { + mHandler.sendEmptyMessage(MSG_UPDATE_CHANNELS); + } + } + + /** + * The value change will be applied to DB when applyPendingDbOperation is called. + */ + public void updateLocked(Long channelId, boolean locked) { + ChannelWrapper channelWrapper = mChannelWrapperMap.get(channelId); + if (channelWrapper == null) { + return; + } + if (channelWrapper.mChannel.isLocked() != locked) { + channelWrapper.mChannel.setLocked(locked); + if (locked == channelWrapper.mLockedInDb) { + mLockedUpdateChannelIds.remove(channelWrapper.mChannel.getId()); + } else { + mLockedUpdateChannelIds.add(channelWrapper.mChannel.getId()); + } + channelWrapper.notifyChannelUpdated(); + } + } + + /** + * Applies the changed values by {@link #updateBrowsable} and {@link #updateLocked} + * to DB. + */ + public void applyUpdatedValuesToDb() { + ArrayList<Long> browsableIds = new ArrayList<>(); + ArrayList<Long> unbrowsableIds = new ArrayList<>(); + for (Long id : mBrowsableUpdateChannelIds) { + ChannelWrapper channelWrapper = mChannelWrapperMap.get(id); + if (channelWrapper == null) { + continue; + } + if (channelWrapper.mChannel.isBrowsable()) { + browsableIds.add(id); + } else { + unbrowsableIds.add(id); + } + channelWrapper.mBrowsableInDb = channelWrapper.mChannel.isBrowsable(); + } + String column = TvContract.Channels.COLUMN_BROWSABLE; + if (browsableIds.size() != 0) { + updateOneColumnValue(column, 1, browsableIds); + } + if (unbrowsableIds.size() != 0) { + updateOneColumnValue(column, 0, unbrowsableIds); + } + mBrowsableUpdateChannelIds.clear(); + + ArrayList<Long> lockedIds = new ArrayList<>(); + ArrayList<Long> unlockedIds = new ArrayList<>(); + for (Long id : mLockedUpdateChannelIds) { + ChannelWrapper channelWrapper = mChannelWrapperMap.get(id); + if (channelWrapper == null) { + continue; + } + if (channelWrapper.mChannel.isLocked()) { + lockedIds.add(id); + } else { + unlockedIds.add(id); + } + channelWrapper.mLockedInDb = channelWrapper.mChannel.isLocked(); + } + column = TvContract.Channels.COLUMN_LOCKED; + if (lockedIds.size() != 0) { + updateOneColumnValue(column, 1, lockedIds); + } + if (unlockedIds.size() != 0) { + updateOneColumnValue(column, 0, unlockedIds); + } + mLockedUpdateChannelIds.clear(); + if (DEBUG) { + Log.d(TAG, "applyUpdatedValuesToDb" + + "\n browsableIds size:" + browsableIds.size() + + "\n unbrowsableIds size:" + unbrowsableIds.size() + + "\n lockedIds size:" + lockedIds.size() + + "\n unlockedIds size:" + unlockedIds.size()); + } + } + + private void addChannel(Channel channel) { + mChannels.add(channel); + String inputId = channel.getInputId(); + MutableInt count = mChannelCountMap.get(inputId); + if (count == null) { + mChannelCountMap.put(inputId, new MutableInt(1)); + } else { + count.value++; + } + } + + private void clearChannels() { + mChannels.clear(); + mChannelCountMap.clear(); + } + + private void handleUpdateChannels() { + if (mChannelsUpdateTask != null) { + mChannelsUpdateTask.cancel(true); + } + mChannelsUpdateTask = new QueryAllChannelsTask(mContentResolver); + mChannelsUpdateTask.executeOnDbThread(); + } + + public interface Listener { + /** + * Called when data load is finished. + */ + void onLoadFinished(); + + /** + * Called when channels are added, deleted, or updated. But, when browsable is changed, + * it won't be called. Instead, {@link #onChannelBrowsableChanged} will be called. + */ + void onChannelListUpdated(); + + /** + * Called when browsable of channels are changed. + */ + void onChannelBrowsableChanged(); + } + + public interface ChannelListener { + /** + * Called when the channel has been removed in DB. + */ + void onChannelRemoved(Channel channel); + + /** + * Called when values of the channel has been changed. + */ + void onChannelUpdated(Channel channel); + } + + private class ChannelWrapper { + final Set<ChannelListener> mChannelListeners = new HashSet<>(); + final Channel mChannel; + boolean mBrowsableInDb; + boolean mLockedInDb; + boolean mInputRemoved; + + ChannelWrapper(Channel channel) { + mChannel = channel; + mBrowsableInDb = channel.isBrowsable(); + mLockedInDb = channel.isLocked(); + mInputRemoved = !mInputManager.hasTvInputInfo(channel.getInputId()); + } + + void addListener(ChannelListener listener) { + mChannelListeners.add(listener); + } + + void removeListener(ChannelListener listener) { + mChannelListeners.remove(listener); + } + + void notifyChannelUpdated() { + for (ChannelListener l : mChannelListeners) { + l.onChannelUpdated(mChannel); + } + } + + void notifyChannelRemoved() { + for (ChannelListener l : mChannelListeners) { + l.onChannelRemoved(mChannel); + } + } + } + + private final class QueryAllChannelsTask extends AsyncDbTask.AsyncChannelQueryTask { + + public QueryAllChannelsTask(ContentResolver contentResolver) { + super(contentResolver); + } + + @Override + protected void onPostExecute(List<Channel> channels) { + mChannelsUpdateTask = null; + if (channels == null) { + if (DEBUG) Log.e(TAG, "onPostExecute with null channels"); + return; + } + Set<Long> removedChannelIds = new HashSet<>(mChannelWrapperMap.keySet()); + List<ChannelWrapper> removedChannelWrappers = new ArrayList<>(); + List<ChannelWrapper> updatedChannelWrappers = new ArrayList<>(); + + boolean channelAdded = false; + boolean channelUpdated = false; + boolean channelRemoved = false; + for (Channel channel : channels) { + long channelId = channel.getId(); + boolean newlyAdded = !removedChannelIds.remove(channelId); + ChannelWrapper channelWrapper; + if (newlyAdded) { + channelWrapper = new ChannelWrapper(channel); + mChannelWrapperMap.put(channel.getId(), channelWrapper); + if (!channelWrapper.mInputRemoved) { + channelAdded = true; + } + } else { + channelWrapper = mChannelWrapperMap.get(channelId); + if (!channelWrapper.mChannel.hasSameReadOnlyInfo(channel)) { + // Channel data updated + Channel oldChannel = channelWrapper.mChannel; + // We assume that mBrowsable and mLocked are controlled by only TV app. + // The values for mBrowsable and mLocked are updated when + // {@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()); + if (!channelWrapper.mInputRemoved) { + channelUpdated = true; + updatedChannelWrappers.add(channelWrapper); + } + } + } + } + + for (long id : removedChannelIds) { + ChannelWrapper channelWrapper = mChannelWrapperMap.remove(id); + if (!channelWrapper.mInputRemoved) { + channelRemoved = true; + removedChannelWrappers.add(channelWrapper); + } + } + clearChannels(); + for (ChannelWrapper channelWrapper : mChannelWrapperMap.values()) { + if (!channelWrapper.mInputRemoved) { + addChannel(channelWrapper.mChannel); + } + } + Collections.sort(mChannels, mChannelComparator); + + if (!mDbLoadFinished) { + mDbLoadFinished = true; + for (Listener l : mListeners) { + l.onLoadFinished(); + } + } else if (channelAdded || channelUpdated || channelRemoved) { + for (Listener l : mListeners) { + l.onChannelListUpdated(); + } + } + for (ChannelWrapper channelWrapper : removedChannelWrappers) { + channelWrapper.notifyChannelRemoved(); + } + for (ChannelWrapper channelWrapper : updatedChannelWrappers) { + channelWrapper.notifyChannelUpdated(); + } + for (Runnable r : mPostRunnablesAfterChannelUpdate) { + r.run(); + } + mPostRunnablesAfterChannelUpdate.clear(); + ChannelLogoFetcher.startFetchingChannelLogos(mContext); + } + } + + /** + * Updates a column {@code columnName} of DB table {@code uri} with the value + * {@code columnValue}. The selective rows in the ID list {@code ids} will be updated. + * The DB operations will run on {@link AsyncDbTask#getExecutor()}. + */ + private void updateOneColumnValue( + final String columnName, final int columnValue, final List<Long> ids) { + AsyncDbTask.execute(new Runnable() { + @Override + public void run() { + String selection = Utils.buildSelectionForIds(Channels._ID, ids); + ContentValues values = new ContentValues(); + values.put(columnName, columnValue); + mContentResolver.update(TvContract.Channels.CONTENT_URI, values, selection, null); + } + }); + } +} |