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 
17 package com.android.tv.menu;
18 
19 import android.content.Context;
20 import android.content.Intent;
21 import android.content.pm.ApplicationInfo;
22 import android.content.pm.PackageManager;
23 import android.graphics.Bitmap;
24 import android.graphics.Canvas;
25 import android.graphics.drawable.BitmapDrawable;
26 import android.graphics.drawable.Drawable;
27 import android.os.AsyncTask;
28 import android.support.annotation.Nullable;
29 import android.text.TextUtils;
30 import android.util.AttributeSet;
31 import android.util.Log;
32 import android.view.View;
33 import android.widget.ImageView;
34 import android.widget.TextView;
35 import androidx.palette.graphics.Palette;
36 import com.android.tv.MainActivity;
37 import com.android.tv.R;
38 import com.android.tv.data.api.Channel;
39 import com.android.tv.util.TvInputManagerHelper;
40 import com.android.tv.util.images.BitmapUtils;
41 import com.android.tv.util.images.ImageLoader;
42 import java.util.Objects;
43 
44 /** A view to render an app link card. */
45 public 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 
AppLinkCardView(Context context)65     public AppLinkCardView(Context context) {
66         this(context, null);
67     }
68 
AppLinkCardView(Context context, AttributeSet attrs)69     public AppLinkCardView(Context context, AttributeSet attrs) {
70         this(context, attrs, 0);
71     }
72 
AppLinkCardView(Context context, AttributeSet attrs, int defStyle)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. */
getIntent()88     public Intent getIntent() {
89         return mIntent;
90     }
91 
92     @Override
onBind(ChannelsRowItem item, boolean selected)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 
createChannelLogoCallback( AppLinkCardView cardView, final Channel channel, final int type)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