1 /*
2  * Copyright (C) 2017 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.dialer.historyitemactions;
18 
19 import android.content.Context;
20 import android.content.res.ColorStateList;
21 import android.os.Bundle;
22 import android.support.annotation.NonNull;
23 import android.support.design.widget.BottomSheetBehavior;
24 import android.support.design.widget.BottomSheetBehavior.BottomSheetCallback;
25 import android.support.design.widget.BottomSheetDialog;
26 import android.text.TextUtils;
27 import android.view.LayoutInflater;
28 import android.view.View;
29 import android.view.View.OnClickListener;
30 import android.view.ViewGroup;
31 import android.view.ViewTreeObserver.OnPreDrawListener;
32 import android.view.Window;
33 import android.view.accessibility.AccessibilityManager;
34 import android.widget.ImageView;
35 import android.widget.LinearLayout;
36 import android.widget.TextView;
37 import com.android.dialer.common.Assert;
38 import com.android.dialer.compat.android.support.design.bottomsheet.BottomSheetStateCompat;
39 import com.android.dialer.theme.base.ThemeComponent;
40 import com.android.dialer.widget.ContactPhotoView;
41 import com.google.common.collect.ImmutableSet;
42 import java.util.List;
43 
44 /**
45  * {@link BottomSheetDialog} used to show a list of actions in a bottom sheet menu.
46  *
47  * <p>{@link #show(Context, HistoryItemBottomSheetHeaderInfo, List)} should be used to create and
48  * display the menu. Modules are built using {@link HistoryItemActionModule} and some defaults are
49  * provided by {@link IntentModule} and {@link DividerModule}.
50  */
51 public class HistoryItemActionBottomSheet extends BottomSheetDialog implements OnClickListener {
52 
53   private final List<HistoryItemActionModule> modules;
54   private final HistoryItemBottomSheetHeaderInfo historyItemBottomSheetHeaderInfo;
55 
56   /**
57    * An {@link OnPreDrawListener} that sets the contact layout's elevation if
58    *
59    * <ul>
60    *   <li>the bottom sheet can expand to full screen, and
61    *   <li>the bottom sheet is fully expanded.
62    * </ul>
63    *
64    * <p>The reason an {@link OnPreDrawListener} instead of a {@link BottomSheetCallback} is used to
65    * handle this is that the initial state of the bottom sheet will be STATE_EXPANDED when the touch
66    * exploration (e.g., TalkBack) is enabled and {@link BottomSheetCallback} won't be triggered in
67    * this case. See {@link #setupBottomSheetBehavior()} for details.
68    */
69   private final OnPreDrawListener onPreDrawListenerForContactLayout =
70       () -> {
71         View contactLayout = findViewById(R.id.contact_layout_root);
72         View background = findViewById(android.support.design.R.id.touch_outside);
73         View bottomSheet = findViewById(android.support.design.R.id.design_bottom_sheet);
74 
75         BottomSheetBehavior<View> behavior = BottomSheetBehavior.from(bottomSheet);
76 
77         // If the height of the background is equal to that of the bottom sheet, the bottom sheet
78         // *can* be expanded to full screen.
79         contactLayout.setElevation(
80             background.getHeight() == bottomSheet.getHeight()
81                     && behavior.getState() == BottomSheetStateCompat.STATE_EXPANDED
82                 ? getContext()
83                     .getResources()
84                     .getDimensionPixelSize(R.dimen.contact_actions_header_elevation)
85                 : 0);
86 
87         return true; // Return true to proceed with the current drawing pass.
88       };
89 
90   private LinearLayout contactLayout;
91 
HistoryItemActionBottomSheet( Context context, HistoryItemBottomSheetHeaderInfo historyItemBottomSheetHeaderInfo, List<HistoryItemActionModule> modules)92   private HistoryItemActionBottomSheet(
93       Context context,
94       HistoryItemBottomSheetHeaderInfo historyItemBottomSheetHeaderInfo,
95       List<HistoryItemActionModule> modules) {
96     super(context, R.style.HistoryItemBottomSheet);
97     this.modules = modules;
98     this.historyItemBottomSheetHeaderInfo = Assert.isNotNull(historyItemBottomSheetHeaderInfo);
99     setContentView(LayoutInflater.from(context).inflate(R.layout.sheet_layout, null));
100   }
101 
show( Context context, HistoryItemBottomSheetHeaderInfo historyItemBottomSheetHeaderInfo, List<HistoryItemActionModule> modules)102   public static HistoryItemActionBottomSheet show(
103       Context context,
104       HistoryItemBottomSheetHeaderInfo historyItemBottomSheetHeaderInfo,
105       List<HistoryItemActionModule> modules) {
106     HistoryItemActionBottomSheet sheet =
107         new HistoryItemActionBottomSheet(context, historyItemBottomSheetHeaderInfo, modules);
108     sheet.show();
109     return sheet;
110   }
111 
112   @Override
onCreate(Bundle bundle)113   protected void onCreate(Bundle bundle) {
114     contactLayout = Assert.isNotNull(findViewById(R.id.contact_layout_root));
115 
116     initBottomSheetState();
117     setupBottomSheetBehavior();
118     setupWindow();
119     setupContactLayout();
120 
121     LinearLayout container = Assert.isNotNull(findViewById(R.id.action_container));
122     for (HistoryItemActionModule module : modules) {
123       if (module instanceof DividerModule) {
124         container.addView(getDividerView(container));
125       } else {
126         container.addView(getModuleView(container, module));
127       }
128     }
129   }
130 
131   @Override
onAttachedToWindow()132   public void onAttachedToWindow() {
133     super.onAttachedToWindow();
134     contactLayout.getViewTreeObserver().addOnPreDrawListener(onPreDrawListenerForContactLayout);
135   }
136 
137   @Override
onDetachedFromWindow()138   public void onDetachedFromWindow() {
139     super.onDetachedFromWindow();
140     contactLayout.getViewTreeObserver().removeOnPreDrawListener(onPreDrawListenerForContactLayout);
141   }
142 
initBottomSheetState()143   private void initBottomSheetState() {
144     // If the touch exploration in the system (e.g., TalkBack) is enabled, the bottom sheet should
145     // be fully expanded because sometimes services like TalkBack won't read all items when the
146     // bottom sheet is not fully expanded.
147     if (isTouchExplorationEnabled()) {
148       BottomSheetBehavior<View> behavior =
149           BottomSheetBehavior.from(findViewById(android.support.design.R.id.design_bottom_sheet));
150       behavior.setState(BottomSheetStateCompat.STATE_EXPANDED);
151     }
152   }
153 
154   /**
155    * Configures the bottom sheet behavior when its state changes.
156    *
157    * <p>If the touch exploration in the system (e.g., TalkBack) is enabled, the bottom sheet will be
158    * canceled if it is in a final state other than {@link BottomSheetBehavior#STATE_EXPANDED}. This
159    * is because sometimes services like TalkBack won't read all items when the bottom sheet is not
160    * fully expanded.
161    *
162    * <p>If the touch exploration is disabled, cancel the bottom sheet when it is in {@link
163    * BottomSheetBehavior#STATE_HIDDEN}.
164    */
setupBottomSheetBehavior()165   private void setupBottomSheetBehavior() {
166     BottomSheetBehavior<View> behavior =
167         BottomSheetBehavior.from(findViewById(android.support.design.R.id.design_bottom_sheet));
168     behavior.setBottomSheetCallback(
169         new BottomSheetCallback() {
170           @Override
171           public void onStateChanged(@NonNull View bottomSheet, int newState) {
172             ImmutableSet<Integer> statesToCancelBottomSheet =
173                 isTouchExplorationEnabled()
174                     ? ImmutableSet.of(
175                         BottomSheetStateCompat.STATE_COLLAPSED,
176                         BottomSheetStateCompat.STATE_HIDDEN,
177                         BottomSheetStateCompat.STATE_HALF_EXPANDED)
178                     : ImmutableSet.of(BottomSheetStateCompat.STATE_HIDDEN);
179 
180             if (statesToCancelBottomSheet.contains(newState)) {
181               cancel();
182             }
183 
184             // TODO(calderwoodra): set the status bar color when expanded, else translucent
185           }
186 
187           @Override
188           public void onSlide(@NonNull View bottomSheet, float slideOffset) {}
189         });
190   }
191 
192   // Overwrites the window size since it doesn't match the parent.
setupWindow()193   private void setupWindow() {
194     Window window = getWindow();
195     if (window == null) {
196       return;
197     }
198     // TODO(calderwoodra): set the nav bar color
199     window.setLayout(
200         /* width = */ ViewGroup.LayoutParams.MATCH_PARENT,
201         /* height = */ ViewGroup.LayoutParams.MATCH_PARENT);
202   }
203 
setupContactLayout()204   private void setupContactLayout() {
205     ContactPhotoView contactPhotoView = contactLayout.findViewById(R.id.contact_photo_view);
206     contactPhotoView.setPhoto(historyItemBottomSheetHeaderInfo.getPhotoInfo());
207 
208     TextView primaryTextView = contactLayout.findViewById(R.id.primary_text);
209     TextView secondaryTextView = contactLayout.findViewById(R.id.secondary_text);
210 
211     primaryTextView.setText(historyItemBottomSheetHeaderInfo.getPrimaryText());
212     if (!TextUtils.isEmpty(historyItemBottomSheetHeaderInfo.getSecondaryText())) {
213       secondaryTextView.setText(historyItemBottomSheetHeaderInfo.getSecondaryText());
214     } else {
215       secondaryTextView.setVisibility(View.GONE);
216       secondaryTextView.setText(null);
217     }
218   }
219 
getDividerView(ViewGroup container)220   private View getDividerView(ViewGroup container) {
221     LayoutInflater inflater = LayoutInflater.from(getContext());
222     return inflater.inflate(R.layout.divider_layout, container, false);
223   }
224 
getModuleView(ViewGroup container, HistoryItemActionModule module)225   private View getModuleView(ViewGroup container, HistoryItemActionModule module) {
226     LayoutInflater inflater = LayoutInflater.from(getContext());
227     View moduleView = inflater.inflate(R.layout.module_layout, container, false);
228     ((TextView) moduleView.findViewById(R.id.module_text)).setText(module.getStringId());
229     ((ImageView) moduleView.findViewById(R.id.module_image))
230         .setImageResource(module.getDrawableId());
231     if (module.tintDrawable()) {
232       ((ImageView) moduleView.findViewById(R.id.module_image))
233           .setImageTintList(
234               ColorStateList.valueOf(ThemeComponent.get(getContext()).theme().getColorIcon()));
235     }
236     moduleView.setOnClickListener(this);
237     moduleView.setTag(module);
238     return moduleView;
239   }
240 
241   @Override
onClick(View view)242   public void onClick(View view) {
243     if (((HistoryItemActionModule) view.getTag()).onClick()) {
244       dismiss();
245     }
246   }
247 
isTouchExplorationEnabled()248   private boolean isTouchExplorationEnabled() {
249     AccessibilityManager accessibilityManager =
250         getContext().getSystemService(AccessibilityManager.class);
251 
252     return accessibilityManager.isTouchExplorationEnabled();
253   }
254 }
255