Skip to content

Commit 0cbbc57

Browse files
committed
Obscure speech for PIN password keys when no headset plugged in.
If applicable, also announces that the user needs a headset when displaying the PIN pad layout. Also fixes accessibility focus "falling through" to the next Z-ordered view. Bug: 7436382 Change-Id: Ic1db5320b2e47ff181c5902e9f7980fe3fe6756b
1 parent 0945282 commit 0cbbc57

File tree

4 files changed

+182
-44
lines changed

4 files changed

+182
-44
lines changed

policy/src/com/android/internal/policy/impl/keyguard/KeyguardPINView.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ public void onClick(View v) {
6464
verifyPasswordAndUnlock();
6565
}
6666
});
67-
ok.setOnHoverListener(new NumPadKey.LiftToActivateListener(getContext()));
67+
ok.setOnHoverListener(new LiftToActivateListener(getContext()));
6868
}
6969

7070
// The delete button is of the PIN keyboard itself in some (e.g. tablet) layouts,
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/*
2+
* Copyright (C) 2013 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.internal.policy.impl.keyguard;
18+
19+
import android.content.Context;
20+
import android.view.MotionEvent;
21+
import android.view.View;
22+
import android.view.accessibility.AccessibilityManager;
23+
24+
/**
25+
* Hover listener that implements lift-to-activate interaction for
26+
* accessibility. May be added to multiple views.
27+
*/
28+
class LiftToActivateListener implements View.OnHoverListener {
29+
/** Manager used to query accessibility enabled state. */
30+
private final AccessibilityManager mAccessibilityManager;
31+
32+
private boolean mCachedClickableState;
33+
34+
public LiftToActivateListener(Context context) {
35+
mAccessibilityManager = (AccessibilityManager) context.getSystemService(
36+
Context.ACCESSIBILITY_SERVICE);
37+
}
38+
39+
@Override
40+
public boolean onHover(View v, MotionEvent event) {
41+
// When touch exploration is turned on, lifting a finger while
42+
// inside the view bounds should perform a click action.
43+
if (mAccessibilityManager.isEnabled()
44+
&& mAccessibilityManager.isTouchExplorationEnabled()) {
45+
switch (event.getActionMasked()) {
46+
case MotionEvent.ACTION_HOVER_ENTER:
47+
// Lift-to-type temporarily disables double-tap
48+
// activation by setting the view as not clickable.
49+
mCachedClickableState = v.isClickable();
50+
v.setClickable(false);
51+
break;
52+
case MotionEvent.ACTION_HOVER_EXIT:
53+
final int x = (int) event.getX();
54+
final int y = (int) event.getY();
55+
if ((x > v.getPaddingLeft()) && (y > v.getPaddingTop())
56+
&& (x < v.getWidth() - v.getPaddingRight())
57+
&& (y < v.getHeight() - v.getPaddingBottom())) {
58+
v.performClick();
59+
}
60+
v.setClickable(mCachedClickableState);
61+
break;
62+
}
63+
}
64+
65+
// Pass the event to View.onHoverEvent() to handle accessibility.
66+
v.onHoverEvent(event);
67+
68+
// Consume the event so it doesn't fall through to other views.
69+
return true;
70+
}
71+
}

policy/src/com/android/internal/policy/impl/keyguard/NumPadKey.java

Lines changed: 9 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,7 @@
2222
import android.text.style.TextAppearanceSpan;
2323
import android.util.AttributeSet;
2424
import android.view.HapticFeedbackConstants;
25-
import android.view.MotionEvent;
2625
import android.view.View;
27-
import android.view.accessibility.AccessibilityManager;
2826
import android.widget.Button;
2927
import android.widget.TextView;
3028

