1 /*
2  * Copyright (C) 2019 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.dialer.widget;
18 
19 import android.content.Context;
20 import android.content.res.TypedArray;
21 import android.util.AttributeSet;
22 import android.view.LayoutInflater;
23 import android.view.View;
24 import android.widget.FrameLayout;
25 import android.widget.ImageView;
26 import android.widget.TextView;
27 
28 import androidx.annotation.DrawableRes;
29 import androidx.annotation.IdRes;
30 import androidx.annotation.IntDef;
31 import androidx.annotation.LayoutRes;
32 import androidx.annotation.MainThread;
33 import androidx.annotation.Nullable;
34 import androidx.annotation.StringRes;
35 
36 import com.android.car.apps.common.util.ViewUtils;
37 import com.android.car.dialer.Constants;
38 import com.android.car.dialer.R;
39 import com.android.car.dialer.log.L;
40 
41 /**
42  * A widget that supports different {@link State}s: NEW, LOADING, CONTENT, EMPTY OR ERROR.
43  */
44 public class LoadingFrameLayout extends FrameLayout {
45     private static final String TAG = "CD.LoadingFrameLayout";
46 
47     /**
48      * Possible states of a service request display.
49      */
50     @IntDef({State.NEW, State.LOADING, State.CONTENT, State.ERROR, State.EMPTY})
51     public @interface State {
52         int NEW = 0;
53         int LOADING = 1;
54         int CONTENT = 2;
55         int ERROR = 3;
56         int EMPTY = 4;
57     }
58 
59     private final Context mContext;
60 
61     private ViewContainer mEmptyView;
62     private ViewContainer mLoadingView;
63     private ViewContainer mErrorView;
64 
65     @State
66     private int mState = State.NEW;
67 
LoadingFrameLayout(Context context, AttributeSet attrs)68     public LoadingFrameLayout(Context context, AttributeSet attrs) {
69         this(context, attrs, 0);
70     }
71 
LoadingFrameLayout(Context context, AttributeSet attrs, int defStyle)72     public LoadingFrameLayout(Context context, AttributeSet attrs, int defStyle) {
73         super(context, attrs, defStyle);
74         mContext = context;
75         TypedArray values =
76                 context.obtainStyledAttributes(attrs, R.styleable.LoadingFrameLayout, defStyle, 0);
77         setLoadingView(
78                 values.getResourceId(
79                         R.styleable.LoadingFrameLayout_progressViewLayout,
80                         R.layout.loading_progress_view));
81         setEmptyView(
82                 values.getResourceId(
83                         R.styleable.LoadingFrameLayout_emptyViewLayout,
84                         R.layout.loading_info_view));
85         setErrorView(
86                 values.getResourceId(
87                         R.styleable.LoadingFrameLayout_errorViewLayout,
88                         R.layout.loading_info_view));
89         values.recycle();
90     }
91 
92     @Override
onFinishInflate()93     public void onFinishInflate() {
94         super.onFinishInflate();
95         // Start with a loading view when inflated from XML.
96         showLoading();
97     }
98 
setLoadingView(int loadingLayoutId)99     private void setLoadingView(int loadingLayoutId) {
100         mLoadingView = new ViewContainer(State.LOADING, loadingLayoutId, 0, 0, 0, 0);
101     }
102 
setEmptyView(int emptyLayoutId)103     private void setEmptyView(int emptyLayoutId) {
104         mEmptyView = new ViewContainer(State.EMPTY, emptyLayoutId, R.id.loading_info_icon,
105                 R.id.loading_info_message, R.id.loading_info_secondary_message,
106                 R.id.loading_info_action_button);
107     }
108 
setErrorView(int errorLayoutId)109     private void setErrorView(int errorLayoutId) {
110         mErrorView = new ViewContainer(State.ERROR, errorLayoutId, R.id.loading_info_icon,
111                 R.id.loading_info_message, R.id.loading_info_secondary_message,
112                 R.id.loading_info_action_button);
113     }
114 
115     /**
116      * Shows the loading view, hides other views.
117      */
118     @MainThread
showLoading()119     public void showLoading() {
120         switchTo(State.LOADING);
121     }
122 
123     /**
124      * Shows the error view where the action button is not available and hides other views.
125      *
126      * @param iconResId             drawable resource id used for the top icon. When it is invalid,
127      *                              hide the icon view.
128      * @param messageResId          string resource id used for the error message. When it is
129      *                              invalid, hide the message view.
130      * @param secondaryMessageResId string resource id for the secondary error message. When it is
131      *                              invalid, hide the secondary message view.
132      */
showError(@rawableRes int iconResId, @StringRes int messageResId, @StringRes int secondaryMessageResId)133     public void showError(@DrawableRes int iconResId, @StringRes int messageResId,
134             @StringRes int secondaryMessageResId) {
135         showError(iconResId, messageResId, secondaryMessageResId, Constants.INVALID_RES_ID, null,
136                 false);
137     }
138 
139     /**
140      * Shows the error view, hides other views.
141      *
142      * @param iconResId                   drawable resource id used for the top icon.When it is
143      *                                    invalid, hide the icon view.
144      * @param messageResId                string resource id used for the error message. When it is
145      *                                    invalid, hide the message view.
146      * @param secondaryMessageResId       string resource id for the secondary error message. When
147      *                                    it is invalid, hide the secondary message view.
148      * @param actionButtonTextResId       string resource id for the action button.
149      * @param actionButtonOnClickListener click listener set on the action button.
150      * @param showActionButton            boolean flag if the action button will show.
151      */
showError( @rawableRes int iconResId, @StringRes int messageResId, @StringRes int secondaryMessageResId, @StringRes int actionButtonTextResId, View.OnClickListener actionButtonOnClickListener, boolean showActionButton)152     public void showError(
153             @DrawableRes int iconResId,
154             @StringRes int messageResId,
155             @StringRes int secondaryMessageResId,
156             @StringRes int actionButtonTextResId,
157             View.OnClickListener actionButtonOnClickListener,
158             boolean showActionButton) {
159         mErrorView.setIcon(iconResId);
160         mErrorView.setMessage(messageResId);
161         mErrorView.setSecondaryMessage(secondaryMessageResId);
162         mErrorView.setActionButtonText(actionButtonTextResId);
163         mErrorView.setActionButtonClickListener(actionButtonOnClickListener);
164         mErrorView.setActionButtonVisible(showActionButton);
165         switchTo(State.ERROR);
166     }
167 
168     /**
169      * Shows the empty view where the action button is not available and hides other views.
170      *
171      * @param iconResId             drawable resource id used for the top icon. When it is invalid,
172      *                              hide the icon view.
173      * @param messageResId          string resource id used for the empty message. When it is
174      *                              invalid, hide the message view.
175      * @param secondaryMessageResId string resource id for the secondary empty message. When it is
176      *                              invalid, hide the secondary message view.
177      */
showEmpty(@rawableRes int iconResId, @StringRes int messageResId, @StringRes int secondaryMessageResId)178     public void showEmpty(@DrawableRes int iconResId, @StringRes int messageResId,
179             @StringRes int secondaryMessageResId) {
180         showEmpty(iconResId, messageResId, secondaryMessageResId, Constants.INVALID_RES_ID, null,
181                 false);
182     }
183 
184     /**
185      * Shows the empty view and hides other views.
186      *
187      * @param iconResId                   drawable resource id used for the top icon.When it is
188      *                                    invalid, hide the icon view.
189      * @param messageResId                string resource id used for the empty message. When it is
190      *                                    invalid, hide the message view.
191      * @param secondaryMessageResId       string resource id for the secondary empty message. When
192      *                                    it is invalid, hide the secondary message view.
193      * @param actionButtonTextResId       string resource id for the action button.
194      * @param actionButtonOnClickListener click listener set on the action button.
195      * @param showActionButton            boolean flag if the action button will show.
196      */
showEmpty( @rawableRes int iconResId, @StringRes int messageResId, @StringRes int secondaryMessageResId, @StringRes int actionButtonTextResId, @Nullable View.OnClickListener actionButtonOnClickListener, boolean showActionButton)197     public void showEmpty(
198             @DrawableRes int iconResId,
199             @StringRes int messageResId,
200             @StringRes int secondaryMessageResId,
201             @StringRes int actionButtonTextResId,
202             @Nullable View.OnClickListener actionButtonOnClickListener,
203             boolean showActionButton) {
204         mEmptyView.setIcon(iconResId);
205         mEmptyView.setMessage(messageResId);
206         mEmptyView.setSecondaryMessage(secondaryMessageResId);
207         mEmptyView.setActionButtonText(actionButtonTextResId);
208         mEmptyView.setActionButtonClickListener(actionButtonOnClickListener);
209         mEmptyView.setActionButtonVisible(showActionButton);
210         switchTo(State.EMPTY);
211     }
212 
213     /**
214      * Shows the content view, hides other views.
215      */
showContent()216     public void showContent() {
217         switchTo(State.CONTENT);
218     }
219 
220     /**
221      * Hide all views.
222      */
reset()223     public void reset() {
224         switchTo(State.NEW);
225     }
226 
switchTo(@tate int state)227     private void switchTo(@State int state) {
228         if (mState != state) {
229             L.d(TAG, "Switch to state: %d", state);
230             // Hides, or shows, all the children, including the loading and error views.
231             ViewUtils.setVisible((View) findViewById(R.id.list_view), state == State.CONTENT);
232 
233             // Corrects the visibility setting for error and loading views since they are
234             // shown independently of the views content.
235             mLoadingView.setVisibilityFromState(state);
236             mErrorView.setVisibilityFromState(state);
237             mEmptyView.setVisibilityFromState(state);
238 
239             mState = state;
240         }
241     }
242 
243     /**
244      * Container for views held by this LoadingFrameLayout. Used for deferring view inflation until
245      * the view is about to be shown.
246      */
247     private class ViewContainer {
248 
249         @State
250         private final int mViewState;
251         private final int mLayoutId;
252         private final int mIconViewId;
253         private final int mMessageViewId;
254         private final int mSecondaryMessageViewId;
255         private final int mActionButtonId;
256 
257         private View mView;
258 
259         private ImageView mIconView;
260         // Cache image view resource id until imageView is inflated.
261         @DrawableRes
262         private int mIconResId;
263 
264         private TextView mActionButton;
265         // Cache action button visibility until action button is inflated.
266         private boolean mIsActionButtonVisible;
267         // Cache action button onClickListener until action button is inflated.
268         private View.OnClickListener mActionButtonOnClickListener;
269         // Cache action button text until action button is inflated.
270         private int mActionButtonTextResId;
271 
272         private TextView mMessageView;
273         // Cache message view string until message view is inflated.
274         private int mMessageResId;
275         private TextView mSecondaryMessageView;
276         // Cache the secondary message view string until the secondary message view is inflated.
277         private int mSecondaryMessageResId;
278 
ViewContainer(@tate int state, @LayoutRes int layoutId, @IdRes int iconViewId, @IdRes int messageViewId, @IdRes int secondaryMessageViewId, @IdRes int actionButtonId)279         private ViewContainer(@State int state, @LayoutRes int layoutId, @IdRes int iconViewId,
280                 @IdRes int messageViewId, @IdRes int secondaryMessageViewId,
281                 @IdRes int actionButtonId) {
282             mViewState = state;
283             mLayoutId = layoutId;
284             mIconViewId = iconViewId;
285             mMessageViewId = messageViewId;
286             mSecondaryMessageViewId = secondaryMessageViewId;
287             mActionButtonId = actionButtonId;
288         }
289 
inflateView()290         private View inflateView() {
291             View view = LayoutInflater.from(mContext).inflate(mLayoutId, LoadingFrameLayout.this,
292                     false);
293 
294             if (mMessageViewId > Constants.INVALID_RES_ID) {
295                 mMessageView = view.findViewById(mMessageViewId);
296                 setMessage(mMessageResId);
297             }
298 
299             if (mSecondaryMessageViewId > Constants.INVALID_RES_ID) {
300                 mSecondaryMessageView = view.findViewById(mSecondaryMessageViewId);
301                 setSecondaryMessage(mSecondaryMessageResId);
302             }
303 
304             if (mIconViewId > Constants.INVALID_RES_ID) {
305                 mIconView = view.findViewById(mIconViewId);
306                 setIcon(mIconResId);
307             }
308 
309             if (mActionButtonId > Constants.INVALID_RES_ID) {
310                 mActionButton = view.findViewById(mActionButtonId);
311                 setActionButtonClickListener(mActionButtonOnClickListener);
312                 setActionButtonVisible(mIsActionButtonVisible);
313                 setActionButtonText(mActionButtonTextResId);
314             }
315 
316             return view;
317         }
318 
setVisibilityFromState(@tate int newState)319         public void setVisibilityFromState(@State int newState) {
320             if (mViewState == newState) {
321                 show();
322             } else {
323                 hide();
324             }
325         }
326 
show()327         private void show() {
328             if (mView == null) {
329                 mView = inflateView();
330                 LoadingFrameLayout.this.addView(mView);
331             }
332             mView.setVisibility(View.VISIBLE);
333         }
334 
hide()335         private void hide() {
336             if (mView != null) {
337                 mView.setVisibility(View.GONE);
338                 mView.clearFocus();
339             }
340         }
341 
setMessage(@tringRes int messageResId)342         private void setMessage(@StringRes int messageResId) {
343             if (messageResId > Constants.INVALID_RES_ID) {
344                 ViewUtils.setText(mMessageView, messageResId);
345             } else {
346                 ViewUtils.setVisible(mMessageView, false);
347             }
348             mMessageResId = messageResId;
349         }
350 
setSecondaryMessage(@tringRes int secondaryMessageResId)351         private void setSecondaryMessage(@StringRes int secondaryMessageResId) {
352             if (secondaryMessageResId > Constants.INVALID_RES_ID) {
353                 ViewUtils.setText(mSecondaryMessageView, secondaryMessageResId);
354             } else {
355                 ViewUtils.setVisible(mSecondaryMessageView, false);
356             }
357             mSecondaryMessageResId = secondaryMessageResId;
358         }
359 
setActionButtonClickListener( View.OnClickListener actionButtonOnClickListener)360         private void setActionButtonClickListener(
361                 View.OnClickListener actionButtonOnClickListener) {
362             ViewUtils.setOnClickListener(mActionButton, actionButtonOnClickListener);
363             mActionButtonOnClickListener = actionButtonOnClickListener;
364         }
365 
setActionButtonText(@tringRes int actionButtonTextResId)366         private void setActionButtonText(@StringRes int actionButtonTextResId) {
367             if (actionButtonTextResId > Constants.INVALID_RES_ID) {
368                 ViewUtils.setText(mActionButton, actionButtonTextResId);
369             }
370             mActionButtonTextResId = actionButtonTextResId;
371         }
372 
setActionButtonVisible(boolean visible)373         private void setActionButtonVisible(boolean visible) {
374             ViewUtils.setVisible(mActionButton, visible);
375             mIsActionButtonVisible = visible;
376         }
377 
setIcon(@rawableRes int iconResId)378         private void setIcon(@DrawableRes int iconResId) {
379             if (iconResId > Constants.INVALID_RES_ID) {
380                 if (mIconView != null) {
381                     mIconView.setImageResource(iconResId);
382                 }
383             } else {
384                 ViewUtils.setVisible(mIconView, false);
385             }
386             mIconResId = iconResId;
387         }
388     }
389 }
390