Skip to content

Commit 33aef98

Browse files
committed
Allowing association between a view and its label for accessibility.
1. For accessibility purposes it is important to be able to associate a view with content with a view that labels it. For example, if an accessibility service knows that a TextView is associated with an EditText, it can provide much richer feedback. This change adds APIs for setting a view to be the label for another one and setting the label for a view, i.e. the reverse association. bug:5016937 Change-Id: I7b837265c5ed9302e3ce352396dc6e88413038b5
1 parent 0f75513 commit 33aef98

File tree

6 files changed

+258
-13
lines changed

6 files changed

+258
-13
lines changed

api/current.txt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -619,6 +619,7 @@ package android {
619619
field public static final int keycode = 16842949; // 0x10100c5
620620
field public static final int killAfterRestore = 16843420; // 0x101029c
621621
field public static final int label = 16842753; // 0x1010001
622+
field public static final int labelFor = 16843717; // 0x10103c5
622623
field public static final int labelTextSize = 16843317; // 0x1010235
623624
field public static final int largeHeap = 16843610; // 0x101035a
624625
field public static final int largeScreens = 16843398; // 0x1010286
@@ -24853,6 +24854,7 @@ package android.view {
2485324854
method public int getImportantForAccessibility();
2485424855
method public boolean getKeepScreenOn();
2485524856
method public android.view.KeyEvent.DispatcherState getKeyDispatcherState();
24857+
method public int getLabelFor();
2485624858
method public int getLayerType();
2485724859
method public int getLayoutDirection();
2485824860
method public android.view.ViewGroup.LayoutParams getLayoutParams();
@@ -25115,6 +25117,7 @@ package android.view {
2511525117
method public void setId(int);
2511625118
method public void setImportantForAccessibility(int);
2511725119
method public void setKeepScreenOn(boolean);
25120+
method public void setLabelFor(int);
2511825121
method public void setLayerPaint(android.graphics.Paint);
2511925122
method public void setLayerType(int, android.graphics.Paint);
2512025123
method public void setLayoutDirection(int);
@@ -26106,6 +26109,8 @@ package android.view.accessibility {
2610626109
method public int getChildCount();
2610726110
method public java.lang.CharSequence getClassName();
2610826111
method public java.lang.CharSequence getContentDescription();
26112+
method public android.view.accessibility.AccessibilityNodeInfo getLabelFor();
26113+
method public android.view.accessibility.AccessibilityNodeInfo getLabeledBy();
2610926114
method public int getMovementGranularities();
2611026115
method public java.lang.CharSequence getPackageName();
2611126116
method public android.view.accessibility.AccessibilityNodeInfo getParent();
@@ -26141,6 +26146,10 @@ package android.view.accessibility {
2614126146
method public void setEnabled(boolean);
2614226147
method public void setFocusable(boolean);
2614326148
method public void setFocused(boolean);
26149+
method public void setLabelFor(android.view.View);
26150+
method public void setLabelFor(android.view.View, int);
26151+
method public void setLabeledBy(android.view.View);
26152+
method public void setLabeledBy(android.view.View, int);
2614426153
method public void setLongClickable(boolean);
2614526154
method public void setMovementGranularities(int);
2614626155
method public void setPackageName(java.lang.CharSequence);
@@ -28997,6 +29006,7 @@ package android.widget {
2899729006
method public void setImageViewUri(int, android.net.Uri);
2899829007
method public void setInt(int, java.lang.String, int);
2899929008
method public void setIntent(int, java.lang.String, android.content.Intent);
29009+
method public void setLabelFor(int, int);
2900029010
method public void setLong(int, java.lang.String, long);
2900129011
method public void setOnClickFillInIntent(int, android.content.Intent);
2900229012
method public void setOnClickPendingIntent(int, android.app.PendingIntent);

core/java/android/view/View.java

Lines changed: 104 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2758,6 +2758,23 @@ static class TransformationInfo {
27582758
*/
27592759
private CharSequence mContentDescription;
27602760

2761+
/**
2762+
* Specifies the id of a view for which this view serves as a label for
2763+
* accessibility purposes.
2764+
*/
2765+
private int mLabelForId = View.NO_ID;
2766+
2767+
/**
2768+
* Predicate for matching labeled view id with its label for
2769+
* accessibility purposes.
2770+
*/
2771+
private MatchLabelForPredicate mMatchLabelForPredicate;
2772+
2773+
/**
2774+
* Predicate for matching a view by its id.
2775+
*/
2776+
private MatchIdPredicate mMatchIdPredicate;
2777+
27612778
/**
27622779
* Cache the paddingRight set by the user to append to the scrollbar's size.
27632780
*
@@ -3370,6 +3387,9 @@ public View(Context context, AttributeSet attrs, int defStyle) {
33703387
case com.android.internal.R.styleable.View_contentDescription:
33713388
setContentDescription(a.getString(attr));
33723389
break;
3390+
case com.android.internal.R.styleable.View_labelFor:
3391+
setLabelFor(a.getResourceId(attr, NO_ID));
3392+
break;
33733393
case com.android.internal.R.styleable.View_soundEffectsEnabled:
33743394
if (!a.getBoolean(attr, true)) {
33753395
viewFlagValues &= ~SOUND_EFFECTS_ENABLED;
@@ -4837,6 +4857,28 @@ void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
48374857
info.setParent((View) parent);
48384858
}
48394859

4860+
if (mID != View.NO_ID) {
4861+
View rootView = getRootView();
4862+
if (rootView == null) {
4863+
rootView = this;
4864+
}
4865+
View label = rootView.findLabelForView(this, mID);
4866+
if (label != null) {
4867+
info.setLabeledBy(label);
4868+
}
4869+
}
4870+
4871+
if (mLabelForId != View.NO_ID) {
4872+
View rootView = getRootView();
4873+
if (rootView == null) {
4874+
rootView = this;
4875+
}
4876+
View labeled = rootView.findViewInsideOutShouldExist(this, mLabelForId);
4877+
if (labeled != null) {
4878+
info.setLabelFor(labeled);
4879+
}
4880+
}
4881+
48404882
info.setVisibleToUser(isVisibleToUser());
48414883

48424884
info.setPackageName(mContext.getPackageName());
@@ -4888,6 +4930,14 @@ void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
48884930
}
48894931
}
48904932

4933+
private View findLabelForView(View view, int labeledId) {
4934+
if (mMatchLabelForPredicate == null) {
4935+
mMatchLabelForPredicate = new MatchLabelForPredicate();
4936+
}
4937+
mMatchLabelForPredicate.mLabeledId = labeledId;
4938+
return findViewByPredicateInsideOut(view, mMatchLabelForPredicate);
4939+
}
4940+
48914941
/**
48924942
* Computes whether this view is visible to the user. Such a view is
48934943
* attached, visible, all its predecessors are visible, it is not clipped
@@ -5058,6 +5108,32 @@ public void setContentDescription(CharSequence contentDescription) {
50585108
}
50595109
}
50605110

5111+
/**
5112+
* Gets the id of a view for which this view serves as a label for
5113+
* accessibility purposes.
5114+
*
5115+
* @return The labeled view id.
5116+
*/
5117+
@ViewDebug.ExportedProperty(category = "accessibility")
5118+
public int getLabelFor() {
5119+
return mLabelForId;
5120+
}
5121+
5122+
/**
5123+
* Sets the id of a view for which this view serves as a label for
5124+
* accessibility purposes.
5125+
*
5126+
* @param id The labeled view id.
5127+
*/
5128+
@RemotableViewMethod
5129+
public void setLabelFor(int id) {
5130+
mLabelForId = id;
5131+
if (mLabelForId != View.NO_ID
5132+
&& mID == View.NO_ID) {
5133+
mID = generateViewId();
5134+
}
5135+
}
5136+
50615137
/**
50625138
* Invoked whenever this view loses focus, either by losing window focus or by losing
50635139
* focus within its window. This method can be used to clear any state tied to the
@@ -6110,17 +6186,14 @@ public boolean apply(View t) {
61106186
return null;
61116187
}
61126188

6113-
private View findViewInsideOutShouldExist(View root, final int childViewId) {
6114-
View result = root.findViewByPredicateInsideOut(this, new Predicate<View>() {
6115-
@Override
6116-
public boolean apply(View t) {
6117-
return t.mID == childViewId;
6118-
}
6119-
});
6120-
6189+
private View findViewInsideOutShouldExist(View root, int id) {
6190+
if (mMatchIdPredicate == null) {
6191+
mMatchIdPredicate = new MatchIdPredicate();
6192+
}
6193+
mMatchIdPredicate.mId = id;
6194+
View result = root.findViewByPredicateInsideOut(this, mMatchIdPredicate);
61216195
if (result == null) {
6122-
Log.w(VIEW_LOG_TAG, "couldn't find next focus view specified "
6123-
+ "by user for id " + childViewId);
6196+
Log.w(VIEW_LOG_TAG, "couldn't find view with id " + id);
61246197
}
61256198
return result;
61266199
}
@@ -14922,6 +14995,9 @@ public final View findViewByPredicateInsideOut(View start, Predicate<View> predi
1492214995
*/
1492314996
public void setId(int id) {
1492414997
mID = id;
14998+
if (mID == View.NO_ID && mLabelForId != View.NO_ID) {
14999+
mID = generateViewId();
15000+
}
1492515001
}
1492615002

1492715003
/**
@@ -18008,4 +18084,22 @@ public AccessibilityNodeProvider getAccessibilityNodeProvider(View host) {
1800818084
return null;
1800918085
}
1801018086
}
18087+
18088+
private class MatchIdPredicate implements Predicate<View> {
18089+
public int mId;
18090+
18091+
@Override
18092+
public boolean apply(View view) {
18093+
return (view.mID == mId);
18094+
}
18095+
}
18096+
18097+
private class MatchLabelForPredicate implements Predicate<View> {
18098+
private int mLabeledId;
18099+
18100+
@Override
18101+
public boolean apply(View view) {
18102+
return (view.mLabelForId == mLabeledId);
18103+
}
18104+
}
1801118105
}

core/java/android/view/accessibility/AccessibilityNodeInfo.java

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,8 @@ public static long makeNodeId(int accessibilityViewId, int virtualDescendantId)
365365
private int mWindowId = UNDEFINED;
366366
private long mSourceNodeId = ROOT_NODE_ID;
367367
private long mParentNodeId = ROOT_NODE_ID;
368+
private long mLabelForId = ROOT_NODE_ID;
369+
private long mLabeledById = ROOT_NODE_ID;
368370

369371
private int mBooleanProperties;
370372
private final Rect mBoundsInParent = new Rect();
@@ -1232,6 +1234,120 @@ public void setContentDescription(CharSequence contentDescription) {
12321234
mContentDescription = contentDescription;
12331235
}
12341236

1237+
/**
1238+
* Sets the view for which the view represented by this info serves as a
1239+
* label for accessibility purposes.
1240+
*
1241+
* @param labeled The view for which this info serves as a label.
1242+
*/
1243+
public void setLabelFor(View labeled) {
1244+
setLabelFor(labeled, UNDEFINED);
1245+
}
1246+
1247+
/**
1248+
* Sets the view for which the view represented by this info serves as a
1249+
* label for accessibility purposes. If <code>virtualDescendantId</code>
1250+
* is {@link View#NO_ID} the root is set as the labeled.
1251+
* <p>
1252+
* A virtual descendant is an imaginary View that is reported as a part of the view
1253+
* hierarchy for accessibility purposes. This enables custom views that draw complex
1254+
* content to report themselves as a tree of virtual views, thus conveying their
1255+
* logical structure.
1256+
* </p>
1257+
* <p>
1258+
* <strong>Note:</strong> Cannot be called from an
1259+
* {@link android.accessibilityservice.AccessibilityService}.
1260+
* This class is made immutable before being delivered to an AccessibilityService.
1261+
* </p>
1262+
*
1263+
* @param root The root whose virtual descendant serves as a label.
1264+
* @param virtualDescendantId The id of the virtual descendant.
1265+
*/
1266+
public void setLabelFor(View root, int virtualDescendantId) {
1267+
enforceNotSealed();
1268+
final int rootAccessibilityViewId = (root != null)
1269+
? root.getAccessibilityViewId() : UNDEFINED;
1270+
mLabelForId = makeNodeId(rootAccessibilityViewId, virtualDescendantId);
1271+
}
1272+
1273+
/**
1274+
* Gets the node info for which the view represented by this info serves as
1275+
* a label for accessibility purposes.
1276+
* <p>
1277+
* <strong>Note:</strong> It is a client responsibility to recycle the
1278+
* received info by calling {@link AccessibilityNodeInfo#recycle()}
1279+
* to avoid creating of multiple instances.
1280+
* </p>
1281+
*
1282+
* @return The labeled info.
1283+
*/
1284+
public AccessibilityNodeInfo getLabelFor() {
1285+
enforceSealed();
1286+
if (!canPerformRequestOverConnection(mLabelForId)) {
1287+
return null;
1288+
}
1289+
AccessibilityInteractionClient client = AccessibilityInteractionClient.getInstance();
1290+
return client.findAccessibilityNodeInfoByAccessibilityId(mConnectionId,
1291+
mWindowId, mLabelForId, FLAG_PREFETCH_DESCENDANTS | FLAG_PREFETCH_SIBLINGS);
1292+
}
1293+
1294+
/**
1295+
* Sets the view which serves as the label of the view represented by
1296+
* this info for accessibility purposes.
1297+
*
1298+
* @param label The view that labels this node's source.
1299+
*/
1300+
public void setLabeledBy(View label) {
1301+
setLabeledBy(label, UNDEFINED);
1302+
}
1303+
1304+
/**
1305+
* Sets the view which serves as the label of the view represented by
1306+
* this info for accessibility purposes. If <code>virtualDescendantId</code>
1307+
* is {@link View#NO_ID} the root is set as the label.
1308+
* <p>
1309+
* A virtual descendant is an imaginary View that is reported as a part of the view
1310+
* hierarchy for accessibility purposes. This enables custom views that draw complex
1311+
* content to report themselves as a tree of virtual views, thus conveying their
1312+
* logical structure.
1313+
* </p>
1314+
* <p>
1315+
* <strong>Note:</strong> Cannot be called from an
1316+
* {@link android.accessibilityservice.AccessibilityService}.
1317+
* This class is made immutable before being delivered to an AccessibilityService.
1318+
* </p>
1319+
*
1320+
* @param root The root whose virtual descendant labels this node's source.
1321+
* @param virtualDescendantId The id of the virtual descendant.
1322+
*/
1323+
public void setLabeledBy(View root, int virtualDescendantId) {
1324+
enforceNotSealed();
1325+
final int rootAccessibilityViewId = (root != null)
1326+
? root.getAccessibilityViewId() : UNDEFINED;
1327+
mLabeledById = makeNodeId(rootAccessibilityViewId, virtualDescendantId);
1328+
}
1329+
1330+
/**
1331+
* Gets the node info which serves as the label of the view represented by
1332+
* this info for accessibility purposes.
1333+
* <p>
1334+
* <strong>Note:</strong> It is a client responsibility to recycle the
1335+
* received info by calling {@link AccessibilityNodeInfo#recycle()}
1336+
* to avoid creating of multiple instances.
1337+
* </p>
1338+
*
1339+
* @return The label.
1340+
*/
1341+
public AccessibilityNodeInfo getLabeledBy() {
1342+
enforceSealed();
1343+
if (!canPerformRequestOverConnection(mLabeledById)) {
1344+
return null;
1345+
}
1346+
AccessibilityInteractionClient client = AccessibilityInteractionClient.getInstance();
1347+
return client.findAccessibilityNodeInfoByAccessibilityId(mConnectionId,
1348+
mWindowId, mLabeledById, FLAG_PREFETCH_DESCENDANTS | FLAG_PREFETCH_SIBLINGS);
1349+
}
1350+
12351351
/**
12361352
* Gets the value of a boolean property.
12371353
*
@@ -1462,6 +1578,8 @@ public void writeToParcel(Parcel parcel, int flags) {
14621578
parcel.writeLong(mSourceNodeId);
14631579
parcel.writeInt(mWindowId);
14641580
parcel.writeLong(mParentNodeId);
1581+
parcel.writeLong(mLabelForId);
1582+
parcel.writeLong(mLabeledById);
14651583
parcel.writeInt(mConnectionId);
14661584

14671585
SparseLongArray childIds = mChildNodeIds;
@@ -1507,6 +1625,8 @@ private void init(AccessibilityNodeInfo other) {
15071625
mSealed = other.mSealed;
15081626
mSourceNodeId = other.mSourceNodeId;
15091627
mParentNodeId = other.mParentNodeId;
1628+
mLabelForId = other.mLabelForId;
1629+
mLabeledById = other.mLabeledById;
15101630
mWindowId = other.mWindowId;
15111631
mConnectionId = other.mConnectionId;
15121632
mBoundsInParent.set(other.mBoundsInParent);
@@ -1534,6 +1654,8 @@ private void initFromParcel(Parcel parcel) {
15341654
mSourceNodeId = parcel.readLong();
15351655
mWindowId = parcel.readInt();
15361656
mParentNodeId = parcel.readLong();
1657+
mLabelForId = parcel.readLong();
1658+
mLabeledById = parcel.readLong();
15371659
mConnectionId = parcel.readInt();
15381660

15391661
SparseLongArray childIds = mChildNodeIds;
@@ -1572,6 +1694,8 @@ private void clear() {
15721694
mSealed = false;
15731695
mSourceNodeId = ROOT_NODE_ID;
15741696
mParentNodeId = ROOT_NODE_ID;
1697+
mLabelForId = ROOT_NODE_ID;
1698+
mLabeledById = ROOT_NODE_ID;
15751699
mWindowId = UNDEFINED;
15761700
mConnectionId = UNDEFINED;
15771701
mMovementGranularities = 0;

0 commit comments

Comments
 (0)