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