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