1 /* 2 * Copyright (C) 2020 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.car.carlauncher.homescreen; 18 19 import android.graphics.drawable.Drawable; 20 import android.os.Bundle; 21 import android.util.Size; 22 import android.view.LayoutInflater; 23 import android.view.View; 24 import android.view.ViewGroup; 25 import android.view.ViewStub; 26 import android.widget.ImageButton; 27 import android.widget.ImageView; 28 import android.widget.TextView; 29 30 import androidx.fragment.app.Fragment; 31 32 import com.android.car.apps.common.CrossfadeImageView; 33 import com.android.car.carlauncher.R; 34 import com.android.car.carlauncher.homescreen.ui.CardContent; 35 import com.android.car.carlauncher.homescreen.ui.CardHeader; 36 import com.android.car.carlauncher.homescreen.ui.DescriptiveTextView; 37 import com.android.car.carlauncher.homescreen.ui.DescriptiveTextWithControlsView; 38 import com.android.car.carlauncher.homescreen.ui.TextBlockView; 39 import com.android.internal.annotations.VisibleForTesting; 40 import com.android.internal.util.ArrayUtils; 41 42 /** 43 * Abstract class for a {@link Fragment} that implements the Home App's View interface. 44 * 45 * {@link HomeCardInterface.View} classes that extend HomeCardFragment will override the update 46 * methods of the types of CardContent that they support. Each CardContent class corresponds to a 47 * layout shown on the card. The layout is a combination of xml files. 48 * {@link DescriptiveTextWithControlsView}: card_content_descriptive_text, card_content_button_trio 49 * {@link DescriptiveTextView}: card_content_descriptive_text, card_content_tap_for_more_text 50 * {@link TextBlockView}: card_content_text_block, card_content_tap_for_more_text 51 */ 52 public class HomeCardFragment extends Fragment implements HomeCardInterface.View { 53 54 private Size mSize; 55 private View mCardBackground; 56 private CrossfadeImageView mCardBackgroundImage; 57 private View mRootView; 58 private TextView mCardTitle; 59 private ImageView mCardIcon; 60 61 // Views from card_content_text_block.xml 62 private View mTextBlockLayoutView; 63 private TextView mTextBlock; 64 private TextView mTextBlockTapForMore; 65 66 // Views from card_content_descriptive_text_only.xml 67 private View mDescriptiveTextOnlyLayoutView; 68 private ImageView mDescriptiveTextOnlyOptionalImage; 69 private TextView mDescriptiveTextOnlyTitle; 70 private TextView mDescriptiveTextOnlySubtitle; 71 private TextView mDescriptiveTextOnlyTapForMore; 72 73 // Views from card_content_descriptive_text_with_controls.xml 74 private View mDescriptiveTextWithControlsLayoutView; 75 private ImageView mDescriptiveTextWithControlsOptionalImage; 76 private TextView mDescriptiveTextWithControlsTitle; 77 private TextView mDescriptiveTextWithControlsSubtitle; 78 private View mControlBarView; 79 private ImageButton mControlBarLeftButton; 80 private ImageButton mControlBarCenterButton; 81 private ImageButton mControlBarRightButton; 82 83 84 private boolean mViewCreated; 85 private OnViewLifecycleChangeListener mOnViewLifecycleChangeListener; 86 87 private OnViewClickListener mOnViewClickListener; 88 89 /** 90 * Interface definition for a callback to be invoked for a view lifecycle changes. 91 */ 92 public interface OnViewLifecycleChangeListener { 93 94 /** 95 * Called when a view has been Created. 96 */ onViewCreated()97 void onViewCreated(); 98 99 /** 100 * Called when a view has been destroyed. 101 */ onViewDestroyed()102 void onViewDestroyed(); 103 } 104 105 /** 106 * Interface definition for a callback to be invoked when a view is clicked. 107 */ 108 public interface OnViewClickListener { 109 110 /** 111 * Called when a view has been clicked. 112 */ onViewClicked()113 void onViewClicked(); 114 } 115 116 @Override onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)117 public View onCreateView(LayoutInflater inflater, ViewGroup container, 118 Bundle savedInstanceState) { 119 mRootView = inflater.inflate(R.layout.card_fragment, container, false); 120 mCardTitle = mRootView.findViewById(R.id.card_name); 121 mCardIcon = mRootView.findViewById(R.id.card_icon); 122 return mRootView; 123 } 124 125 @Override onViewCreated(View view, Bundle savedInstanceState)126 public void onViewCreated(View view, Bundle savedInstanceState) { 127 super.onViewCreated(view, savedInstanceState); 128 mViewCreated = true; 129 if (mOnViewLifecycleChangeListener != null) { 130 mOnViewLifecycleChangeListener.onViewCreated(); 131 } 132 mRootView.setOnClickListener(v -> mOnViewClickListener.onViewClicked()); 133 } 134 135 @Override onDestroy()136 public void onDestroy() { 137 super.onDestroy(); 138 mViewCreated = false; 139 if (mOnViewLifecycleChangeListener != null) { 140 mOnViewLifecycleChangeListener.onViewDestroyed(); 141 } 142 mSize = null; 143 } 144 145 @Override getFragment()146 public Fragment getFragment() { 147 return this; 148 } 149 150 /** 151 * Register a callback to be invoked when this view lifecycle changes. 152 * 153 * @param onViewLifecycleChangeListener The callback that will run 154 */ setOnViewLifecycleChangeListener( OnViewLifecycleChangeListener onViewLifecycleChangeListener)155 public void setOnViewLifecycleChangeListener( 156 OnViewLifecycleChangeListener onViewLifecycleChangeListener) { 157 mOnViewLifecycleChangeListener = onViewLifecycleChangeListener; 158 if (mViewCreated) { 159 mOnViewLifecycleChangeListener.onViewCreated(); 160 } 161 } 162 163 /** 164 * Register a callback to be invoked when this view is clicked. 165 * 166 * @param onViewClickListener The callback that will run 167 */ setOnViewClickListener(OnViewClickListener onViewClickListener)168 public void setOnViewClickListener(OnViewClickListener onViewClickListener) { 169 mOnViewClickListener = onViewClickListener; 170 } 171 172 /** 173 * Returns the size of the card or null if the view hasn't yet been laid out 174 */ getCardSize()175 protected Size getCardSize() { 176 if (mSize == null && mRootView.isLaidOut()) { 177 mSize = new Size(mRootView.getWidth(), mRootView.getHeight()); 178 } 179 return mSize; 180 } 181 182 /** 183 * Removes the audio card from view 184 */ 185 @Override hideCard()186 public void hideCard() { 187 hideAllViews(); 188 mRootView.setVisibility(View.GONE); 189 } 190 191 /** 192 * Updates the card's header: name and icon of source app 193 */ updateHeaderView(CardHeader header)194 public void updateHeaderView(CardHeader header) { 195 requireActivity().runOnUiThread(() -> { 196 mRootView.setVisibility(View.VISIBLE); 197 mCardTitle.setText(header.getCardTitle()); 198 mCardIcon.setImageDrawable(header.getCardIcon()); 199 }); 200 } 201 202 /** 203 * Updates the card's content 204 */ updateContentView(CardContent content)205 public final void updateContentView(CardContent content) { 206 requireActivity().runOnUiThread(() -> { 207 hideAllViews(); 208 updateContentViewInternal(content); 209 }); 210 } 211 212 213 /** 214 * Child classes can override this method for updating their specific types of card content 215 */ updateContentViewInternal(CardContent content)216 protected void updateContentViewInternal(CardContent content) { 217 switch (content.getType()) { 218 case DESCRIPTIVE_TEXT: 219 DescriptiveTextView descriptiveTextContent = (DescriptiveTextView) content; 220 updateDescriptiveTextOnlyView(descriptiveTextContent.getTitle(), 221 descriptiveTextContent.getSubtitle(), descriptiveTextContent.getImage(), 222 descriptiveTextContent.getFooter()); 223 break; 224 case DESCRIPTIVE_TEXT_WITH_CONTROLS: 225 DescriptiveTextWithControlsView descriptiveTextWithControlsContent = 226 (DescriptiveTextWithControlsView) content; 227 updateDescriptiveTextWithControlsView(descriptiveTextWithControlsContent.getTitle(), 228 descriptiveTextWithControlsContent.getSubtitle(), 229 descriptiveTextWithControlsContent.getImage(), 230 descriptiveTextWithControlsContent.getLeftControl(), 231 descriptiveTextWithControlsContent.getCenterControl(), 232 descriptiveTextWithControlsContent.getRightControl()); 233 break; 234 case TEXT_BLOCK: 235 TextBlockView textBlockContent = (TextBlockView) content; 236 updateTextBlock(textBlockContent.getText(), textBlockContent.getFooter()); 237 break; 238 } 239 } 240 updateDescriptiveTextOnlyView(CharSequence primaryText, CharSequence secondaryText, Drawable optionalImage, CharSequence tapForMoreText)241 protected final void updateDescriptiveTextOnlyView(CharSequence primaryText, 242 CharSequence secondaryText, Drawable optionalImage, CharSequence tapForMoreText) { 243 getDescriptiveTextOnlyLayoutView().setVisibility(View.VISIBLE); 244 mDescriptiveTextOnlyTitle.setText(primaryText); 245 mDescriptiveTextOnlySubtitle.setText(secondaryText); 246 mDescriptiveTextOnlyOptionalImage.setImageDrawable(optionalImage); 247 mDescriptiveTextOnlyOptionalImage.setVisibility( 248 optionalImage == null ? View.GONE : View.VISIBLE); 249 mDescriptiveTextOnlyTapForMore.setText(tapForMoreText); 250 mDescriptiveTextOnlyTapForMore.setVisibility( 251 tapForMoreText == null ? View.GONE : View.VISIBLE); 252 } 253 updateDescriptiveTextWithControlsView(CharSequence primaryText, CharSequence secondaryText, CardContent.CardBackgroundImage optionalImage, DescriptiveTextWithControlsView.Control leftButton, DescriptiveTextWithControlsView.Control centerButton, DescriptiveTextWithControlsView.Control rightButton)254 protected final void updateDescriptiveTextWithControlsView(CharSequence primaryText, 255 CharSequence secondaryText, CardContent.CardBackgroundImage optionalImage, 256 DescriptiveTextWithControlsView.Control leftButton, 257 DescriptiveTextWithControlsView.Control centerButton, 258 DescriptiveTextWithControlsView.Control rightButton) { 259 getDescriptiveTextWithControlsLayoutView().setVisibility(View.VISIBLE); 260 mDescriptiveTextWithControlsTitle.setText(primaryText); 261 mDescriptiveTextWithControlsSubtitle.setText(secondaryText); 262 if (optionalImage != null) { 263 mDescriptiveTextWithControlsOptionalImage.setImageDrawable( 264 optionalImage.getForeground()); 265 } 266 mDescriptiveTextWithControlsOptionalImage.setVisibility( 267 (optionalImage == null || optionalImage.getForeground() == null) ? View.GONE 268 : View.VISIBLE); 269 270 updateControlBarButton(leftButton, mControlBarLeftButton); 271 updateControlBarButton(centerButton, mControlBarCenterButton); 272 updateControlBarButton(rightButton, mControlBarRightButton); 273 } 274 updateControlBarButton(DescriptiveTextWithControlsView.Control buttonContent, ImageButton buttonView)275 private void updateControlBarButton(DescriptiveTextWithControlsView.Control buttonContent, 276 ImageButton buttonView) { 277 if (buttonContent != null) { 278 buttonView.setImageDrawable(buttonContent.getIcon()); 279 if (buttonContent.getIcon() != null) { 280 // update the button view according to icon's selected state 281 buttonView.setSelected(ArrayUtils.contains(buttonContent.getIcon().getState(), 282 android.R.attr.state_selected)); 283 } 284 buttonView.setOnClickListener(buttonContent.getOnClickListener()); 285 buttonView.setVisibility(View.VISIBLE); 286 } else { 287 buttonView.setVisibility(View.GONE); 288 } 289 } 290 updateTextBlock(CharSequence mainText, CharSequence tapForMoreText)291 protected final void updateTextBlock(CharSequence mainText, CharSequence tapForMoreText) { 292 getTextBlockLayoutView().setVisibility(View.VISIBLE); 293 mTextBlock.setText(mainText); 294 mTextBlockTapForMore.setText(tapForMoreText); 295 mTextBlockTapForMore.setVisibility(tapForMoreText == null ? View.GONE : View.VISIBLE); 296 } 297 hideAllViews()298 protected void hideAllViews() { 299 getTextBlockLayoutView().setVisibility(View.GONE); 300 getDescriptiveTextOnlyLayoutView().setVisibility(View.GONE); 301 getDescriptiveTextWithControlsLayoutView().setVisibility(View.GONE); 302 } 303 getRootView()304 protected final View getRootView() { 305 return mRootView; 306 } 307 getCardBackground()308 protected final View getCardBackground() { 309 if (mCardBackground == null) { 310 mCardBackground = getRootView().findViewById(R.id.card_background); 311 } 312 return mCardBackground; 313 } 314 getCardBackgroundImage()315 protected final CrossfadeImageView getCardBackgroundImage() { 316 if (mCardBackgroundImage == null) { 317 mCardBackgroundImage = getRootView().findViewById(R.id.card_background_image); 318 } 319 return mCardBackgroundImage; 320 } 321 getDescriptiveTextOnlyLayoutView()322 protected final View getDescriptiveTextOnlyLayoutView() { 323 if (mDescriptiveTextOnlyLayoutView == null) { 324 ViewStub stub = mRootView.findViewById(R.id.descriptive_text_layout); 325 mDescriptiveTextOnlyLayoutView = stub.inflate(); 326 mDescriptiveTextOnlyTitle = mDescriptiveTextOnlyLayoutView.findViewById( 327 R.id.primary_text); 328 mDescriptiveTextOnlySubtitle = mDescriptiveTextOnlyLayoutView.findViewById( 329 R.id.secondary_text); 330 mDescriptiveTextOnlyOptionalImage = mDescriptiveTextOnlyLayoutView.findViewById( 331 R.id.optional_image); 332 mDescriptiveTextOnlyTapForMore = mDescriptiveTextOnlyLayoutView.findViewById( 333 R.id.tap_for_more_text); 334 } 335 return mDescriptiveTextOnlyLayoutView; 336 } 337 getDescriptiveTextWithControlsLayoutView()338 protected final View getDescriptiveTextWithControlsLayoutView() { 339 if (mDescriptiveTextWithControlsLayoutView == null) { 340 ViewStub stub = mRootView.findViewById(R.id.descriptive_text_with_controls_layout); 341 mDescriptiveTextWithControlsLayoutView = stub.inflate(); 342 mDescriptiveTextWithControlsTitle = mDescriptiveTextWithControlsLayoutView.findViewById( 343 R.id.primary_text); 344 mDescriptiveTextWithControlsSubtitle = 345 mDescriptiveTextWithControlsLayoutView.findViewById(R.id.secondary_text); 346 mDescriptiveTextWithControlsOptionalImage = 347 mDescriptiveTextWithControlsLayoutView.findViewById(R.id.optional_image); 348 mControlBarView = mDescriptiveTextWithControlsLayoutView.findViewById(R.id.button_trio); 349 mControlBarLeftButton = mDescriptiveTextWithControlsLayoutView.findViewById( 350 R.id.button_left); 351 mControlBarCenterButton = mDescriptiveTextWithControlsLayoutView.findViewById( 352 R.id.button_center); 353 mControlBarRightButton = mDescriptiveTextWithControlsLayoutView.findViewById( 354 R.id.button_right); 355 } 356 return mDescriptiveTextWithControlsLayoutView; 357 } 358 getTextBlockLayoutView()359 private View getTextBlockLayoutView() { 360 if (mTextBlockLayoutView == null) { 361 ViewStub stub = mRootView.findViewById(R.id.text_block_layout); 362 mTextBlockLayoutView = stub.inflate(); 363 mTextBlock = mTextBlockLayoutView.findViewById(R.id.text_block); 364 mTextBlockTapForMore = mTextBlockLayoutView.findViewById(R.id.tap_for_more_text); 365 } 366 return mTextBlockLayoutView; 367 } 368 369 @VisibleForTesting setControlBarLeftButton(ImageButton controlBarLeftButton)370 void setControlBarLeftButton(ImageButton controlBarLeftButton) { 371 mControlBarLeftButton = controlBarLeftButton; 372 } 373 } 374