1/*
2 * Copyright (C) 2015 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.tv.menu;
18
19import android.content.Context;
20import android.content.Intent;
21import android.content.pm.ApplicationInfo;
22import android.content.pm.PackageManager;
23import android.graphics.Bitmap;
24import android.graphics.Canvas;
25import android.graphics.drawable.BitmapDrawable;
26import android.graphics.drawable.Drawable;
27import android.os.AsyncTask;
28import android.support.annotation.Nullable;
29import android.support.v7.graphics.Palette;
30import android.text.TextUtils;
31import android.util.AttributeSet;
32import android.util.Log;
33import android.view.View;
34import android.widget.ImageView;
35import android.widget.TextView;
36import com.android.tv.MainActivity;
37import com.android.tv.R;
38import com.android.tv.data.api.Channel;
39import com.android.tv.util.TvInputManagerHelper;
40import com.android.tv.util.images.BitmapUtils;
41import com.android.tv.util.images.ImageLoader;
42import java.util.Objects;
43
44/** A view to render an app link card. */
45public class AppLinkCardView extends BaseCardView<ChannelsRowItem> {
46    private static final String TAG = MenuView.TAG;
47    private static final boolean DEBUG = MenuView.DEBUG;
48
49    private final int mCardImageWidth;
50    private final int mCardImageHeight;
51    private final int mIconWidth;
52    private final int mIconHeight;
53    private final int mIconPadding;
54    private final int mIconColorFilter;
55    private final Drawable mDefaultDrawable;
56
57    private ImageView mImageView;
58    private TextView mAppInfoView;
59    private View mMetaViewHolder;
60    private Channel mChannel;
61    private Intent mIntent;
62    private final PackageManager mPackageManager;
63    private final TvInputManagerHelper mTvInputManagerHelper;
64
65    public AppLinkCardView(Context context) {
66        this(context, null);
67    }
68
69    public AppLinkCardView(Context context, AttributeSet attrs) {
70        this(context, attrs, 0);
71    }
72
73    public AppLinkCardView(Context context, AttributeSet attrs, int defStyle) {
74        super(context, attrs, defStyle);
75
76        mCardImageWidth = getResources().getDimensionPixelSize(R.dimen.card_image_layout_width);
77        mCardImageHeight = getResources().getDimensionPixelSize(R.dimen.card_image_layout_height);
78        mIconWidth = getResources().getDimensionPixelSize(R.dimen.app_link_card_icon_width);
79        mIconHeight = getResources().getDimensionPixelSize(R.dimen.app_link_card_icon_height);
80        mIconPadding = getResources().getDimensionPixelOffset(R.dimen.app_link_card_icon_padding);
81        mPackageManager = context.getPackageManager();
82        mTvInputManagerHelper = ((MainActivity) context).getTvInputManagerHelper();
83        mIconColorFilter = getResources().getColor(R.color.app_link_card_icon_color_filter, null);
84        mDefaultDrawable = getResources().getDrawable(R.drawable.ic_recent_thumbnail_default, null);
85    }
86
87    /** Returns the intent which will be started once this card is clicked. */
88    public Intent getIntent() {
89        return mIntent;
90    }
91
92    @Override
93    public void onBind(ChannelsRowItem item, boolean selected) {
94        Channel newChannel = item.getChannel();
95        boolean channelChanged = !Objects.equals(mChannel, newChannel);
96        String previousPosterArtUri = mChannel == null ? null : mChannel.getAppLinkPosterArtUri();
97        boolean posterArtChanged =
98                previousPosterArtUri == null
99                        || newChannel.getAppLinkPosterArtUri() == null
100                        || !TextUtils.equals(
101                                previousPosterArtUri, newChannel.getAppLinkPosterArtUri());
102        mChannel = newChannel;
103        if (DEBUG) {
104            Log.d(
105                    TAG,
106                    "onBind(channelName="
107                            + mChannel.getDisplayName()
108                            + ", selected="
109                            + selected
110                            + ")");
111        }
112        ApplicationInfo appInfo = mTvInputManagerHelper.getTvInputAppInfo(mChannel.getInputId());
113        if (channelChanged) {
114            int linkType = mChannel.getAppLinkType(getContext());
115            mIntent = mChannel.getAppLinkIntent(getContext());
116
117            CharSequence appLabel;
118            switch (linkType) {
119                case Channel.APP_LINK_TYPE_CHANNEL:
120                    setText(mChannel.getAppLinkText());
121                    mAppInfoView.setVisibility(VISIBLE);
122                    mAppInfoView.setCompoundDrawablePadding(mIconPadding);
123                    mAppInfoView.setCompoundDrawablesRelative(null, null, null, null);
124                    appLabel =
125                            mTvInputManagerHelper.getTvInputApplicationLabel(mChannel.getInputId());
126                    if (appLabel != null) {
127                        mAppInfoView.setText(appLabel);
128                    } else {
129                        new AsyncTask<Void, Void, CharSequence>() {
130                            private final String mLoadTvInputId = mChannel.getInputId();
131
132                            @Override
133                            protected CharSequence doInBackground(Void... params) {
134                                if (appInfo != null) {
135                                    return mPackageManager.getApplicationLabel(appInfo);
136                                }
137                                return null;
138                            }
139
140                            @Override
141                            protected void onPostExecute(CharSequence appLabel) {
142                                mTvInputManagerHelper.setTvInputApplicationLabel(
143                                        mLoadTvInputId, appLabel);
144                                if (mLoadTvInputId.equals(mChannel.getInputId())
145                                        || !isAttachedToWindow()) {
146                                    return;
147                                }
148                                mAppInfoView.setText(appLabel);
149                            }
150                        }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
151                    }
152                    if (!TextUtils.isEmpty(mChannel.getAppLinkIconUri())) {
153                        mChannel.loadBitmap(
154                                getContext(),
155                                Channel.LOAD_IMAGE_TYPE_APP_LINK_ICON,
156                                mIconWidth,
157                                mIconHeight,
158                                createChannelLogoCallback(
159                                        this, mChannel, Channel.LOAD_IMAGE_TYPE_APP_LINK_ICON));
160                    } else if (appInfo.icon != 0) {
161                        Drawable appIcon =
162                                mTvInputManagerHelper.getTvInputApplicationIcon(
163                                        mChannel.getInputId());
164                        if (appIcon != null) {
165                            BitmapUtils.setColorFilterToDrawable(mIconColorFilter, appIcon);
166                            appIcon.setBounds(0, 0, mIconWidth, mIconHeight);
167                            mAppInfoView.setCompoundDrawablesRelative(appIcon, null, null, null);
168                        } else {
169                            new AsyncTask<Void, Void, Drawable>() {
170                                private final String mLoadTvInputId = mChannel.getInputId();
171
172                                @Override
173                                protected Drawable doInBackground(Void... params) {
174                                    return mPackageManager.getApplicationIcon(appInfo);
175                                }
176
177                                @Override
178                                protected void onPostExecute(Drawable appIcon) {
179                                    mTvInputManagerHelper.setTvInputApplicationIcon(
180                                            mLoadTvInputId, appIcon);
181                                    if (!mLoadTvInputId.equals(mChannel.getInputId())
182                                            || !isAttachedToWindow()) {
183                                        return;
184                                    }
185                                    BitmapUtils.setColorFilterToDrawable(mIconColorFilter, appIcon);
186                                    appIcon.setBounds(0, 0, mIconWidth, mIconHeight);
187                                    mAppInfoView.setCompoundDrawablesRelative(
188                                            appIcon, null, null, null);
189                                }
190                            }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
191                        }
192                    }
193                    break;
194                case Channel.APP_LINK_TYPE_APP:
195                    appLabel =
196                            mTvInputManagerHelper.getTvInputApplicationLabel(mChannel.getInputId());
197                    if (appLabel != null) {
198                        setText(
199                                getContext()
200                                        .getString(
201                                                R.string.channels_item_app_link_app_launcher,
202                                                appLabel));
203                    } else {
204                        new AsyncTask<Void, Void, CharSequence>() {
205                            private final String mLoadTvInputId = mChannel.getInputId();
206
207                            @Override
208                            protected CharSequence doInBackground(Void... params) {
209                                if (appInfo != null) {
210                                    return mPackageManager.getApplicationLabel(appInfo);
211                                }
212                                return null;
213                            }
214
215                            @Override
216                            protected void onPostExecute(CharSequence appLabel) {
217                                mTvInputManagerHelper.setTvInputApplicationLabel(
218                                        mLoadTvInputId, appLabel);
219                                if (!mLoadTvInputId.equals(mChannel.getInputId())
220                                        || !isAttachedToWindow()) {
221                                    return;
222                                }
223                                setText(
224                                        getContext()
225                                                .getString(
226                                                        R.string
227                                                                .channels_item_app_link_app_launcher,
228                                                        appLabel));
229                            }
230                        }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
231                    }
232                    mAppInfoView.setVisibility(GONE);
233                    break;
234                default:
235                    mAppInfoView.setVisibility(GONE);
236                    Log.d(TAG, "Should not be here.");
237            }
238
239            if (mChannel.getAppLinkColor() == 0) {
240                mMetaViewHolder.setBackgroundResource(R.color.channel_card_meta_background);
241            } else {
242                mMetaViewHolder.setBackgroundColor(mChannel.getAppLinkColor());
243            }
244        }
245        if (posterArtChanged) {
246            mImageView.setImageDrawable(mDefaultDrawable);
247            mImageView.setForeground(null);
248            if (!TextUtils.isEmpty(mChannel.getAppLinkPosterArtUri())) {
249                mChannel.loadBitmap(
250                        getContext(),
251                        Channel.LOAD_IMAGE_TYPE_APP_LINK_POSTER_ART,
252                        mCardImageWidth,
253                        mCardImageHeight,
254                        createChannelLogoCallback(
255                                this, mChannel, Channel.LOAD_IMAGE_TYPE_APP_LINK_POSTER_ART));
256            } else {
257                setCardImageWithBanner(appInfo);
258            }
259        }
260        super.onBind(item, selected);
261    }
262
263    private static ImageLoader.ImageLoaderCallback<AppLinkCardView> createChannelLogoCallback(
264            AppLinkCardView cardView, final Channel channel, final int type) {
265        return new ImageLoader.ImageLoaderCallback<AppLinkCardView>(cardView) {
266            @Override
267            public void onBitmapLoaded(AppLinkCardView cardView, @Nullable Bitmap bitmap) {
268                // mChannel can be changed before the image load finished.
269                if (!cardView.mChannel.hasSameReadOnlyInfo(channel)) {
270                    return;
271                }
272                cardView.updateChannelLogo(bitmap, type);
273            }
274        };
275    }
276
277    private void updateChannelLogo(@Nullable Bitmap bitmap, int type) {
278        if (type == Channel.LOAD_IMAGE_TYPE_APP_LINK_ICON) {
279            BitmapDrawable drawable = null;
280            if (bitmap != null) {
281                drawable = new BitmapDrawable(getResources(), bitmap);
282                if (bitmap.getWidth() > bitmap.getHeight()) {
283                    drawable.setBounds(
284                            0, 0, mIconWidth, mIconWidth * bitmap.getHeight() / bitmap.getWidth());
285                } else {
286                    drawable.setBounds(
287                            0,
288                            0,
289                            mIconHeight * bitmap.getWidth() / bitmap.getHeight(),
290                            mIconHeight);
291                }
292            }
293            BitmapUtils.setColorFilterToDrawable(mIconColorFilter, drawable);
294            mAppInfoView.setCompoundDrawablesRelative(drawable, null, null, null);
295        } else if (type == Channel.LOAD_IMAGE_TYPE_APP_LINK_POSTER_ART) {
296            if (bitmap == null) {
297                setCardImageWithBanner(
298                        mTvInputManagerHelper.getTvInputAppInfo(mChannel.getInputId()));
299            } else {
300                mImageView.setImageBitmap(bitmap);
301                mImageView.setForeground(getContext().getDrawable(R.drawable.card_image_gradient));
302                if (mChannel.getAppLinkColor() == 0) {
303                    extractAndSetMetaViewBackgroundColor(bitmap);
304                }
305            }
306        }
307    }
308
309    @Override
310    protected void onFinishInflate() {
311        super.onFinishInflate();
312        mImageView = (ImageView) findViewById(R.id.image);
313        mAppInfoView = (TextView) findViewById(R.id.app_info);
314        mMetaViewHolder = findViewById(R.id.app_link_text_holder);
315    }
316
317    // Try to set the card image with following order:
318    // 1) Provided poster art image, 2) Activity banner, 3) Activity icon, 4) Application banner,
319    // 5) Application icon, and 6) default image.
320    private void setCardImageWithBanner(ApplicationInfo appInfo) {
321        new AsyncTask<Void, Void, Drawable>() {
322            private String mLoadTvInputId = mChannel.getInputId();
323
324            @Override
325            protected Drawable doInBackground(Void... params) {
326                Drawable banner = null;
327                if (mIntent != null) {
328                    try {
329                        banner = mPackageManager.getActivityBanner(mIntent);
330                        if (banner == null) {
331                            banner = mPackageManager.getActivityIcon(mIntent);
332                        }
333                    } catch (PackageManager.NameNotFoundException e) {
334                        // do nothing.
335                    }
336                }
337                return banner;
338            }
339
340            @Override
341            protected void onPostExecute(Drawable banner) {
342                if (mLoadTvInputId.equals(mChannel.getInputId()) || !isAttachedToWindow()) {
343                    return;
344                }
345                if (banner != null) {
346                    setCardImageWithBannerInternal(banner);
347                } else {
348                    setCardImageWithApplicationInfoBanner(appInfo);
349                }
350            }
351        }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
352    }
353
354    private void setCardImageWithApplicationInfoBanner(ApplicationInfo appInfo) {
355        Drawable appBanner =
356                mTvInputManagerHelper.getTvInputApplicationBanner(mChannel.getInputId());
357        if (appBanner != null) {
358            setCardImageWithBannerInternal(appBanner);
359        } else {
360            new AsyncTask<Void, Void, Drawable>() {
361                private final String mLoadTvInputId = mChannel.getInputId();
362
363                @Override
364                protected Drawable doInBackground(Void... params) {
365                    Drawable banner = null;
366                    if (appInfo != null) {
367                        if (appInfo.banner != 0) {
368                            banner = mPackageManager.getApplicationBanner(appInfo);
369                        }
370                        if (banner == null && appInfo.icon != 0) {
371                            banner = mPackageManager.getApplicationIcon(appInfo);
372                        }
373                    }
374                    return banner;
375                }
376
377                @Override
378                protected void onPostExecute(Drawable banner) {
379                    mTvInputManagerHelper.setTvInputApplicationBanner(mLoadTvInputId, banner);
380                    if (!TextUtils.equals(mLoadTvInputId, mChannel.getInputId())
381                            || !isAttachedToWindow()) {
382                        return;
383                    }
384                    setCardImageWithBannerInternal(banner);
385                }
386            }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
387        }
388    }
389
390    private void setCardImageWithBannerInternal(Drawable banner) {
391        if (banner == null) {
392            mImageView.setImageDrawable(mDefaultDrawable);
393            mImageView.setBackgroundResource(R.color.channel_card);
394        } else {
395            Bitmap bitmap =
396                    Bitmap.createBitmap(mCardImageWidth, mCardImageHeight, Bitmap.Config.ARGB_8888);
397            Canvas canvas = new Canvas(bitmap);
398            banner.setBounds(0, 0, mCardImageWidth, mCardImageHeight);
399            banner.draw(canvas);
400            mImageView.setImageDrawable(banner);
401            mImageView.setForeground(getContext().getDrawable(R.drawable.card_image_gradient));
402            if (mChannel.getAppLinkColor() == 0) {
403                extractAndSetMetaViewBackgroundColor(bitmap);
404            }
405        }
406    }
407
408    private void extractAndSetMetaViewBackgroundColor(Bitmap bitmap) {
409        new Palette.Builder(bitmap)
410                .generate(
411                        new Palette.PaletteAsyncListener() {
412                            @Override
413                            public void onGenerated(Palette palette) {
414                                mMetaViewHolder.setBackgroundColor(
415                                        palette.getDarkVibrantColor(
416                                                getResources()
417                                                        .getColor(
418                                                                R.color
419                                                                        .channel_card_meta_background,
420                                                                null)));
421                            }
422                        });
423    }
424}
425