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