12030f45f8f411cf2907ad5929feed3882c050a73Noah Wang/* 22030f45f8f411cf2907ad5929feed3882c050a73Noah Wang * Copyright (C) 2016 The Android Open Source Project 32030f45f8f411cf2907ad5929feed3882c050a73Noah Wang * 42030f45f8f411cf2907ad5929feed3882c050a73Noah Wang * Licensed under the Apache License, Version 2.0 (the "License"); 52030f45f8f411cf2907ad5929feed3882c050a73Noah Wang * you may not use this file except in compliance with the License. 62030f45f8f411cf2907ad5929feed3882c050a73Noah Wang * You may obtain a copy of the License at 72030f45f8f411cf2907ad5929feed3882c050a73Noah Wang * 82030f45f8f411cf2907ad5929feed3882c050a73Noah Wang * http://www.apache.org/licenses/LICENSE-2.0 92030f45f8f411cf2907ad5929feed3882c050a73Noah Wang * 102030f45f8f411cf2907ad5929feed3882c050a73Noah Wang * Unless required by applicable law or agreed to in writing, software 112030f45f8f411cf2907ad5929feed3882c050a73Noah Wang * distributed under the License is distributed on an "AS IS" BASIS, 122030f45f8f411cf2907ad5929feed3882c050a73Noah Wang * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 132030f45f8f411cf2907ad5929feed3882c050a73Noah Wang * See the License for the specific language governing permissions and 142030f45f8f411cf2907ad5929feed3882c050a73Noah Wang * limitations under the License. 152030f45f8f411cf2907ad5929feed3882c050a73Noah Wang */ 162030f45f8f411cf2907ad5929feed3882c050a73Noah Wang 172030f45f8f411cf2907ad5929feed3882c050a73Noah Wangpackage com.android.settings.widget; 182030f45f8f411cf2907ad5929feed3882c050a73Noah Wang 192030f45f8f411cf2907ad5929feed3882c050a73Noah Wangimport android.content.Context; 2035a95617fd467b823a6c1c66f0f5d9651752f2efNoah Wangimport android.content.res.Configuration; 212030f45f8f411cf2907ad5929feed3882c050a73Noah Wangimport android.graphics.Rect; 222030f45f8f411cf2907ad5929feed3882c050a73Noah Wangimport android.os.Bundle; 232030f45f8f411cf2907ad5929feed3882c050a73Noah Wangimport android.support.v4.view.ViewCompat; 242030f45f8f411cf2907ad5929feed3882c050a73Noah Wangimport android.support.v4.view.accessibility.AccessibilityNodeInfoCompat; 252030f45f8f411cf2907ad5929feed3882c050a73Noah Wangimport android.support.v4.widget.ExploreByTouchHelper; 262030f45f8f411cf2907ad5929feed3882c050a73Noah Wangimport android.util.AttributeSet; 272030f45f8f411cf2907ad5929feed3882c050a73Noah Wangimport android.view.MotionEvent; 2835a95617fd467b823a6c1c66f0f5d9651752f2efNoah Wangimport android.view.View; 292030f45f8f411cf2907ad5929feed3882c050a73Noah Wangimport android.view.accessibility.AccessibilityEvent; 302030f45f8f411cf2907ad5929feed3882c050a73Noah Wangimport android.widget.RadioButton; 312030f45f8f411cf2907ad5929feed3882c050a73Noah Wangimport android.widget.RadioGroup; 322030f45f8f411cf2907ad5929feed3882c050a73Noah Wangimport android.widget.SeekBar; 332030f45f8f411cf2907ad5929feed3882c050a73Noah Wang 342030f45f8f411cf2907ad5929feed3882c050a73Noah Wangimport java.util.List; 352030f45f8f411cf2907ad5929feed3882c050a73Noah Wang 362030f45f8f411cf2907ad5929feed3882c050a73Noah Wang/** 372030f45f8f411cf2907ad5929feed3882c050a73Noah Wang * LabeledSeekBar represent a seek bar assigned with labeled, discrete values. 382030f45f8f411cf2907ad5929feed3882c050a73Noah Wang * It pretends to be a group of radio button for AccessibilityServices, in order to adjust the 392030f45f8f411cf2907ad5929feed3882c050a73Noah Wang * behavior of these services to keep the mental model of the visual discrete SeekBar. 402030f45f8f411cf2907ad5929feed3882c050a73Noah Wang */ 412030f45f8f411cf2907ad5929feed3882c050a73Noah Wangpublic class LabeledSeekBar extends SeekBar { 422030f45f8f411cf2907ad5929feed3882c050a73Noah Wang 433550a32ca9044fbf05676d68e8b89987521ed121Alan Viverette private final ExploreByTouchHelper mAccessHelper; 443550a32ca9044fbf05676d68e8b89987521ed121Alan Viverette 453550a32ca9044fbf05676d68e8b89987521ed121Alan Viverette /** Seek bar change listener set via public method. */ 463550a32ca9044fbf05676d68e8b89987521ed121Alan Viverette private OnSeekBarChangeListener mOnSeekBarChangeListener; 473550a32ca9044fbf05676d68e8b89987521ed121Alan Viverette 483550a32ca9044fbf05676d68e8b89987521ed121Alan Viverette /** Labels for discrete progress values. */ 493550a32ca9044fbf05676d68e8b89987521ed121Alan Viverette private String[] mLabels; 503550a32ca9044fbf05676d68e8b89987521ed121Alan Viverette 513550a32ca9044fbf05676d68e8b89987521ed121Alan Viverette public LabeledSeekBar(Context context, AttributeSet attrs) { 523550a32ca9044fbf05676d68e8b89987521ed121Alan Viverette this(context, attrs, com.android.internal.R.attr.seekBarStyle); 533550a32ca9044fbf05676d68e8b89987521ed121Alan Viverette } 543550a32ca9044fbf05676d68e8b89987521ed121Alan Viverette 553550a32ca9044fbf05676d68e8b89987521ed121Alan Viverette public LabeledSeekBar(Context context, AttributeSet attrs, int defStyleAttr) { 563550a32ca9044fbf05676d68e8b89987521ed121Alan Viverette this(context, attrs, defStyleAttr, 0); 573550a32ca9044fbf05676d68e8b89987521ed121Alan Viverette } 583550a32ca9044fbf05676d68e8b89987521ed121Alan Viverette 593550a32ca9044fbf05676d68e8b89987521ed121Alan Viverette public LabeledSeekBar(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 603550a32ca9044fbf05676d68e8b89987521ed121Alan Viverette super(context, attrs, defStyleAttr, defStyleRes); 613550a32ca9044fbf05676d68e8b89987521ed121Alan Viverette 623550a32ca9044fbf05676d68e8b89987521ed121Alan Viverette mAccessHelper = new LabeledSeekBarExploreByTouchHelper(this); 633550a32ca9044fbf05676d68e8b89987521ed121Alan Viverette ViewCompat.setAccessibilityDelegate(this, mAccessHelper); 643550a32ca9044fbf05676d68e8b89987521ed121Alan Viverette 653550a32ca9044fbf05676d68e8b89987521ed121Alan Viverette super.setOnSeekBarChangeListener(mProxySeekBarListener); 663550a32ca9044fbf05676d68e8b89987521ed121Alan Viverette } 673550a32ca9044fbf05676d68e8b89987521ed121Alan Viverette 683550a32ca9044fbf05676d68e8b89987521ed121Alan Viverette @Override 693550a32ca9044fbf05676d68e8b89987521ed121Alan Viverette public synchronized void setProgress(int progress) { 703550a32ca9044fbf05676d68e8b89987521ed121Alan Viverette // This method gets called from the constructor, so mAccessHelper may 713550a32ca9044fbf05676d68e8b89987521ed121Alan Viverette // not have been assigned yet. 723550a32ca9044fbf05676d68e8b89987521ed121Alan Viverette if (mAccessHelper != null) { 733550a32ca9044fbf05676d68e8b89987521ed121Alan Viverette mAccessHelper.invalidateRoot(); 743550a32ca9044fbf05676d68e8b89987521ed121Alan Viverette } 753550a32ca9044fbf05676d68e8b89987521ed121Alan Viverette 763550a32ca9044fbf05676d68e8b89987521ed121Alan Viverette super.setProgress(progress); 773550a32ca9044fbf05676d68e8b89987521ed121Alan Viverette } 783550a32ca9044fbf05676d68e8b89987521ed121Alan Viverette 793550a32ca9044fbf05676d68e8b89987521ed121Alan Viverette public void setLabels(String[] labels) { 803550a32ca9044fbf05676d68e8b89987521ed121Alan Viverette mLabels = labels; 813550a32ca9044fbf05676d68e8b89987521ed121Alan Viverette } 823550a32ca9044fbf05676d68e8b89987521ed121Alan Viverette 833550a32ca9044fbf05676d68e8b89987521ed121Alan Viverette @Override 843550a32ca9044fbf05676d68e8b89987521ed121Alan Viverette public void setOnSeekBarChangeListener(OnSeekBarChangeListener l) { 853550a32ca9044fbf05676d68e8b89987521ed121Alan Viverette // The callback set in the constructor will proxy calls to this 863550a32ca9044fbf05676d68e8b89987521ed121Alan Viverette // listener. 873550a32ca9044fbf05676d68e8b89987521ed121Alan Viverette mOnSeekBarChangeListener = l; 883550a32ca9044fbf05676d68e8b89987521ed121Alan Viverette } 893550a32ca9044fbf05676d68e8b89987521ed121Alan Viverette 903550a32ca9044fbf05676d68e8b89987521ed121Alan Viverette @Override 913550a32ca9044fbf05676d68e8b89987521ed121Alan Viverette protected boolean dispatchHoverEvent(MotionEvent event) { 923550a32ca9044fbf05676d68e8b89987521ed121Alan Viverette return mAccessHelper.dispatchHoverEvent(event) || super.dispatchHoverEvent(event); 933550a32ca9044fbf05676d68e8b89987521ed121Alan Viverette } 943550a32ca9044fbf05676d68e8b89987521ed121Alan Viverette 953550a32ca9044fbf05676d68e8b89987521ed121Alan Viverette private void sendClickEventForAccessibility(int progress) { 963550a32ca9044fbf05676d68e8b89987521ed121Alan Viverette mAccessHelper.invalidateRoot(); 973550a32ca9044fbf05676d68e8b89987521ed121Alan Viverette mAccessHelper.sendEventForVirtualView(progress, AccessibilityEvent.TYPE_VIEW_CLICKED); 983550a32ca9044fbf05676d68e8b89987521ed121Alan Viverette } 993550a32ca9044fbf05676d68e8b89987521ed121Alan Viverette 1003550a32ca9044fbf05676d68e8b89987521ed121Alan Viverette private final OnSeekBarChangeListener mProxySeekBarListener = new OnSeekBarChangeListener() { 1013550a32ca9044fbf05676d68e8b89987521ed121Alan Viverette @Override 1023550a32ca9044fbf05676d68e8b89987521ed121Alan Viverette public void onStopTrackingTouch(SeekBar seekBar) { 1033550a32ca9044fbf05676d68e8b89987521ed121Alan Viverette if (mOnSeekBarChangeListener != null) { 1043550a32ca9044fbf05676d68e8b89987521ed121Alan Viverette mOnSeekBarChangeListener.onStopTrackingTouch(seekBar); 1053550a32ca9044fbf05676d68e8b89987521ed121Alan Viverette } 1063550a32ca9044fbf05676d68e8b89987521ed121Alan Viverette } 1073550a32ca9044fbf05676d68e8b89987521ed121Alan Viverette 1083550a32ca9044fbf05676d68e8b89987521ed121Alan Viverette @Override 1093550a32ca9044fbf05676d68e8b89987521ed121Alan Viverette public void onStartTrackingTouch(SeekBar seekBar) { 1103550a32ca9044fbf05676d68e8b89987521ed121Alan Viverette if (mOnSeekBarChangeListener != null) { 1113550a32ca9044fbf05676d68e8b89987521ed121Alan Viverette mOnSeekBarChangeListener.onStartTrackingTouch(seekBar); 1123550a32ca9044fbf05676d68e8b89987521ed121Alan Viverette } 1133550a32ca9044fbf05676d68e8b89987521ed121Alan Viverette } 1143550a32ca9044fbf05676d68e8b89987521ed121Alan Viverette 1153550a32ca9044fbf05676d68e8b89987521ed121Alan Viverette @Override 1163550a32ca9044fbf05676d68e8b89987521ed121Alan Viverette public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { 1173550a32ca9044fbf05676d68e8b89987521ed121Alan Viverette if (mOnSeekBarChangeListener != null) { 1183550a32ca9044fbf05676d68e8b89987521ed121Alan Viverette mOnSeekBarChangeListener.onProgressChanged(seekBar, progress, fromUser); 1193550a32ca9044fbf05676d68e8b89987521ed121Alan Viverette sendClickEventForAccessibility(progress); 1203550a32ca9044fbf05676d68e8b89987521ed121Alan Viverette } 1213550a32ca9044fbf05676d68e8b89987521ed121Alan Viverette } 1223550a32ca9044fbf05676d68e8b89987521ed121Alan Viverette }; 1233550a32ca9044fbf05676d68e8b89987521ed121Alan Viverette 1242030f45f8f411cf2907ad5929feed3882c050a73Noah Wang private class LabeledSeekBarExploreByTouchHelper extends ExploreByTouchHelper { 1252030f45f8f411cf2907ad5929feed3882c050a73Noah Wang 12635a95617fd467b823a6c1c66f0f5d9651752f2efNoah Wang private boolean mIsLayoutRtl; 12735a95617fd467b823a6c1c66f0f5d9651752f2efNoah Wang 1283550a32ca9044fbf05676d68e8b89987521ed121Alan Viverette public LabeledSeekBarExploreByTouchHelper(LabeledSeekBar forView) { 1292030f45f8f411cf2907ad5929feed3882c050a73Noah Wang super(forView); 13035a95617fd467b823a6c1c66f0f5d9651752f2efNoah Wang mIsLayoutRtl = forView.getResources().getConfiguration() 13135a95617fd467b823a6c1c66f0f5d9651752f2efNoah Wang .getLayoutDirection() == View.LAYOUT_DIRECTION_RTL; 1322030f45f8f411cf2907ad5929feed3882c050a73Noah Wang } 1332030f45f8f411cf2907ad5929feed3882c050a73Noah Wang 1342030f45f8f411cf2907ad5929feed3882c050a73Noah Wang @Override 1352030f45f8f411cf2907ad5929feed3882c050a73Noah Wang protected int getVirtualViewAt(float x, float y) { 1362030f45f8f411cf2907ad5929feed3882c050a73Noah Wang return getVirtualViewIdIndexFromX(x); 1372030f45f8f411cf2907ad5929feed3882c050a73Noah Wang } 1382030f45f8f411cf2907ad5929feed3882c050a73Noah Wang 1392030f45f8f411cf2907ad5929feed3882c050a73Noah Wang @Override 1402030f45f8f411cf2907ad5929feed3882c050a73Noah Wang protected void getVisibleVirtualViews(List<Integer> list) { 1412030f45f8f411cf2907ad5929feed3882c050a73Noah Wang for (int i = 0, c = LabeledSeekBar.this.getMax(); i <= c; ++i) { 1422030f45f8f411cf2907ad5929feed3882c050a73Noah Wang list.add(i); 1432030f45f8f411cf2907ad5929feed3882c050a73Noah Wang } 1442030f45f8f411cf2907ad5929feed3882c050a73Noah Wang } 1452030f45f8f411cf2907ad5929feed3882c050a73Noah Wang 1462030f45f8f411cf2907ad5929feed3882c050a73Noah Wang @Override 1472030f45f8f411cf2907ad5929feed3882c050a73Noah Wang protected boolean onPerformActionForVirtualView(int virtualViewId, int action, 1482030f45f8f411cf2907ad5929feed3882c050a73Noah Wang Bundle arguments) { 1492030f45f8f411cf2907ad5929feed3882c050a73Noah Wang if (virtualViewId == ExploreByTouchHelper.HOST_ID) { 1502030f45f8f411cf2907ad5929feed3882c050a73Noah Wang // Do nothing 1512030f45f8f411cf2907ad5929feed3882c050a73Noah Wang return false; 1522030f45f8f411cf2907ad5929feed3882c050a73Noah Wang } 1532030f45f8f411cf2907ad5929feed3882c050a73Noah Wang 1542030f45f8f411cf2907ad5929feed3882c050a73Noah Wang switch (action) { 1552030f45f8f411cf2907ad5929feed3882c050a73Noah Wang case AccessibilityNodeInfoCompat.ACTION_CLICK: 1562030f45f8f411cf2907ad5929feed3882c050a73Noah Wang LabeledSeekBar.this.setProgress(virtualViewId); 1572030f45f8f411cf2907ad5929feed3882c050a73Noah Wang sendEventForVirtualView(virtualViewId, AccessibilityEvent.TYPE_VIEW_CLICKED); 1582030f45f8f411cf2907ad5929feed3882c050a73Noah Wang return true; 1592030f45f8f411cf2907ad5929feed3882c050a73Noah Wang default: 1602030f45f8f411cf2907ad5929feed3882c050a73Noah Wang return false; 1612030f45f8f411cf2907ad5929feed3882c050a73Noah Wang } 1622030f45f8f411cf2907ad5929feed3882c050a73Noah Wang } 1632030f45f8f411cf2907ad5929feed3882c050a73Noah Wang 1642030f45f8f411cf2907ad5929feed3882c050a73Noah Wang @Override 1652030f45f8f411cf2907ad5929feed3882c050a73Noah Wang protected void onPopulateNodeForVirtualView( 1662030f45f8f411cf2907ad5929feed3882c050a73Noah Wang int virtualViewId, AccessibilityNodeInfoCompat node) { 1672030f45f8f411cf2907ad5929feed3882c050a73Noah Wang node.setClassName(RadioButton.class.getName()); 1682030f45f8f411cf2907ad5929feed3882c050a73Noah Wang node.setBoundsInParent(getBoundsInParentFromVirtualViewId(virtualViewId)); 1692030f45f8f411cf2907ad5929feed3882c050a73Noah Wang node.addAction(AccessibilityNodeInfoCompat.ACTION_CLICK); 1702030f45f8f411cf2907ad5929feed3882c050a73Noah Wang node.setContentDescription(mLabels[virtualViewId]); 1712030f45f8f411cf2907ad5929feed3882c050a73Noah Wang node.setClickable(true); 1722030f45f8f411cf2907ad5929feed3882c050a73Noah Wang node.setCheckable(true); 1732030f45f8f411cf2907ad5929feed3882c050a73Noah Wang node.setChecked(virtualViewId == LabeledSeekBar.this.getProgress()); 1742030f45f8f411cf2907ad5929feed3882c050a73Noah Wang } 1752030f45f8f411cf2907ad5929feed3882c050a73Noah Wang 1762030f45f8f411cf2907ad5929feed3882c050a73Noah Wang @Override 1772030f45f8f411cf2907ad5929feed3882c050a73Noah Wang protected void onPopulateEventForVirtualView(int virtualViewId, AccessibilityEvent event) { 1782030f45f8f411cf2907ad5929feed3882c050a73Noah Wang event.setClassName(RadioButton.class.getName()); 1792030f45f8f411cf2907ad5929feed3882c050a73Noah Wang event.setContentDescription(mLabels[virtualViewId]); 1802030f45f8f411cf2907ad5929feed3882c050a73Noah Wang event.setChecked(virtualViewId == LabeledSeekBar.this.getProgress()); 1812030f45f8f411cf2907ad5929feed3882c050a73Noah Wang } 1822030f45f8f411cf2907ad5929feed3882c050a73Noah Wang 1832030f45f8f411cf2907ad5929feed3882c050a73Noah Wang @Override 1842030f45f8f411cf2907ad5929feed3882c050a73Noah Wang protected void onPopulateNodeForHost(AccessibilityNodeInfoCompat node) { 1852030f45f8f411cf2907ad5929feed3882c050a73Noah Wang node.setClassName(RadioGroup.class.getName()); 1862030f45f8f411cf2907ad5929feed3882c050a73Noah Wang } 1872030f45f8f411cf2907ad5929feed3882c050a73Noah Wang 1882030f45f8f411cf2907ad5929feed3882c050a73Noah Wang @Override 1892030f45f8f411cf2907ad5929feed3882c050a73Noah Wang protected void onPopulateEventForHost(AccessibilityEvent event) { 1902030f45f8f411cf2907ad5929feed3882c050a73Noah Wang event.setClassName(RadioGroup.class.getName()); 1912030f45f8f411cf2907ad5929feed3882c050a73Noah Wang } 1922030f45f8f411cf2907ad5929feed3882c050a73Noah Wang 1932030f45f8f411cf2907ad5929feed3882c050a73Noah Wang private int getHalfVirtualViewWidth() { 1942030f45f8f411cf2907ad5929feed3882c050a73Noah Wang final int width = LabeledSeekBar.this.getWidth(); 1952030f45f8f411cf2907ad5929feed3882c050a73Noah Wang final int barWidth = width - LabeledSeekBar.this.getPaddingStart() 1962030f45f8f411cf2907ad5929feed3882c050a73Noah Wang - LabeledSeekBar.this.getPaddingEnd(); 1972030f45f8f411cf2907ad5929feed3882c050a73Noah Wang return Math.max(0, barWidth / (LabeledSeekBar.this.getMax() * 2)); 1982030f45f8f411cf2907ad5929feed3882c050a73Noah Wang } 1992030f45f8f411cf2907ad5929feed3882c050a73Noah Wang 2002030f45f8f411cf2907ad5929feed3882c050a73Noah Wang private int getVirtualViewIdIndexFromX(float x) { 20135a95617fd467b823a6c1c66f0f5d9651752f2efNoah Wang int posBase = Math.max(0, 2022030f45f8f411cf2907ad5929feed3882c050a73Noah Wang ((int) x - LabeledSeekBar.this.getPaddingStart()) / getHalfVirtualViewWidth()); 20335a95617fd467b823a6c1c66f0f5d9651752f2efNoah Wang posBase = (posBase + 1) / 2; 204b605a4e2013337774a4e9b04eca5bf388c437234Phil Weaver posBase = Math.min(posBase, LabeledSeekBar.this.getMax()); 20535a95617fd467b823a6c1c66f0f5d9651752f2efNoah Wang return mIsLayoutRtl ? LabeledSeekBar.this.getMax() - posBase : posBase; 2062030f45f8f411cf2907ad5929feed3882c050a73Noah Wang } 2072030f45f8f411cf2907ad5929feed3882c050a73Noah Wang 2082030f45f8f411cf2907ad5929feed3882c050a73Noah Wang private Rect getBoundsInParentFromVirtualViewId(int virtualViewId) { 20935a95617fd467b823a6c1c66f0f5d9651752f2efNoah Wang final int updatedVirtualViewId = mIsLayoutRtl 21035a95617fd467b823a6c1c66f0f5d9651752f2efNoah Wang ? LabeledSeekBar.this.getMax() - virtualViewId : virtualViewId; 21135a95617fd467b823a6c1c66f0f5d9651752f2efNoah Wang int left = (updatedVirtualViewId * 2 - 1) * getHalfVirtualViewWidth() 2122030f45f8f411cf2907ad5929feed3882c050a73Noah Wang + LabeledSeekBar.this.getPaddingStart(); 21335a95617fd467b823a6c1c66f0f5d9651752f2efNoah Wang int right = (updatedVirtualViewId * 2 + 1) * getHalfVirtualViewWidth() 2142030f45f8f411cf2907ad5929feed3882c050a73Noah Wang + LabeledSeekBar.this.getPaddingStart(); 2152030f45f8f411cf2907ad5929feed3882c050a73Noah Wang 2162030f45f8f411cf2907ad5929feed3882c050a73Noah Wang // Edge case 21735a95617fd467b823a6c1c66f0f5d9651752f2efNoah Wang left = updatedVirtualViewId == 0 ? 0 : left; 21835a95617fd467b823a6c1c66f0f5d9651752f2efNoah Wang right = updatedVirtualViewId == LabeledSeekBar.this.getMax() 2192030f45f8f411cf2907ad5929feed3882c050a73Noah Wang ? LabeledSeekBar.this.getWidth() : right; 2202030f45f8f411cf2907ad5929feed3882c050a73Noah Wang 2212030f45f8f411cf2907ad5929feed3882c050a73Noah Wang final Rect r = new Rect(); 2222030f45f8f411cf2907ad5929feed3882c050a73Noah Wang r.set(left, 0, right, LabeledSeekBar.this.getHeight()); 2232030f45f8f411cf2907ad5929feed3882c050a73Noah Wang return r; 2242030f45f8f411cf2907ad5929feed3882c050a73Noah Wang } 2252030f45f8f411cf2907ad5929feed3882c050a73Noah Wang } 2262030f45f8f411cf2907ad5929feed3882c050a73Noah Wang} 227