@@ -75,6 +73,7 @@ public NumPadKey(Context context, AttributeSet attrs, int defStyle) {
7573

7674
setOnClickListener(mListener);
7775
setOnHoverListener(new LiftToActivateListener(context));
76+
setAccessibilityDelegate(new ObscureSpeechDelegate(context));
7877

7978
mEnableHaptics = new LockPatternUtils(context).isTactileFeedbackEnabled();
8079

@@ -99,6 +98,14 @@ public NumPadKey(Context context, AttributeSet attrs, int defStyle) {
9998
setText(builder);
10099
}
101100

101+
@Override
102+
public void onDetachedFromWindow() {
103+
super.onDetachedFromWindow();
104+
105+
// Reset the "announced headset" flag when detached.
106+
ObscureSpeechDelegate.sAnnouncedHeadset = false;
107+
}
108+
102109
public void setTextView(TextView tv) {
103110
mTextView = tv;
104111
}
@@ -116,45 +123,4 @@ public void doHapticKeyClick() {
116123
| HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING);
117124
}
118125
}
119-
120-
/**
121-
* Hover listener that implements lift-to-activate interaction for
122-
* accessibility. May be added to multiple views.
123-
*/
124-
static class LiftToActivateListener implements View.OnHoverListener {
125-
/** Manager used to query accessibility enabled state. */
126-
private final AccessibilityManager mAccessibilityManager;
127-
128-
public LiftToActivateListener(Context context) {
129-
mAccessibilityManager = (AccessibilityManager) context.getSystemService(
130-
Context.ACCESSIBILITY_SERVICE);
131-
}
132-
133-
@Override
134-
public boolean onHover(View v, MotionEvent event) {
135-
// When touch exploration is turned on, lifting a finger while
136-
// inside the view bounds should perform a click action.
137-
if (mAccessibilityManager.isEnabled()
138-
&& mAccessibilityManager.isTouchExplorationEnabled()) {
139-
switch (event.getActionMasked()) {
140-
case MotionEvent.ACTION_HOVER_ENTER:
141-
// Lift-to-type temporarily disables double-tap
142-
// activation.
143-
v.setClickable(false);
144-
break;
145-
case MotionEvent.ACTION_HOVER_EXIT:
146-
final int x = (int) event.getX();
147-
final int y = (int) event.getY();
148-
if ((x > v.getPaddingLeft()) && (y > v.getPaddingTop())
149-
&& (x < v.getWidth() - v.getPaddingRight())
150-
&& (y < v.getHeight() - v.getPaddingBottom())) {
151-
v.performClick();
152-
}
153-
v.setClickable(true);
154-
break;
155-
}
156-
}
157-
return false;
158-
}
159-
}
160126
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
/*
2+
* Copyright (C) 2013 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.internal.policy.impl.keyguard;
18+
19+
import android.content.ContentResolver;
20+
import android.content.Context;
21+
import android.media.AudioManager;
22+
import android.provider.Settings;
23+
import android.view.View;
24+
import android.view.View.AccessibilityDelegate;
25+
import android.view.accessibility.AccessibilityEvent;
26+
import android.view.accessibility.AccessibilityNodeInfo;
27+
28+
import com.android.internal.R;
29+
30+
/**
31+
* Accessibility delegate that obscures speech for a view when the user has
32+
* not turned on the "speak passwords" preference and is not listening
33+
* through headphones.
34+
*/
35+
class ObscureSpeechDelegate extends AccessibilityDelegate {
36+
/** Whether any client has announced the "headset" notification. */
37+
static boolean sAnnouncedHeadset = false;
38+
39+
private final ContentResolver mContentResolver;
40+
private final AudioManager mAudioManager;
41+
42+
public ObscureSpeechDelegate(Context context) {
43+
mContentResolver = context.getContentResolver();
44+
mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
45+
}
46+
47+
@Override
48+
public void sendAccessibilityEvent(View host, int eventType) {
49+
super.sendAccessibilityEvent(host, eventType);
50+
51+
// Play the "headset required" announcement the first time the user
52+
// places accessibility focus on a key.
53+
if ((eventType == AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED)
54+
&& !sAnnouncedHeadset && shouldObscureSpeech()) {
55+
sAnnouncedHeadset = true;
56+
host.announceForAccessibility(host.getContext().getString(
57+
R.string.keyboard_headset_required_to_hear_password));
58+
}
59+
}
60+
61+
@Override
62+
public void onPopulateAccessibilityEvent(View host, AccessibilityEvent event) {
63+
super.onPopulateAccessibilityEvent(host, event);
64+
65+
if ((event.getEventType() != AccessibilityEvent.TYPE_ANNOUNCEMENT)
66+
&& shouldObscureSpeech()) {
67+
event.getText().clear();
68+
event.setContentDescription(host.getContext().getString(
69+
R.string.keyboard_password_character_no_headset));
70+
}
71+
}
72+
73+
@Override
74+
public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
75+
super.onInitializeAccessibilityNodeInfo(host, info);
76+
77+
if (shouldObscureSpeech()) {
78+
final Context ctx = host.getContext();
79+
info.setText(null);
80+
info.setContentDescription(
81+
ctx.getString(R.string.keyboard_password_character_no_headset));
82+
}
83+
}
84+
85+
@SuppressWarnings("deprecation")
86+
private boolean shouldObscureSpeech() {
87+
// The user can optionally force speaking passwords.
88+
if (Settings.Secure.getInt(mContentResolver,
89+
Settings.Secure.ACCESSIBILITY_SPEAK_PASSWORD, 0) != 0) {
90+
return false;
91+
}
92+
93+
// Always speak if the user is listening through headphones.
94+
if (mAudioManager.isWiredHeadsetOn() || mAudioManager.isBluetoothA2dpOn()) {
95+
return false;
96+
}
97+
98+
// Don't speak since this key is used to type a password.
99+
return true;
100+
}
101+
}

0 commit comments

Comments
 (0)