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 */
16package android.car.ui.provider;
17
18import android.car.app.menu.CarMenuCallbacks;
19import android.car.app.menu.RootMenu;
20import android.car.app.menu.SearchBoxEditListener;
21import android.content.Context;
22import android.content.res.Resources;
23import android.graphics.Bitmap;
24import android.graphics.PorterDuff;
25import android.graphics.PorterDuffColorFilter;
26import android.graphics.drawable.BitmapDrawable;
27import android.os.Bundle;
28import android.support.car.input.CarRestrictedEditText;
29import android.support.car.ui.DrawerArrowDrawable;
30import android.support.car.ui.PagedListView;
31import android.support.v7.widget.CardView;
32import android.text.Editable;
33import android.text.TextWatcher;
34import android.util.Log;
35import android.view.Gravity;
36import android.view.KeyEvent;
37import android.widget.EditText;
38import android.widget.FrameLayout;
39import android.widget.ImageView;
40import android.widget.LinearLayout;
41import android.widget.TextView;
42import android.view.View;
43import android.view.LayoutInflater;
44
45import android.support.car.ui.R;
46
47public 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         */
84        void onClick();
85    }
86
87    public CarUiEntry(Context providerContext, Context appContext) {
88        super(providerContext, appContext);
89    }
90
91    @Override
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
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
182    public int getFragmentContainerId() {
183        return R.id.container;
184    }
185
186    @Override
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
194    public void hideMenuButton() {
195        mMenuButton.setVisibility(View.GONE);
196    }
197
198    @Override
199    public void restoreMenuDrawable() {
200        mMenuButton.setImageDrawable(mDrawerArrowDrawable);
201    }
202
203    public void setMenuButtonBitmap(Bitmap bitmap) {
204        mMenuButton.setImageDrawable(new BitmapDrawable(mUiLibContext.getResources(), bitmap));
205    }
206
207    @Override
208    public void setScrimColor(int color) {
209        mDrawerLayout.setScrimColor(color);
210    }
211
212    @Override
213    public void setTitle(CharSequence title) {
214        mDrawerController.setTitle(title);
215    }
216
217    @Override
218    public void closeDrawer() {
219        mDrawerController.closeDrawer();
220    }
221
222    @Override
223    public void openDrawer() {
224        mDrawerController.openDrawer();
225    }
226
227    @Override
228    public void showMenu(String id, String title) {
229        mDrawerController.showMenu(id, title);
230    }
231
232
233    @Override
234    public void setMenuButtonColor(int color) {
235        setViewColor(mMenuButton, color);
236        setViewColor(mTitleView, color);
237    }
238
239    @Override
240    public void showTitle() {
241        mTitleView.setVisibility(View.VISIBLE);
242    }
243
244    @Override
245    public void hideTitle() {
246        mTitleView.setVisibility(View.GONE);
247    }
248
249    @Override
250    public void setLightMode() {
251        mDrawerController.setLightMode();
252    }
253
254    @Override
255    public void setDarkMode() {
256        mDrawerController.setDarkMode();
257    }
258
259    @Override
260    public void setAutoLightDarkMode() {
261        mDrawerController.setAutoLightDarkMode();
262    }
263
264    @Override
265    public void showToast(String msg, long duration) {
266        // TODO: add toast support
267    }
268
269    @Override
270    public CharSequence getSearchBoxText() {
271        return mCarRestrictedEditText.getText();
272    }
273
274    @Override
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
284    public void onRestoreInstanceState(Bundle savedInstanceState) {
285        if (mDrawerController != null) {
286            mDrawerController.restoreState(savedInstanceState);
287        }
288    }
289
290    @Override
291    public void onSaveInstanceState(Bundle outState) {
292        if (mDrawerController != null) {
293            mDrawerController.saveState(outState);
294        }
295    }
296
297    @Override
298    public void onStart() {
299
300    }
301
302    @Override
303    public void onResume() {
304
305    }
306
307    @Override
308    public void onPause() {
309
310    }
311
312    @Override
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
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
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
349    public void showSearchBox(final View.OnClickListener listener) {
350        setSearchBoxMode(SEARCH_BOX_MODE_SMALL);
351        mSearchBoxClickListener = wrapSearchBoxClickListener(listener);
352    }
353
354    @Override
355    public void stopInput() {
356        setSearchBoxMode(SEARCH_BOX_MODE_NONE);
357    }
358
359    @Override
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     */
372    public void setMenuProgress(float progress) {
373        mDrawerArrowDrawable.setProgress(progress);
374    }
375
376    private void setSearchBoxModeLarge(String hint) {
377        mCarRestrictedEditText.setHint(hint);
378        setSearchBoxMode(SEARCH_BOX_MODE_LARGE);
379    }
380
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     */
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
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