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