diff options
Diffstat (limited to 'src/com/android/tv/data/ChannelImpl.java')
-rw-r--r-- | src/com/android/tv/data/ChannelImpl.java | 777 |
1 files changed, 777 insertions, 0 deletions
diff --git a/src/com/android/tv/data/ChannelImpl.java b/src/com/android/tv/data/ChannelImpl.java new file mode 100644 index 00000000..703f69c9 --- /dev/null +++ b/src/com/android/tv/data/ChannelImpl.java @@ -0,0 +1,777 @@ +/* + * 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.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.database.Cursor; +import android.media.tv.TvContract; +import android.media.tv.TvInputInfo; +import android.net.Uri; +import android.support.annotation.Nullable; +import android.support.annotation.UiThread; +import android.support.annotation.VisibleForTesting; +import android.text.TextUtils; +import android.util.Log; +import com.android.tv.common.CommonConstants; +import com.android.tv.common.util.CommonUtils; +import com.android.tv.data.api.Channel; +import com.android.tv.util.TvInputManagerHelper; +import com.android.tv.util.Utils; +import com.android.tv.util.images.ImageLoader; +import java.net.URISyntaxException; +import java.util.Comparator; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +/** A convenience class to create and insert channel entries into the database. */ +public final class ChannelImpl implements Channel { + private static final String TAG = "ChannelImpl"; + + /** Compares the channel numbers of channels which belong to the same input. */ + public static final Comparator<Channel> CHANNEL_NUMBER_COMPARATOR = + new Comparator<Channel>() { + @Override + public int compare(Channel lhs, Channel rhs) { + return ChannelNumber.compare(lhs.getDisplayNumber(), rhs.getDisplayNumber()); + } + }; + + private static final int APP_LINK_TYPE_NOT_SET = 0; + private static final String INVALID_PACKAGE_NAME = "packageName"; + + public static final String[] PROJECTION = { + // Columns must match what is read in ChannelImpl.fromCursor() + TvContract.Channels._ID, + TvContract.Channels.COLUMN_PACKAGE_NAME, + TvContract.Channels.COLUMN_INPUT_ID, + TvContract.Channels.COLUMN_TYPE, + TvContract.Channels.COLUMN_DISPLAY_NUMBER, + TvContract.Channels.COLUMN_DISPLAY_NAME, + TvContract.Channels.COLUMN_DESCRIPTION, + TvContract.Channels.COLUMN_VIDEO_FORMAT, + TvContract.Channels.COLUMN_BROWSABLE, + TvContract.Channels.COLUMN_SEARCHABLE, + TvContract.Channels.COLUMN_LOCKED, + TvContract.Channels.COLUMN_APP_LINK_TEXT, + TvContract.Channels.COLUMN_APP_LINK_COLOR, + TvContract.Channels.COLUMN_APP_LINK_ICON_URI, + TvContract.Channels.COLUMN_APP_LINK_POSTER_ART_URI, + TvContract.Channels.COLUMN_APP_LINK_INTENT_URI, + TvContract.Channels.COLUMN_INTERNAL_PROVIDER_FLAG2, // Only used in bundled input + }; + + /** + * Creates {@code ChannelImpl} object from cursor. + * + * <p>The query that created the cursor MUST use {@link #PROJECTION} + */ + public static ChannelImpl fromCursor(Cursor cursor) { + // Columns read must match the order of {@link #PROJECTION} + ChannelImpl channel = new ChannelImpl(); + int index = 0; + channel.mId = cursor.getLong(index++); + channel.mPackageName = Utils.intern(cursor.getString(index++)); + channel.mInputId = Utils.intern(cursor.getString(index++)); + channel.mType = Utils.intern(cursor.getString(index++)); + channel.mDisplayNumber = normalizeDisplayNumber(cursor.getString(index++)); + channel.mDisplayName = cursor.getString(index++); + channel.mDescription = cursor.getString(index++); + channel.mVideoFormat = Utils.intern(cursor.getString(index++)); + channel.mBrowsable = cursor.getInt(index++) == 1; + channel.mSearchable = cursor.getInt(index++) == 1; + channel.mLocked = cursor.getInt(index++) == 1; + channel.mAppLinkText = cursor.getString(index++); + channel.mAppLinkColor = cursor.getInt(index++); + channel.mAppLinkIconUri = cursor.getString(index++); + channel.mAppLinkPosterArtUri = cursor.getString(index++); + channel.mAppLinkIntentUri = cursor.getString(index++); + if (CommonUtils.isBundledInput(channel.mInputId)) { + channel.mRecordingProhibited = cursor.getInt(index++) != 0; + } + return channel; + } + + /** Replaces the channel number separator with dash('-'). */ + public static String normalizeDisplayNumber(String string) { + if (!TextUtils.isEmpty(string)) { + int length = string.length(); + for (int i = 0; i < length; i++) { + char c = string.charAt(i); + if (c == '.' + || Character.isWhitespace(c) + || Character.getType(c) == Character.DASH_PUNCTUATION) { + StringBuilder sb = new StringBuilder(string); + sb.setCharAt(i, CHANNEL_NUMBER_DELIMITER); + return sb.toString(); + } + } + } + return string; + } + + /** ID of this channel. Matches to BaseColumns._ID. */ + private long mId; + + private String mPackageName; + private String mInputId; + private String mType; + private String mDisplayNumber; + private String mDisplayName; + private String mDescription; + private String mVideoFormat; + private boolean mBrowsable; + private boolean mSearchable; + private boolean mLocked; + private boolean mIsPassthrough; + private String mAppLinkText; + private int mAppLinkColor; + private String mAppLinkIconUri; + private String mAppLinkPosterArtUri; + private String mAppLinkIntentUri; + private Intent mAppLinkIntent; + private int mAppLinkType; + private String mLogoUri; + private boolean mRecordingProhibited; + + private boolean mChannelLogoExist; + + private ChannelImpl() { + // Do nothing. + } + + @Override + public long getId() { + return mId; + } + + @Override + public Uri getUri() { + if (isPassthrough()) { + return TvContract.buildChannelUriForPassthroughInput(mInputId); + } else { + return TvContract.buildChannelUri(mId); + } + } + + @Override + public String getPackageName() { + return mPackageName; + } + + @Override + public String getInputId() { + return mInputId; + } + + @Override + public String getType() { + return mType; + } + + @Override + public String getDisplayNumber() { + return mDisplayNumber; + } + + @Override + @Nullable + public String getDisplayName() { + return mDisplayName; + } + + @Override + public String getDescription() { + return mDescription; + } + + @Override + public String getVideoFormat() { + return mVideoFormat; + } + + @Override + public boolean isPassthrough() { + return mIsPassthrough; + } + + /** + * Gets identification text for displaying or debugging. It's made from Channels' display number + * plus their display name. + */ + @Override + public String getDisplayText() { + return TextUtils.isEmpty(mDisplayName) + ? mDisplayNumber + : mDisplayNumber + " " + mDisplayName; + } + + @Override + public String getAppLinkText() { + return mAppLinkText; + } + + @Override + public int getAppLinkColor() { + return mAppLinkColor; + } + + @Override + public String getAppLinkIconUri() { + return mAppLinkIconUri; + } + + @Override + public String getAppLinkPosterArtUri() { + return mAppLinkPosterArtUri; + } + + @Override + public String getAppLinkIntentUri() { + return mAppLinkIntentUri; + } + + /** Returns channel logo uri which is got from cloud, it's used only for ChannelLogoFetcher. */ + @Override + public String getLogoUri() { + return mLogoUri; + } + + @Override + public boolean isRecordingProhibited() { + return mRecordingProhibited; + } + + /** Checks whether this channel is physical tuner channel or not. */ + @Override + public boolean isPhysicalTunerChannel() { + return !TextUtils.isEmpty(mType) && !TvContract.Channels.TYPE_OTHER.equals(mType); + } + + /** Checks if two channels equal by checking ids. */ + @Override + public boolean equals(Object o) { + if (!(o instanceof ChannelImpl)) { + return false; + } + ChannelImpl other = (ChannelImpl) o; + // All pass-through TV channels have INVALID_ID value for mId. + return mId == other.mId + && TextUtils.equals(mInputId, other.mInputId) + && mIsPassthrough == other.mIsPassthrough; + } + + @Override + public int hashCode() { + return Objects.hash(mId, mInputId, mIsPassthrough); + } + + @Override + public boolean isBrowsable() { + return mBrowsable; + } + + /** Checks whether this channel is searchable or not. */ + @Override + public boolean isSearchable() { + return mSearchable; + } + + @Override + public boolean isLocked() { + return mLocked; + } + + public void setBrowsable(boolean browsable) { + mBrowsable = browsable; + } + + public void setLocked(boolean locked) { + mLocked = locked; + } + + /** Sets channel logo uri which is got from cloud. */ + public void setLogoUri(String logoUri) { + mLogoUri = logoUri; + } + + /** + * Check whether {@code other} has same read-only channel info as this. But, it cannot check two + * channels have same logos. It also excludes browsable and locked, because two fields are + * changed by TV app. + */ + @Override + public boolean hasSameReadOnlyInfo(Channel other) { + return other != null + && Objects.equals(mId, other.getId()) + && Objects.equals(mPackageName, other.getPackageName()) + && Objects.equals(mInputId, other.getInputId()) + && Objects.equals(mType, other.getType()) + && Objects.equals(mDisplayNumber, other.getDisplayNumber()) + && Objects.equals(mDisplayName, other.getDisplayName()) + && Objects.equals(mDescription, other.getDescription()) + && Objects.equals(mVideoFormat, other.getVideoFormat()) + && mIsPassthrough == other.isPassthrough() + && Objects.equals(mAppLinkText, other.getAppLinkText()) + && mAppLinkColor == other.getAppLinkColor() + && Objects.equals(mAppLinkIconUri, other.getAppLinkIconUri()) + && Objects.equals(mAppLinkPosterArtUri, other.getAppLinkPosterArtUri()) + && Objects.equals(mAppLinkIntentUri, other.getAppLinkIntentUri()) + && Objects.equals(mRecordingProhibited, other.isRecordingProhibited()); + } + + @Override + public String toString() { + return "Channel{" + + "id=" + + mId + + ", packageName=" + + mPackageName + + ", inputId=" + + mInputId + + ", type=" + + mType + + ", displayNumber=" + + mDisplayNumber + + ", displayName=" + + mDisplayName + + ", description=" + + mDescription + + ", videoFormat=" + + mVideoFormat + + ", isPassthrough=" + + mIsPassthrough + + ", browsable=" + + mBrowsable + + ", searchable=" + + mSearchable + + ", locked=" + + mLocked + + ", appLinkText=" + + mAppLinkText + + ", recordingProhibited=" + + mRecordingProhibited + + "}"; + } + + @Override + public void copyFrom(Channel channel) { + if (channel instanceof ChannelImpl) { + copyFrom((ChannelImpl) channel); + } else { + // copy what we can + mId = channel.getId(); + mPackageName = channel.getPackageName(); + mInputId = channel.getInputId(); + mType = channel.getType(); + mDisplayNumber = channel.getDisplayNumber(); + mDisplayName = channel.getDisplayName(); + mDescription = channel.getDescription(); + mVideoFormat = channel.getVideoFormat(); + mIsPassthrough = channel.isPassthrough(); + mBrowsable = channel.isBrowsable(); + mSearchable = channel.isSearchable(); + mLocked = channel.isLocked(); + mAppLinkText = channel.getAppLinkText(); + mAppLinkColor = channel.getAppLinkColor(); + mAppLinkIconUri = channel.getAppLinkIconUri(); + mAppLinkPosterArtUri = channel.getAppLinkPosterArtUri(); + mAppLinkIntentUri = channel.getAppLinkIntentUri(); + mRecordingProhibited = channel.isRecordingProhibited(); + mChannelLogoExist = channel.channelLogoExists(); + } + } + + @SuppressWarnings("ReferenceEquality") + public void copyFrom(ChannelImpl channel) { + ChannelImpl other = (ChannelImpl) channel; + if (this == other) { + return; + } + mId = other.mId; + mPackageName = other.mPackageName; + mInputId = other.mInputId; + mType = other.mType; + mDisplayNumber = other.mDisplayNumber; + mDisplayName = other.mDisplayName; + mDescription = other.mDescription; + mVideoFormat = other.mVideoFormat; + mIsPassthrough = other.mIsPassthrough; + mBrowsable = other.mBrowsable; + mSearchable = other.mSearchable; + mLocked = other.mLocked; + mAppLinkText = other.mAppLinkText; + mAppLinkColor = other.mAppLinkColor; + mAppLinkIconUri = other.mAppLinkIconUri; + mAppLinkPosterArtUri = other.mAppLinkPosterArtUri; + mAppLinkIntentUri = other.mAppLinkIntentUri; + mAppLinkIntent = other.mAppLinkIntent; + mAppLinkType = other.mAppLinkType; + mRecordingProhibited = other.mRecordingProhibited; + mChannelLogoExist = other.mChannelLogoExist; + } + + /** Creates a channel for a passthrough TV input. */ + public static ChannelImpl createPassthroughChannel(Uri uri) { + if (!TvContract.isChannelUriForPassthroughInput(uri)) { + throw new IllegalArgumentException("URI is not a passthrough channel URI"); + } + String inputId = uri.getPathSegments().get(1); + return createPassthroughChannel(inputId); + } + + /** Creates a channel for a passthrough TV input with {@code inputId}. */ + public static ChannelImpl createPassthroughChannel(String inputId) { + return new Builder().setInputId(inputId).setPassthrough(true).build(); + } + + /** Checks whether the channel is valid or not. */ + public static boolean isValid(Channel channel) { + return channel != null && (channel.getId() != INVALID_ID || channel.isPassthrough()); + } + + /** + * Builder class for {@code ChannelImpl}. Suppress using this outside of ChannelDataManager so + * Channels could be managed by ChannelDataManager. + */ + public static final class Builder { + private final ChannelImpl mChannel; + + public Builder() { + mChannel = new ChannelImpl(); + // Fill initial data. + mChannel.mId = INVALID_ID; + mChannel.mPackageName = INVALID_PACKAGE_NAME; + mChannel.mInputId = "inputId"; + mChannel.mType = "type"; + mChannel.mDisplayNumber = "0"; + mChannel.mDisplayName = "name"; + mChannel.mDescription = "description"; + mChannel.mBrowsable = true; + mChannel.mSearchable = true; + } + + public Builder(Channel other) { + mChannel = new ChannelImpl(); + mChannel.copyFrom(other); + } + + @VisibleForTesting + public Builder setId(long id) { + mChannel.mId = id; + return this; + } + + @VisibleForTesting + public Builder setPackageName(String packageName) { + mChannel.mPackageName = packageName; + return this; + } + + public Builder setInputId(String inputId) { + mChannel.mInputId = inputId; + return this; + } + + public Builder setType(String type) { + mChannel.mType = type; + return this; + } + + @VisibleForTesting + public Builder setDisplayNumber(String displayNumber) { + mChannel.mDisplayNumber = normalizeDisplayNumber(displayNumber); + return this; + } + + @VisibleForTesting + public Builder setDisplayName(String displayName) { + mChannel.mDisplayName = displayName; + return this; + } + + @VisibleForTesting + public Builder setDescription(String description) { + mChannel.mDescription = description; + return this; + } + + public Builder setVideoFormat(String videoFormat) { + mChannel.mVideoFormat = videoFormat; + return this; + } + + public Builder setBrowsable(boolean browsable) { + mChannel.mBrowsable = browsable; + return this; + } + + public Builder setSearchable(boolean searchable) { + mChannel.mSearchable = searchable; + return this; + } + + public Builder setLocked(boolean locked) { + mChannel.mLocked = locked; + return this; + } + + public Builder setPassthrough(boolean isPassthrough) { + mChannel.mIsPassthrough = isPassthrough; + return this; + } + + @VisibleForTesting + public Builder setAppLinkText(String appLinkText) { + mChannel.mAppLinkText = appLinkText; + return this; + } + + public Builder setAppLinkColor(int appLinkColor) { + mChannel.mAppLinkColor = appLinkColor; + return this; + } + + public Builder setAppLinkIconUri(String appLinkIconUri) { + mChannel.mAppLinkIconUri = appLinkIconUri; + return this; + } + + public Builder setAppLinkPosterArtUri(String appLinkPosterArtUri) { + mChannel.mAppLinkPosterArtUri = appLinkPosterArtUri; + return this; + } + + @VisibleForTesting + public Builder setAppLinkIntentUri(String appLinkIntentUri) { + mChannel.mAppLinkIntentUri = appLinkIntentUri; + return this; + } + + public Builder setRecordingProhibited(boolean recordingProhibited) { + mChannel.mRecordingProhibited = recordingProhibited; + return this; + } + + public ChannelImpl build() { + ChannelImpl channel = new ChannelImpl(); + channel.copyFrom(mChannel); + return channel; + } + } + + /** Prefetches the images for this channel. */ + public void prefetchImage(Context context, int type, int maxWidth, int maxHeight) { + String uriString = getImageUriString(type); + if (!TextUtils.isEmpty(uriString)) { + ImageLoader.prefetchBitmap(context, uriString, maxWidth, maxHeight); + } + } + + /** + * Loads the bitmap of this channel and returns it via {@code callback}. The loaded bitmap will + * be cached and resized with given params. + * + * <p>Note that it may directly call {@code callback} if the bitmap is already loaded. + * + * @param context A context. + * @param type The type of bitmap which will be loaded. It should be one of follows: {@link + * Channel#LOAD_IMAGE_TYPE_CHANNEL_LOGO}, {@link Channel#LOAD_IMAGE_TYPE_APP_LINK_ICON}, or + * {@link Channel#LOAD_IMAGE_TYPE_APP_LINK_POSTER_ART}. + * @param maxWidth The max width of the loaded bitmap. + * @param maxHeight The max height of the loaded bitmap. + * @param callback A callback which will be called after the loading finished. + */ + @UiThread + public void loadBitmap( + Context context, + final int type, + int maxWidth, + int maxHeight, + ImageLoader.ImageLoaderCallback callback) { + String uriString = getImageUriString(type); + ImageLoader.loadBitmap(context, uriString, maxWidth, maxHeight, callback); + } + + /** + * Sets if the channel logo exists. This method should be only called from {@link + * ChannelDataManager}. + */ + @Override + public void setChannelLogoExist(boolean exist) { + mChannelLogoExist = exist; + } + + /** Returns if channel logo exists. */ + public boolean channelLogoExists() { + return mChannelLogoExist; + } + + /** + * Returns the type of app link for this channel. It returns {@link + * Channel#APP_LINK_TYPE_CHANNEL} if the channel has a non null app link text and a valid app + * link intent, it returns {@link Channel#APP_LINK_TYPE_APP} if the input service which holds + * the channel has leanback launch intent, and it returns {@link Channel#APP_LINK_TYPE_NONE} + * otherwise. + */ + public int getAppLinkType(Context context) { + if (mAppLinkType == APP_LINK_TYPE_NOT_SET) { + initAppLinkTypeAndIntent(context); + } + return mAppLinkType; + } + + /** + * Returns the app link intent for this channel. If the type of app link is {@link + * Channel#APP_LINK_TYPE_NONE}, it returns {@code null}. + */ + public Intent getAppLinkIntent(Context context) { + if (mAppLinkType == APP_LINK_TYPE_NOT_SET) { + initAppLinkTypeAndIntent(context); + } + return mAppLinkIntent; + } + + private void initAppLinkTypeAndIntent(Context context) { + mAppLinkType = APP_LINK_TYPE_NONE; + mAppLinkIntent = null; + PackageManager pm = context.getPackageManager(); + if (!TextUtils.isEmpty(mAppLinkText) && !TextUtils.isEmpty(mAppLinkIntentUri)) { + try { + Intent intent = Intent.parseUri(mAppLinkIntentUri, Intent.URI_INTENT_SCHEME); + if (intent.resolveActivityInfo(pm, 0) != null) { + mAppLinkIntent = intent; + mAppLinkIntent.putExtra( + CommonConstants.EXTRA_APP_LINK_CHANNEL_URI, getUri().toString()); + mAppLinkType = APP_LINK_TYPE_CHANNEL; + return; + } else { + Log.w(TAG, "No activity exists to handle : " + mAppLinkIntentUri); + } + } catch (URISyntaxException e) { + Log.w(TAG, "Unable to set app link for " + mAppLinkIntentUri, e); + // Do nothing. + } + } + if (mPackageName.equals(context.getApplicationContext().getPackageName())) { + return; + } + mAppLinkIntent = pm.getLeanbackLaunchIntentForPackage(mPackageName); + if (mAppLinkIntent != null) { + mAppLinkIntent.putExtra( + CommonConstants.EXTRA_APP_LINK_CHANNEL_URI, getUri().toString()); + mAppLinkType = APP_LINK_TYPE_APP; + } + } + + private String getImageUriString(int type) { + switch (type) { + case LOAD_IMAGE_TYPE_CHANNEL_LOGO: + return TvContract.buildChannelLogoUri(mId).toString(); + case LOAD_IMAGE_TYPE_APP_LINK_ICON: + return mAppLinkIconUri; + case LOAD_IMAGE_TYPE_APP_LINK_POSTER_ART: + return mAppLinkPosterArtUri; + } + return null; + } + + /** + * Default Channel ordering. + * + * <p>Ordering + * <li>{@link TvInputManagerHelper#isPartnerInput(String)} + * <li>{@link #getInputLabelForChannel(Channel)} + * <li>{@link #getInputId()} + * <li>{@link ChannelNumber#compare(String, String)} + * <li> + * </ol> + */ + public static class DefaultComparator implements Comparator<Channel> { + private final Context mContext; + private final TvInputManagerHelper mInputManager; + private final Map<String, String> mInputIdToLabelMap = new HashMap<>(); + private boolean mDetectDuplicatesEnabled; + + public DefaultComparator(Context context, TvInputManagerHelper inputManager) { + mContext = context; + mInputManager = inputManager; + } + + public void setDetectDuplicatesEnabled(boolean detectDuplicatesEnabled) { + mDetectDuplicatesEnabled = detectDuplicatesEnabled; + } + + @SuppressWarnings("ReferenceEquality") + @Override + public int compare(Channel lhs, Channel rhs) { + if (lhs == rhs) { + return 0; + } + // Put channels from OEM/SOC inputs first. + boolean lhsIsPartner = mInputManager.isPartnerInput(lhs.getInputId()); + boolean rhsIsPartner = mInputManager.isPartnerInput(rhs.getInputId()); + if (lhsIsPartner != rhsIsPartner) { + return lhsIsPartner ? -1 : 1; + } + // Compare the input labels. + String lhsLabel = getInputLabelForChannel(lhs); + String rhsLabel = getInputLabelForChannel(rhs); + int result = + lhsLabel == null + ? (rhsLabel == null ? 0 : 1) + : rhsLabel == null ? -1 : lhsLabel.compareTo(rhsLabel); + if (result != 0) { + return result; + } + // Compare the input IDs. The input IDs cannot be null. + result = lhs.getInputId().compareTo(rhs.getInputId()); + if (result != 0) { + return result; + } + // Compare the channel numbers if both channels belong to the same input. + result = ChannelNumber.compare(lhs.getDisplayNumber(), rhs.getDisplayNumber()); + if (mDetectDuplicatesEnabled && result == 0) { + Log.w( + TAG, + "Duplicate channels detected! - \"" + + lhs.getDisplayText() + + "\" and \"" + + rhs.getDisplayText() + + "\""); + } + return result; + } + + @VisibleForTesting + String getInputLabelForChannel(Channel channel) { + String label = mInputIdToLabelMap.get(channel.getInputId()); + if (label == null) { + TvInputInfo info = mInputManager.getTvInputInfo(channel.getInputId()); + if (info != null) { + label = Utils.loadLabel(mContext, info); + if (label != null) { + mInputIdToLabelMap.put(channel.getInputId(), label); + } + } + } + return label; + } + } +} |