1 /* 2 * Copyright (C) 2018 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.phone; 18 19 import android.animation.Animator; 20 import android.animation.AnimatorListenerAdapter; 21 import android.content.Context; 22 import android.util.AttributeSet; 23 import android.view.MotionEvent; 24 import android.view.View; 25 import android.view.ViewAnimationUtils; 26 import android.view.accessibility.AccessibilityManager; 27 import android.widget.FrameLayout; 28 import android.widget.ImageView; 29 import android.widget.TextView; 30 31 import androidx.annotation.NonNull; 32 33 /** 34 * Emergency shortcut button displays a local emergency phone number information(including phone 35 * number, and phone type). To decrease false clicking, it need to click twice to confirm to place 36 * an emergency phone call. 37 * 38 * <p> The button need to be set an {@link OnConfirmClickListener} from activity to handle dial 39 * function. 40 * 41 * <p> First clicking on the button, it would change the view of call number information to 42 * the view of confirmation. And then clicking on the view of confirmation, it will place an 43 * emergency call. 44 * 45 * <p> For screen reader, it changed to click twice on the view of call number information to 46 * place an emergency call. The view of confirmation will not display. 47 */ 48 public class EmergencyShortcutButton extends FrameLayout implements View.OnClickListener { 49 // Time to hide view of confirmation. 50 private static final long HIDE_DELAY = 3000; 51 52 private static final int[] ICON_VIEWS = {R.id.phone_type_icon, R.id.confirmed_phone_type_icon}; 53 private View mCallNumberInfoView; 54 private View mConfirmView; 55 56 private TextView mPhoneNumber; 57 private TextView mPhoneTypeDescription; 58 private TextView mPhoneCallHint; 59 private MotionEvent mPendingTouchEvent; 60 private OnConfirmClickListener mOnConfirmClickListener; 61 62 private boolean mConfirmViewHiding; 63 EmergencyShortcutButton(Context context, AttributeSet attrs)64 public EmergencyShortcutButton(Context context, AttributeSet attrs) { 65 super(context, attrs); 66 } 67 68 /** 69 * Interface definition for a callback to be invoked when the view of confirmation on shortcut 70 * button is clicked. 71 */ 72 public interface OnConfirmClickListener { 73 /** 74 * Called when the view of confirmation on shortcut button has been clicked. 75 * 76 * @param button The shortcut button that was clicked. 77 */ onConfirmClick(EmergencyShortcutButton button)78 void onConfirmClick(EmergencyShortcutButton button); 79 } 80 81 /** 82 * Register a callback {@link OnConfirmClickListener} to be invoked when view of confirmation 83 * is clicked. 84 * 85 * @param onConfirmClickListener The callback that will run. 86 */ setOnConfirmClickListener(OnConfirmClickListener onConfirmClickListener)87 public void setOnConfirmClickListener(OnConfirmClickListener onConfirmClickListener) { 88 mOnConfirmClickListener = onConfirmClickListener; 89 } 90 91 /** 92 * Set icon for different phone number type. 93 * 94 * @param resId The resource identifier of the drawable. 95 */ setPhoneTypeIcon(int resId)96 public void setPhoneTypeIcon(int resId) { 97 for (int iconView : ICON_VIEWS) { 98 ImageView phoneTypeIcon = findViewById(iconView); 99 phoneTypeIcon.setImageResource(resId); 100 } 101 } 102 103 /** 104 * Set emergency phone number description. 105 */ setPhoneDescription(@onNull CharSequence description)106 public void setPhoneDescription(@NonNull CharSequence description) { 107 mPhoneTypeDescription.setText(description); 108 } 109 110 /** 111 * Set emergency phone number. 112 */ setPhoneNumber(@onNull CharSequence number)113 public void setPhoneNumber(@NonNull CharSequence number) { 114 mPhoneNumber.setText(number); 115 mPhoneCallHint.setText( 116 getContext().getString(R.string.emergency_call_shortcut_hint, number)); 117 118 // Set content description for phone number. 119 if (number.length() > 1) { 120 StringBuilder stringBuilder = new StringBuilder(); 121 for (char c : number.toString().toCharArray()) { 122 stringBuilder.append(c).append(" "); 123 } 124 mPhoneNumber.setContentDescription(stringBuilder.toString().trim()); 125 } 126 } 127 128 /** 129 * Get emergency phone number. 130 * 131 * @return phone number, or {@code null} if {@code mPhoneNumber} does not be set. 132 */ getPhoneNumber()133 public String getPhoneNumber() { 134 return mPhoneNumber != null ? mPhoneNumber.getText().toString() : null; 135 } 136 137 /** 138 * Called by the activity before a touch event is dispatched to the view hierarchy. 139 */ onPreTouchEvent(MotionEvent event)140 public void onPreTouchEvent(MotionEvent event) { 141 mPendingTouchEvent = event; 142 } 143 144 @Override dispatchTouchEvent(MotionEvent event)145 public boolean dispatchTouchEvent(MotionEvent event) { 146 boolean handled = super.dispatchTouchEvent(event); 147 if (mPendingTouchEvent == event && handled) { 148 mPendingTouchEvent = null; 149 } 150 return handled; 151 } 152 153 /** 154 * Called by the activity after a touch event is dispatched to the view hierarchy. 155 */ onPostTouchEvent(MotionEvent event)156 public void onPostTouchEvent(MotionEvent event) { 157 // Hide the confirmation button if a touch event was delivered to the activity but not to 158 // this view. 159 if (mPendingTouchEvent != null) { 160 hideSelectedButton(); 161 } 162 mPendingTouchEvent = null; 163 } 164 165 @Override onFinishInflate()166 protected void onFinishInflate() { 167 super.onFinishInflate(); 168 mCallNumberInfoView = findViewById(R.id.emergency_call_number_info_view); 169 mConfirmView = findViewById(R.id.emergency_call_confirm_view); 170 171 mCallNumberInfoView.setOnClickListener(this); 172 mConfirmView.setOnClickListener(this); 173 174 mPhoneNumber = (TextView) mCallNumberInfoView.findViewById(R.id.phone_number); 175 mPhoneTypeDescription = (TextView) mCallNumberInfoView.findViewById( 176 R.id.phone_number_description); 177 178 mPhoneCallHint = (TextView) mConfirmView.findViewById(R.id.phone_call_hint); 179 180 mConfirmViewHiding = true; 181 } 182 183 @Override onClick(View view)184 public void onClick(View view) { 185 if (view.getId() == R.id.emergency_call_number_info_view) { 186 AccessibilityManager accessibilityMgr = 187 (AccessibilityManager) getContext().getSystemService( 188 Context.ACCESSIBILITY_SERVICE); 189 if (accessibilityMgr.isTouchExplorationEnabled()) { 190 // TalkBack itself includes a prompt to confirm click action implicitly, 191 // so we don't need an additional confirmation with second tap on button. 192 if (mOnConfirmClickListener != null) { 193 mOnConfirmClickListener.onConfirmClick(this); 194 } 195 } else { 196 revealSelectedButton(); 197 } 198 } else if (view.getId() == R.id.emergency_call_confirm_view) { 199 if (mOnConfirmClickListener != null) { 200 mOnConfirmClickListener.onConfirmClick(this); 201 } 202 } 203 } 204 revealSelectedButton()205 private void revealSelectedButton() { 206 mConfirmViewHiding = false; 207 208 mConfirmView.setVisibility(View.VISIBLE); 209 int centerX = mCallNumberInfoView.getLeft() + mCallNumberInfoView.getWidth() / 2; 210 int centerY = mCallNumberInfoView.getTop() + mCallNumberInfoView.getHeight() / 2; 211 Animator reveal = ViewAnimationUtils.createCircularReveal( 212 mConfirmView, 213 centerX, 214 centerY, 215 0, 216 Math.max(centerX, mConfirmView.getWidth() - centerX) 217 + Math.max(centerY, mConfirmView.getHeight() - centerY)); 218 reveal.start(); 219 220 postDelayed(mCancelSelectedButtonRunnable, HIDE_DELAY); 221 mConfirmView.requestFocus(); 222 } 223 hideSelectedButton()224 private void hideSelectedButton() { 225 if (mConfirmViewHiding || mConfirmView.getVisibility() != VISIBLE) { 226 return; 227 } 228 229 mConfirmViewHiding = true; 230 231 removeCallbacks(mCancelSelectedButtonRunnable); 232 int centerX = mConfirmView.getLeft() + mConfirmView.getWidth() / 2; 233 int centerY = mConfirmView.getTop() + mConfirmView.getHeight() / 2; 234 Animator reveal = ViewAnimationUtils.createCircularReveal( 235 mConfirmView, 236 centerX, 237 centerY, 238 Math.max(centerX, mCallNumberInfoView.getWidth() - centerX) 239 + Math.max(centerY, mCallNumberInfoView.getHeight() - centerY), 240 0); 241 reveal.addListener(new AnimatorListenerAdapter() { 242 @Override 243 public void onAnimationEnd(Animator animation) { 244 mConfirmView.setVisibility(INVISIBLE); 245 } 246 }); 247 reveal.start(); 248 249 mCallNumberInfoView.requestFocus(); 250 } 251 252 private final Runnable mCancelSelectedButtonRunnable = new Runnable() { 253 @Override 254 public void run() { 255 if (!isAttachedToWindow()) return; 256 hideSelectedButton(); 257 } 258 }; 259 } 260