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