• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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