/*
 * 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.ActivityInfo;
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 =
            (Channel lhs, Channel rhs) ->
                    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_NETWORK_AFFILIATION,
        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++);
        channel.mNetworkAffiliation = 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 String mNetworkAffiliation;
    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;
    }

    @Override
    public String getNetworkAffiliation() {
        return mNetworkAffiliation;
    }

    /** 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;
    }

    @Override
    public void setNetworkAffiliation(String networkAffiliation) {
        mNetworkAffiliation = networkAffiliation;
    }

    /**
     * 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();
            mNetworkAffiliation = channel.getNetworkAffiliation();
            mRecordingProhibited = channel.isRecordingProhibited();
            mChannelLogoExist = channel.channelLogoExists();
            mNetworkAffiliation = channel.getNetworkAffiliation();
        }
    }

    @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;
        mNetworkAffiliation = channel.mNetworkAffiliation;
        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;
        }

        @VisibleForTesting
        public Builder setNetworkAffiliation(String networkAffiliation) {
            mChannel.mNetworkAffiliation = networkAffiliation;
            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);
                ActivityInfo activityInfo = intent.resolveActivityInfo(pm, 0);
                if (activityInfo != null) {
                    String packageName = activityInfo.packageName;
                    // Prevent creation of App Links to private activities in this package
                    boolean isProtectedActivity = packageName != null
                            && (packageName.equals(CommonConstants.BASE_PACKAGE)
                            || packageName.startsWith(CommonConstants.BASE_PACKAGE + "."));
                    if (isProtectedActivity) {
                        Log.w(TAG,"Attempt to add app link to protected activity: "
                                + mAppLinkIntentUri);
                        return;
                    }
                    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;
        }
    }
}
