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 package android.car.ui.provider;
17 
18 import android.car.app.menu.CarMenuCallbacks;
19 import android.car.app.menu.RootMenu;
20 import android.car.app.menu.SearchBoxEditListener;
21 import android.content.Context;
22 import android.content.res.Resources;
23 import android.graphics.Bitmap;
24 import android.graphics.PorterDuff;
25 import android.graphics.PorterDuffColorFilter;
26 import android.graphics.drawable.BitmapDrawable;
27 import android.os.Bundle;
28 import android.support.car.input.CarRestrictedEditText;
29 import android.support.car.ui.DrawerArrowDrawable;
30 import android.support.car.ui.PagedListView;
31 import android.support.v7.widget.CardView;
32 import android.text.Editable;
33 import android.text.TextWatcher;
34 import android.util.Log;
35 import android.view.Gravity;
36 import android.view.KeyEvent;
37 import android.widget.EditText;
38 import android.widget.FrameLayout;
39 import android.widget.ImageView;
40 import android.widget.LinearLayout;
41 import android.widget.TextView;
42 import android.view.View;
43 import android.view.LayoutInflater;
44 
45 import android.support.car.ui.R;
46 
47 public class CarUiEntry extends android.car.app.menu.CarUiEntry {
48     private static final String TAG = "Embedded_CarUiEntry";
49 
50     // These values and setSearchBoxMode exist rather than separate methods to make sure exactly the
51     // same set of things get set for each mode, just to different values.
52     /** The search box is not visible. */
53     private static final int SEARCH_BOX_MODE_NONE = 0;
54     /** The small search box is shown in the header beneath the microphone button. */
55     private static final int SEARCH_BOX_MODE_SMALL = 1;
56     /** The whole header between the menu button and the microphone button is taken up by the
57      * search box. */
58     private static final int SEARCH_BOX_MODE_LARGE = 2;
59 
60     private View mContentView;
61     private ImageView mMenuButton;
62     private TextView mTitleView;
63     private CardView mTruncatedListCardView;
64     private CarDrawerLayout mDrawerLayout;
65     private DrawerController mDrawerController;
66     private PagedListView mListView;
67     private DrawerArrowDrawable mDrawerArrowDrawable;
68     private CarRestrictedEditText mCarRestrictedEditText;
69     private SearchBoxClickListener mSearchBoxClickListener;
70 
71     private View mSearchBox;
72     private View mSearchBoxContents;
73     private View mSearchBoxSearchLogoContainer;
74     private ImageView mSearchBoxSearchLogo;
75     private ImageView mSearchBoxSuperSearchLogo;
76     private FrameLayout mSearchBoxEndView;
77     private View mTitleContainer;
78     private SearchBoxEditListener mSearchBoxEditListener;
79 
80     public interface SearchBoxClickListener {
81         /**
82          * The user clicked the search box while it was in small mode.
83          */
onClick()84         void onClick();
85     }
86 
CarUiEntry(Context providerContext, Context appContext)87     public CarUiEntry(Context providerContext, Context appContext) {
88         super(providerContext, appContext);
89     }
90 
91     @Override
getContentView()92     public View getContentView() {
93         LayoutInflater inflater = LayoutInflater.from(mUiLibContext);
94         mContentView = inflater.inflate(R.layout.car_activity, null);
95         mDrawerLayout = (CarDrawerLayout) mContentView.findViewById(R.id.drawer_container);
96         adjustDrawer();
97         mMenuButton = (ImageView) mContentView.findViewById(R.id.car_drawer_button);
98         mTitleView = (TextView) mContentView.findViewById(R.id.car_drawer_title);
99         mTruncatedListCardView = (CardView) mContentView.findViewById(R.id.truncated_list_card);
100         mDrawerArrowDrawable = new DrawerArrowDrawable(mUiLibContext);
101         restoreMenuDrawable();
102         mListView = (PagedListView) mContentView.findViewById(R.id.list_view);
103         mListView.setOnScrollBarListener(mOnScrollBarListener);
104         mMenuButton.setOnClickListener(mMenuListener);
105         mDrawerController = new DrawerController(this, mMenuButton,
106                  mDrawerLayout, mListView, mTruncatedListCardView);
107         mTitleContainer = mContentView.findViewById(R.id.car_drawer_title_container);
108 
109         mSearchBoxEndView = (FrameLayout) mContentView.findViewById(R.id.car_search_box_end_view);
110         mSearchBox = mContentView.findViewById(R.id.car_search_box);
111         mSearchBoxContents = mContentView.findViewById(R.id.car_search_box_contents);
112         mSearchBoxSearchLogoContainer = mContentView.findViewById(
113                 R.id.car_search_box_search_logo_container);
114         mSearchBoxSearchLogoContainer.setOnClickListener(new View.OnClickListener() {
115             @Override
116             public void onClick(View view) {
117                 if (mSearchBoxClickListener != null) {
118                     mSearchBoxClickListener.onClick();
119                 }
120             }
121         });
122         mSearchBoxSearchLogo = (ImageView) mContentView.findViewById(
123                 R.id.car_search_box_search_logo);
124         mSearchBoxSearchLogo.setImageDrawable(mUiLibContext.getResources()
125                 .getDrawable(R.drawable.ic_google));
126         mSearchBoxSuperSearchLogo = (ImageView) mContentView.findViewById(
127                 R.id.car_search_box_super_logo);
128         mSearchBoxSuperSearchLogo.setImageDrawable(mUiLibContext.getResources()
129                 .getDrawable(R.drawable.ic_googleg));
130 
131         mCarRestrictedEditText = (CarRestrictedEditText) mContentView.findViewById(
132                 R.id.car_search_box_edit_text);
133         mCarRestrictedEditText.setOnEditorActionListener(new TextView.OnEditorActionListener() {
134             @Override
135             public boolean onEditorAction(TextView view, int actionId, KeyEvent event) {
136                 if (mSearchBoxEditListener != null) {
137                     mSearchBoxEditListener.onSearch(mCarRestrictedEditText.getText().toString());
138                 }
139                 return false;
140             }
141         });
142         mCarRestrictedEditText.addTextChangedListener(new TextWatcher() {
143             @Override
144             public void beforeTextChanged(CharSequence text, int start, int count, int after) {
145             }
146 
147             @Override
148             public void onTextChanged(CharSequence text, int start, int before, int count) {
149             }
150 
151             @Override
152             public void afterTextChanged(Editable text) {
153                 if (mSearchBoxEditListener != null) {
154                     mSearchBoxEditListener.onEdit(text.toString());
155                 }
156             }
157         });
158         setSearchBoxMode(SEARCH_BOX_MODE_NONE);
159         return mContentView;
160     }
161 
162     private final View.OnClickListener mMenuListener = new View.OnClickListener() {
163         @Override
164         public void onClick(View v) {
165             CarUiEntry.this.mDrawerController.openDrawer();
166         }
167     };
168 
169     @Override
setCarMenuCallbacks(CarMenuCallbacks callbacks)170     public void setCarMenuCallbacks(CarMenuCallbacks callbacks){
171         RootMenu rootMenu = callbacks.getRootMenu(null);
172         if (rootMenu != null) {
173             mDrawerController.setRootAndCallbacks(
174                     rootMenu.getId(), callbacks);
175             mDrawerController.setDrawerEnabled(true);
176         } else {
177             hideMenuButton();
178         }
179     }
180 
181     @Override
getFragmentContainerId()182     public int getFragmentContainerId() {
183         return R.id.container;
184     }
185 
186     @Override
setBackground(Bitmap bitmap)187     public void setBackground(Bitmap bitmap) {
188         BitmapDrawable bd = new BitmapDrawable(mUiLibContext.getResources(), bitmap);
189         ImageView bg = (ImageView) mContentView.findViewById(R.id.background);
190         bg.setBackground(bd);
191     }
192 
193     @Override
hideMenuButton()194     public void hideMenuButton() {
195         mMenuButton.setVisibility(View.GONE);
196     }
197 
198     @Override
restoreMenuDrawable()199     public void restoreMenuDrawable() {
200         mMenuButton.setImageDrawable(mDrawerArrowDrawable);
201     }
202 
setMenuButtonBitmap(Bitmap bitmap)203     public void setMenuButtonBitmap(Bitmap bitmap) {
204         mMenuButton.setImageDrawable(new BitmapDrawable(mUiLibContext.getResources(), bitmap));
205     }
206 
207     @Override
setScrimColor(int color)208     public void setScrimColor(int color) {
209         mDrawerLayout.setScrimColor(color);
210     }
211 
212     @Override
setTitle(CharSequence title)213     public void setTitle(CharSequence title) {
214         mDrawerController.setTitle(title);
215     }
216 
217     @Override
closeDrawer()218     public void closeDrawer() {
219         mDrawerController.closeDrawer();
220     }
221 
222     @Override
openDrawer()223     public void openDrawer() {
224         mDrawerController.openDrawer();
225     }
226 
227     @Override
showMenu(String id, String title)228     public void showMenu(String id, String title) {
229         mDrawerController.showMenu(id, title);
230     }
231 
232 
233     @Override
setMenuButtonColor(int color)234     public void setMenuButtonColor(int color) {
235         setViewColor(mMenuButton, color);
236         setViewColor(mTitleView, color);
237     }
238 
239     @Override
showTitle()240     public void showTitle() {
241         mTitleView.setVisibility(View.VISIBLE);
242     }
243 
244     @Override
hideTitle()245     public void hideTitle() {
246         mTitleView.setVisibility(View.GONE);
247     }
248 
249     @Override
setLightMode()250     public void setLightMode() {
251         mDrawerController.setLightMode();
252     }
253 
254     @Override
setDarkMode()255     public void setDarkMode() {
256         mDrawerController.setDarkMode();
257     }
258 
259     @Override
setAutoLightDarkMode()260     public void setAutoLightDarkMode() {
261         mDrawerController.setAutoLightDarkMode();
262     }
263 
264     @Override
showToast(String msg, long duration)265     public void showToast(String msg, long duration) {
266         // TODO: add toast support
267     }
268 
269     @Override
getSearchBoxText()270     public CharSequence getSearchBoxText() {
271         return mCarRestrictedEditText.getText();
272     }
273 
274     @Override
startInput(String hint, View.OnClickListener searchBoxClickListener)275     public EditText startInput(String hint,
276             View.OnClickListener searchBoxClickListener) {
277         mSearchBoxClickListener = wrapSearchBoxClickListener(searchBoxClickListener);
278         setSearchBoxModeLarge(hint);
279         return mCarRestrictedEditText;
280     }
281 
282 
283     @Override
onRestoreInstanceState(Bundle savedInstanceState)284     public void onRestoreInstanceState(Bundle savedInstanceState) {
285         if (mDrawerController != null) {
286             mDrawerController.restoreState(savedInstanceState);
287         }
288     }
289 
290     @Override
onSaveInstanceState(Bundle outState)291     public void onSaveInstanceState(Bundle outState) {
292         if (mDrawerController != null) {
293             mDrawerController.saveState(outState);
294         }
295     }
296 
297     @Override
onStart()298     public void onStart() {
299 
300     }
301 
302     @Override
onResume()303     public void onResume() {
304 
305     }
306 
307     @Override
onPause()308     public void onPause() {
309 
310     }
311 
312     @Override
onStop()313     public void onStop() {
314 
315     }
316 
317     /**
318      * Sets the colors of all the parts of the search box (regardless of whether it is currently
319      * showing).
320      */
321     @Override
setSearchBoxColors(int backgroundColor, int searchLogoColor, int textColor, int hintTextColor)322     public void setSearchBoxColors(int backgroundColor, int searchLogoColor, int textColor,
323                                    int hintTextColor) {
324         // set background color of mSearchBox to get rid of the animation artifact in b/23767062
325         mSearchBox.setBackgroundColor(backgroundColor);
326         mSearchBoxContents.setBackgroundColor(backgroundColor);
327         mSearchBoxSearchLogo.setColorFilter(searchLogoColor, PorterDuff.Mode.SRC_IN);
328         mCarRestrictedEditText.setTextColor(textColor);
329         mCarRestrictedEditText.setHintTextColor(hintTextColor);
330     }
331 
332     /**
333      * Sets the view to be displayed at the end of the search box, or null to clear any existing
334      * views.
335      */
336     @Override
setSearchBoxEndView(View endView)337     public void setSearchBoxEndView(View endView) {
338         if (endView == null) {
339             mSearchBoxEndView.removeAllViews();
340         } else if (mSearchBoxEndView.getChildCount() == 0) {
341             mSearchBoxEndView.addView(endView);
342         } else if (mSearchBoxEndView.getChildAt(0) != endView) {
343             mSearchBoxEndView.removeViewAt(0);
344             mSearchBoxEndView.addView(endView);
345         }
346     }
347 
348     @Override
showSearchBox(final View.OnClickListener listener)349     public void showSearchBox(final View.OnClickListener listener) {
350         setSearchBoxMode(SEARCH_BOX_MODE_SMALL);
351         mSearchBoxClickListener = wrapSearchBoxClickListener(listener);
352     }
353 
354     @Override
stopInput()355     public void stopInput() {
356         setSearchBoxMode(SEARCH_BOX_MODE_NONE);
357     }
358 
359     @Override
setSearchBoxEditListener(SearchBoxEditListener listener)360     public void setSearchBoxEditListener(SearchBoxEditListener listener) {
361         mSearchBoxEditListener = listener;
362     }
363 
364 
365     /**
366      * Set the progress of the animated {@link DrawerArrowDrawable}.
367      * @param progress 0f displays a menu button
368      *                 1f displays a back button
369      *                 anything in between will be an interpolation of the drawable between
370      *                 back and menu
371      */
setMenuProgress(float progress)372     public void setMenuProgress(float progress) {
373         mDrawerArrowDrawable.setProgress(progress);
374     }
375 
setSearchBoxModeLarge(String hint)376     private void setSearchBoxModeLarge(String hint) {
377         mCarRestrictedEditText.setHint(hint);
378         setSearchBoxMode(SEARCH_BOX_MODE_LARGE);
379     }
380 
setTitleText(CharSequence title)381     public void setTitleText(CharSequence title) {
382         mTitleView.setText(title);
383     }
384 
385     /**
386      * Sets all the view visibilities and layout params for a search box mode.
387      */
setSearchBoxMode(int searchBoxMode)388     private void setSearchBoxMode(int searchBoxMode) {
389         // Set the visibility and width of the search box, and whether the rest of the header sits
390         // beside or beneath the microphone button.
391         LinearLayout.LayoutParams searchBoxLayoutParams =
392                 (LinearLayout.LayoutParams) mSearchBox.getLayoutParams();
393         if (searchBoxMode == SEARCH_BOX_MODE_LARGE) {
394             int screenWidth = mAppContext.getResources().getDisplayMetrics().widthPixels;
395             int searchBoxMargin = mUiLibContext.getResources()
396                     .getDimensionPixelSize(R.dimen.car_drawer_header_menu_button_size);
397             int maxSearchBoxWidth = mUiLibContext.getResources().getDimensionPixelSize(
398                     R.dimen.car_card_max_width);
399             int searchBoxMarginStart = 0;
400             int searchBoxMarginEnd = searchBoxMargin;
401             // If the width of search bar is larger than max card width, we adjust margin to fix it.
402             if (screenWidth - searchBoxMargin * 2 > maxSearchBoxWidth) {
403                 searchBoxMarginEnd = (screenWidth - maxSearchBoxWidth) / 2;
404                 searchBoxMarginStart = searchBoxMarginEnd - searchBoxMargin;
405             }
406             searchBoxLayoutParams.width = 0;
407             searchBoxLayoutParams.weight = 1.0f;
408             searchBoxLayoutParams.setMarginStart(searchBoxMarginStart);
409             searchBoxLayoutParams.setMarginEnd(searchBoxMarginEnd);
410         } else if (searchBoxMode == SEARCH_BOX_MODE_SMALL) {
411             searchBoxLayoutParams.width = mUiLibContext.getResources().getDimensionPixelSize(
412                     R.dimen.car_app_layout_search_box_small_width);
413             searchBoxLayoutParams.weight = 0.0f;
414             searchBoxLayoutParams.setMarginStart(mUiLibContext.getResources()
415                     .getDimensionPixelOffset(R.dimen.car_app_layout_search_box_small_margin));
416             searchBoxLayoutParams.setMarginEnd(mUiLibContext.getResources().getDimensionPixelOffset(
417                     R.dimen.car_app_layout_search_box_small_margin));
418         } else {
419             searchBoxLayoutParams.width = mUiLibContext.getResources().getDimensionPixelSize(
420                     R.dimen.car_app_layout_search_box_small_width);
421             searchBoxLayoutParams.weight = 0.0f;
422             searchBoxLayoutParams.setMarginStart(mUiLibContext.getResources().getDimensionPixelSize(
423                     R.dimen.car_drawer_header_menu_button_size));
424             searchBoxLayoutParams.setMarginEnd(-searchBoxLayoutParams.width);
425         }
426         mSearchBox.setLayoutParams(searchBoxLayoutParams);
427 
428         // Animate the visibility of the contents of the search box - either the Search logo or the
429         // edit text is visible (the super logo also is visible when the edit text is visible).
430         View searchBoxEditTextContainer = (View) mCarRestrictedEditText.getParent();
431         if (searchBoxMode == SEARCH_BOX_MODE_SMALL) {
432             if (mSearchBoxSearchLogoContainer.getVisibility() != View.VISIBLE) {
433                 mSearchBoxSearchLogoContainer.setAlpha(0f);
434                 mSearchBoxSearchLogoContainer.setVisibility(View.VISIBLE);
435             }
436             // 300ms delay to stagger the fade in behind the fade out animation.
437             mSearchBoxSearchLogoContainer.animate().alpha(1f).setStartDelay(300);
438             // Animate the container so it includes the super G logo.
439             if (searchBoxEditTextContainer.getVisibility() == View.VISIBLE) {
440                 searchBoxEditTextContainer.animate().alpha(0f).setStartDelay(0)
441                         .withEndAction(mSetEditTextGoneRunnable);
442             }
443         } else if (searchBoxMode == SEARCH_BOX_MODE_LARGE) {
444             if (searchBoxEditTextContainer.getVisibility() != View.VISIBLE) {
445                 searchBoxEditTextContainer.setAlpha(0f);
446                 searchBoxEditTextContainer.setVisibility(View.VISIBLE);
447             }
448             searchBoxEditTextContainer.animate().alpha(1f).setStartDelay(300);
449             if (mSearchBoxSearchLogoContainer.getVisibility() == View.VISIBLE) {
450                 mSearchBoxSearchLogoContainer.animate().alpha(0f).setStartDelay(0)
451                         .withEndAction(mSetSearchBoxLogoGoneRunnable);
452             }
453         } else {
454             searchBoxEditTextContainer.setVisibility(View.GONE);
455         }
456 
457         // Set the visibility of the title and status containers.
458         if (searchBoxMode == SEARCH_BOX_MODE_LARGE) {
459             mTitleContainer.setVisibility(View.GONE);
460         } else {
461             mTitleContainer.setVisibility(View.VISIBLE);
462         }
463     }
464 
465 
466     private final Runnable mSetEditTextGoneRunnable = new Runnable() {
467         @Override
468         public void run() {
469             ((View) mCarRestrictedEditText.getParent()).setVisibility(View.GONE);
470         }
471     };
472 
473     private final Runnable mSetSearchBoxLogoGoneRunnable = new Runnable() {
474         @Override
475         public void run() {
476             mSearchBoxSearchLogoContainer.setVisibility(View.GONE);
477         }
478     };
479 
480 
wrapSearchBoxClickListener(final View.OnClickListener listener)481     private SearchBoxClickListener wrapSearchBoxClickListener(final View.OnClickListener listener) {
482         return new SearchBoxClickListener() {
483             @Override
484             public void onClick() {
485                 listener.onClick(null);
486             }
487         };
488     }
489 
490 
491     private static void setViewColor(View view, int color) {
492         if (view instanceof TextView) {
493             ((TextView) view).setTextColor(color);
494         } else if (view instanceof ImageView) {
495             ImageView imageView = (ImageView) view;
496             PorterDuffColorFilter filter =
497                     new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN);
498             imageView.setColorFilter(filter);
499         } else {
500             if (Log.isLoggable(TAG, Log.WARN)) {
501                 Log.w(TAG, "Setting color is only supported for TextView and ImageView.");
502             }
503         }
504     }
505 
506     private void adjustDrawer() {
507         Resources resources = mUiLibContext.getResources();
508         float width = resources.getDisplayMetrics().widthPixels;
509         CarDrawerLayout.LayoutParams layoutParams = new CarDrawerLayout.LayoutParams(
510                 CarDrawerLayout.LayoutParams.MATCH_PARENT,
511                 CarDrawerLayout.LayoutParams.MATCH_PARENT);
512         layoutParams.gravity = Gravity.LEFT;
513         // 1. If the screen width is larger than 800dp, the drawer width is kept as 704dp;
514         // 2. Else the drawer width is adjusted to keep the margin end of drawer as 96dp.
515 
516 //        if (width > resources.getDimension(R.dimen.car_standard_width)) {
517 //            layoutParams.setMarginEnd(
518 //                    (int) (width - resources.getDimension(R.dimen.car_drawer_standard_width)));
519 //        } else {
520 //            layoutParams.setMarginEnd(
521 //                    (int) resources.getDimension(R.dimen.car_card_margin));
522 //        }
523         // TODO: For UX, need to update max drawer width for the large screen use case. The previous
524         // 704dp width no longer works.
525         layoutParams.setMarginEnd((int) resources.getDimension(R.dimen.car_card_margin));
526         mContentView.findViewById(R.id.drawer).setLayoutParams(layoutParams);
527     }
528 
529     private final PagedListView.OnScrollBarListener mOnScrollBarListener =
530             new PagedListView.OnScrollBarListener() {
531 
532                 @Override
533                 public void onReachBottom() {
534                     if (mDrawerController.isTruncatedList()) {
535                         mTruncatedListCardView.setVisibility(View.VISIBLE);
536                     }
537                 }
538 
539                 @Override
540                 public void onLeaveBottom() {
541                     mTruncatedListCardView.setVisibility(View.GONE);
542                 }
543             };
544 }
545