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