본문 바로가기
[Developer]/Android

TalkBack

by 해피빈이 2010. 2. 8.

class SpeechRule, SpeechRuleProcessor

================================

 

    private static final HashMap<String, Integer> sEventTypeNameToValueMap = new HashMap<String, Integer>();
    static {
        sEventTypeNameToValueMap.put("TYPE_VIEW_CLICKED", 1);
        sEventTypeNameToValueMap.put("TYPE_VIEW_LONG_CLICKED", 2);
        sEventTypeNameToValueMap.put("TYPE_VIEW_SELECTED", 4);
        sEventTypeNameToValueMap.put("TYPE_VIEW_FOCUSED", 8);
        sEventTypeNameToValueMap.put("TYPE_VIEW_TEXT_CHANGED", 16);
        sEventTypeNameToValueMap.put("TYPE_WINDOW_STATE_CHANGED", 32);
        sEventTypeNameToValueMap.put("TYPE_NOTIFICATION_STATE_CHANGED", 64);
    }

sEventTypeNameToValueMap이라는 HashMap 선언 후, 이 Map에 event type name에 따른 각 번호들을 매핑시키는 과정이다.

    private static final HashMap<String, Integer> sQueueModeNameToQueueModeMap = new HashMap<String, Integer>();
    static {
        sQueueModeNameToQueueModeMap.put("QUEUE", 1);
        sQueueModeNameToQueueModeMap.put("INTERRUPT", 2);
        sQueueModeNameToQueueModeMap.put("COMPUTE_FROM_EVENT_CONTEXT", 3);
    }

sQueueModeNameToQueueModeMap이라는 HashMap 선언 후, 이 Map에 queue mode name에 따른 각 번호들을 매핑시키는 과정이다.

    private SpeechRule(Node node, int ruleIndex) {
        Filter filter = null;
        Formatter formatter = null;

        // avoid call to Document#getNodesByTagName, it traverses the entire
        // document
        NodeList children = node.getChildNodes();
        for (int i = 0, count = children.getLength(); i <count; i++) {
            Node child = children.item(i);
            if (child.getNodeType() != Node.ELEMENT_NODE) {
                continue;
            }
            String nodeName = getUnqualifiedNodeName(child);
            if (NODE_NAME_METADATA.equalsIgnoreCase(nodeName)) {

                pupulateMetadata(child);

            } else if (NODE_NAME_FILTER.equals(nodeName)) {

                filter = createFilter(child);
            } else if (NODE_NAME_FORMATTER.equals(nodeName)) {
                formatter = createFormatter(child);
            }
        }
        mFilter = filter;
        mFormatter = formatter;

        mRuleIndex = ruleIndex;
    }



private으로 선언된 내부 생성자이다. 생성자이기 때문에 초기화 과정을 처음에 거친다.
filter와 formatter에 대하여 null로 초기화를 하고, NodeList라는 형태로 children을 선언하여 Node item을 추가한다. nodeName안에는 child를 이용하여 NodeName을 얻어와서 저장한다. 그리고 그것을 NODE_NAME_METADATA, NODE_NAME_FILTER, NODE_NAME_FORMATTER 세 종류에 따라서 비교를 한 뒤에 nodeName과 일치하는 것에 create를 해 준다. 그리고 그것을 member변수에 저장해서 놓는 것이 생성자가 할 역할이 된다.

    public boolean apply(AccessibilityEvent event, Utterance utterance) {        // no filter matches all events        // no formatter drops the event on the floor        boolean matched = (mFilter == null || mFilter.accept(event));        boolean hasFormatter = (mFormatter != null);        if (matched) {            utterance.getMetadata().putAll(mMetadata);            if (hasFormatter) {                mFormatter.format(event, utterance);            }        }        return matched;    }

AccessibilityEvent를 적용한다. 만약 event가 filter에 의해 accept된다면, Formatter는 정형화된 Utterance로 채워지는데 사용된다. event가 accept되었는지 여부에 대한 boolean값을 리턴한다.

    private void pupulateMetadata(Node node) {        NodeList metadata = node.getChildNodes();        for (int i = 0, count = metadata.getLength(); i <count; i++) {            Node child = metadata.item(i);            if (child.getNodeType() != Node.ELEMENT_NODE) {                continue;            }            String unqualifiedName = getUnqualifiedNodeName(child);            String textContent = getTextContent(child);            Object parsedValue = null;            if (PROPERTY_QUEUING.equals(unqualifiedName)) {                parsedValue = sQueueModeNameToQueueModeMap.get(textContent);            } else {                parsedValue = parsePropertyValue(unqualifiedName, textContent);            }            mMetadata.put(unqualifiedName, parsedValue);        }    }

Utterance가 정형화 된 이 규칙에 의해 발표되어져야만 한다고 결정된 메타데이타를 채우는데 사용되는 mathod이다. 입력받은 node에 대해서 ChildNode를 얻어오고 그것을 UnqualifiedNodeName이 있는지 검사한 후에 있다면 그것을 parsedValue에 Object Type으로 저장 후, 그것을 다시 mMetadata에 key-value형식으로 저장하는 로직을 사용하는 것이다.

    static Object parsePropertyValue(String name, String value) {        if (isIntegerProperty(name)) {            try {                return Integer.parseInt(value);            } catch (NumberFormatException nfe) {                Log.w(LOG_TAG, "Property: '" + name + "' not interger. Ignoring!");                return null;            }        } else if (isFloatProperty(name)) {            try {                return Float.parseFloat(value);            } catch (NumberFormatException nfe) {                Log.w(LOG_TAG, "Property: '" + name + "' not float. Ignoring!");                return null;            }        } else if (isBooleanProperty(name)) {            return Boolean.parseBoolean(value);        } else if (isStringProperty(name)) {            return value;        } else {            throw new IllegalArgumentException("Unknown property: " + name);        }    }

받아들인 name에 대해 value값이 IntegerProperty인지, 아니면 FloatProperty인지, 아니면 BooleanProperty인지, 아니면 StringProperty인지 검사를 해서 해당되는 것으로 parse한 뒤에 return해주는 method이다. Object형으로 리턴하지만, 쓰일 때는 그것을 캐스트해서 쓰면 된다.

    static boolean isIntegerProperty(String propertyName) {        return (PROPERTY_EVENT_TYPE.equals(propertyName) ||            PROPERTY_ITEM_COUNT.equals(propertyName) ||            PROPERTY_CURRENT_ITEM_INDEX.equals(propertyName) ||            PROPERTY_FROM_INDEX.equals(propertyName) ||            PROPERTY_ADDED_COUNT.equals(propertyName) ||            PROPERTY_REMOVED_COUNT.equals(propertyName) ||            PROPERTY_QUEUING.equals(propertyName));    }    static boolean isFloatProperty(String propertyName) {        return PROPERTY_EVENT_TIME.equals(propertyName);    }    static boolean isStringProperty(String propertyName) {        return (PROPERTY_PACKAGE_NAME.equals(propertyName) ||                PROPERTY_CLASS_NAME.equals(propertyName) ||                PROPERTY_TEXT.equals(propertyName) ||                PROPERTY_BEFORE_TEXT.equals(propertyName) ||                PROPERTY_CONTENT_DESCRIPTION.equals(propertyName));    }    static boolean isBooleanProperty(String propertyName) {        return (PROPERTY_CHECKED.equals(propertyName) ||                PROPERTY_ENABLED.equals(propertyName) ||                PROPERTY_FULL_SCREEN.equals(propertyName) ||                PROPERTY_PASSWORD.equals(propertyName));    }

Property가 어떠한 type인지 검사하기 위해서 정의해 놓은 static method이다. 해당되는 것과 일치하면 true를, 아니면 false를 리턴하여 아무것도 해당되지 않으면 null을 return하게 된다. 위에서 풀어놓은 parsePropertyValue에 쓰이게 된다.

    private Filter createFilter(Node node) {        NodeList children = node.getChildNodes();        // do we have a custom filter        for (int i = 0, count = children.getLength(); i <count; i++) {            Node child = children.item(i);            if (child.getNodeType() != Node.ELEMENT_NODE) {                continue;            }            String nodeName = getUnqualifiedNodeName(child);            if (NODE_NAME_CUSTOM.equals(nodeName)) {                return createNewInstance(getTextContent(child), Filter.class);            }        }        return new DefaultFilter(node);    }    private Formatter createFormatter(Node node) {        NodeList children = node.getChildNodes();        for (int i = 0, count = children.getLength(); i <count; i++) {            Node child = children.item(i);            if (child.getNodeType() != Node.ELEMENT_NODE) {                continue;            }            String nodeName = getUnqualifiedNodeName(child);            if (NODE_NAME_CUSTOM.equals(nodeName)) {                return createNewInstance(getTextContent(child), Formatter.class);            }        }        return new DefaultFormatter(node);    }

DOM node로 주어진 Filter를 만든다.DOM node로 주어진 Formatter를 만든다.둘 다 반환하는 것은 새로이 method이름에 매칭되는 객체를 생성해서 반환하게 된다.

    private <T> T createNewInstance(String className, Class<T> expectedClass) {        try {            Class<T> clazz = (Class<T>) sContext.getClassLoader().loadClass(className);            return clazz.newInstance();        } catch (ClassNotFoundException cnfe) {            Log.e(LOG_TAG, "Rule: #" + mRuleIndex + ". Could not load class: '" + className                    + "'. Possibly a typo or the class is not in the TalkBack package");        } catch (InstantiationException ie) {            Log.e(LOG_TAG, "Rule: #" + mRuleIndex + ". Could not instantiate class: '" + className                    + "'");        } catch (IllegalAccessException iae) {            Log.e(LOG_TAG, "Rule: #" + mRuleIndex + ". Could not instantiate class: '" + className                    + "'. Possibly you do not have a public no arguments constructor.");        } catch (ClassCastException cce) {            Log.e(LOG_TAG, "Rule: #" + mRuleIndex + ". Could not instantiate class: '" + className                    + "'. The class is not instanceof: '" + expectedClass + "'.");        }        return null;    }

className과 the expectedClass에 반드시 속하도록 새로운 instance를 만드는 역할을 한다.

    public static ArrayList<SpeechRule> createSpeechRules(Document document, Context context) {        ArrayList<SpeechRule> speechRules = new ArrayList<SpeechRule>();        if (document != null) {            sContext = context;            NodeList children = document.getDocumentElement().getChildNodes();            for (int i = 0, count = children.getLength(); i <count; i++) {                Node child = children.item(i);                if (child.getNodeType() == Node.ELEMENT_NODE) {                    speechRules.add(new SpeechRule(child, i));                }            }            Log.d(LOG_TAG, speechRules.size() + " speech rules loaded");        }        return speechRules;    }

speechstrategy.xml에 의해 표현되는 DOM으로부터 모든 Speech rule을 만드는 Factory Method이다. 문서가 잘 작성되어 있고, 클라이언트에 의해 책임을 진다는 전제하에 있기 때문에 확인하는 단계는 거치지 않는다.

    static String getLocalizedTextContent(Node node) {        String textContent = getTextContent(node);        return getStringResource(textContent);    }    static String getStringResource(String resourceIdentifier) {        if (resourceIdentifier.startsWith("@")) {            int id = sContext.getResources().getIdentifier(resourceIdentifier.substring(1), null,                    null);            return sContext.getString(id);        }        return resourceIdentifier;    }    private static String getTextContent(Node node) {        StringBuilder builder = sTempBuilder;        getTextContentRecursive(node, builder);        String text = builder.toString();        builder.delete(0, builder.length());        return text;    }    private static void getTextContentRecursive(Node node, StringBuilder builder) {        NodeList children = node.getChildNodes();        for (int i = 0, count = children.getLength(); i <count; i++) {            Node child = children.item(i);            if (child.getNodeType() == Node.TEXT_NODE) {                builder.append(child.getNodeValue());            }            getTextContentRecursive(child, builder);        }    }

위 네개의 메소드는 연동이 된다.node의 text contents를 localized하여 return한다.return할 때 아래의 method를 이용한다. getStringResource에서는 Identifier가 @로 시작하는지 체크를 하여 resource로 링크되는 값을 찾아와서 리턴해주거나, 그런 문자가 아니라면 그냥 리턴해준다.맨 아래에 있는 메소드는 TextContent를 Recursive를 돈다. 그렇게 돌아서 나온 결과에 대해서 text가 구성되게 된다.

    private static String getUnqualifiedNodeName(Node node) {        String nodeName = node.getNodeName();        int colonIndex = nodeName.indexOf(COLON);        if (colonIndex >-1) {            nodeName = nodeName.substring(colonIndex + 1);        }        return nodeName;    }

prefix를 제거하고 unqualified node name을 반환한다.

    static StringBuilder getEventText(AccessibilityEvent event) {        StringBuilder aggregator = new StringBuilder();        for (CharSequence text : event.getText()) {            aggregator.append(text);            aggregator.append(SPACE);        }        if (aggregator.length() >0) {            aggregator.deleteCharAt(aggregator.length() - 1);        }        return aggregator;    }

space를 delimeter로 이용하여 aggregator를 생성하는 method이다. event에서 text를 얻어와서 가공하게 된다.

------ SpeechRuleProcessor ----------------

speechstrategy.xml 파일로부터 XML을 읽어와서 parsing하고 처리할 수 있도록 돕는 class이다. 이것을 이용하여 가공한다. 가공된 것을 가지고 다른 클래스에서 활용하게 된다. 구체적인 code는 생략.아래는 speechstrategy.xml의 일부이다.

....  <ss:rule>    <ss:filter>      <ss:eventType>TYPE_WINDOW_STATE_CHANGED</ss:eventType>      <ss:className>com.google.android.voicesearch.RecognitionActivity</ss:className>    </ss:filter>  </ss:rule>  <!-- TYPE_VIEW_CLICKED -->  <!-- CompoundButton - select -->  <ss:rule>    <ss:filter>      <ss:eventType>TYPE_VIEW_CLICKED</ss:eventType>      <ss:className>android.widget.CompoundButton</ss:className>      <ss:checked>true</ss:checked>    </ss:filter>    <ss:formatter>      <ss:template>@com.google.android.marvin.talkback:string/template_compound_button_selected</ss:template>      <ss:property>text</ss:property>    </ss:formatter>  </ss:rule>  <!-- CompoundButton - unselect -->  <ss:rule>    <ss:filter>      <ss:eventType>TYPE_VIEW_CLICKED</ss:eventType>      <ss:className>android.widget.CompoundButton</ss:className>    </ss:filter>    <ss:formatter>      <ss:template>@com.google.android.marvin.talkback:string/template_compound_button_not_selected</ss:template>      <ss:property>text</ss:property>    </ss:formatter>  </ss:rule>...

NotificationCache, NotificationType(enum)

=================================

 

생성자.

    NotificationCache() {        /* do nothing */    }

아무일도 하지 않는다.-.-; 상속조차 받지 않았으니. 그냥 있는것같다.

    public boolean addNotification(NotificationType type, String notification) {        List<String> notifications = mTypeToMessageMap.get(type);        if (notifications == null) {            notifications = new ArrayList<String>();            mTypeToMessageMap.put(type, notifications);        }        return notifications.add(notification);    }

주어진 NotificationType에 대한 notification을 추가한다. 이것을 사전에 정의되어 있는 mTypeToMessageMap에서 type에 대한 value(notification)를 얻어오고, 이것을 List로 선언되어 있는 notifications에 추가하는 구조이다. 이것이 정상적으로 추가되면 true를 반환하게 되어있다. 만약 type이 사전에 Map상에 정의되어 있지 않은 type이라면 입력받은 type과 notification을 쌍으로 이루어서 Map에 추가를 한 뒤에 notifications에 추가를 한다.

    public boolean removeNotification(NotificationType type, String notification) {        return getNotificationsForType(type).remove(notification);    }

주어진 type에 대한 notification을 제거한다.

    public List<String> getNotificationsAll() {        List<String> notifications = new ArrayList<String>();        for (NotificationType type : mTypeToMessageMap.keySet()) {            notifications.addAll(getNotificationsForType(type));        }        return notifications;    }

현재 저장된 모든 notification을 return한다. List type으로 notifications를 만들어서 return한다.

    public void removeNotificationsAll() {        mTypeToMessageMap.clear();    }

모든 notification을 제거한다.

    public String getFormattedForType(NotificationType type) {        StringBuilder formatted = new StringBuilder();        List<String> notifications = getNotificationsForType(type);        for (String notification : notifications) {            formatted.append(getStringForResourceId(type.getValue()));            formatted.append(" ");            formatted.append(notification);            formatted.append(" ");        }        return formatted.toString();    }

파라메터로 주어진 type에 대한 모든 notification을 가져온다.(String type으로 가져옴)

    public String getFormattedAll() {        StringBuilder formatted = new StringBuilder();        for (NotificationType type : mTypeToMessageMap.keySet()) {            formatted.append(getFormattedForType(type));        }        return formatted.toString();    }

표현을 위해 모든 notification을 정형화(format)한다. 조합하기 위해서 StringBuilder로 선언했지만, 결국 return시에는 String으로 return한다.

    public String getFormattedSummary() {        StringBuilder summary = new StringBuilder();        for (Map.Entry<NotificationType, List<String>> entry : mTypeToMessageMap.entrySet()) {            int count = entry.getValue().size();            if (count >0) {                summary.append(count);                summary.append(" ");                summary.append(getStringForResourceId(entry.getKey().getValue()));                if (count >1) {                    summary.append("s");                }                summary.append("\n");            }        }        return summary.toString();    }

notification의 요약을 return한다. 이 때 요약의 형식은 entry의 size와 Value로 구성되어 있다. 그리고 count가 0으로 떨어지기 전까지 이것을 반복한 뒤 끝나면 summary한 것을 String으로 변환하여 return한다.

    private String getStringForResourceId(int resourceId) {        return TalkBackService.asContext().getResources().getString(resourceId);    }

resourceId를 입력받아 String으로 변환하여 return한다.이를테면 strings.xml에 있는 수많은 정의되어 있는 값들에 대해 매칭하여 활용이 가능하게 한다.---------------------일단 NotificationType은 enum이다. 그리고 이것은 Parcelable을 implements 한 것이다.

    private NotificationType(int value) {        mValue = value;    }    public int getValue() {        return mValue;    }

먼저 NotificationType의 생성자는 mValue에 입력받은 인자값을 넣는 것이다.그리고 getValue를 통해서는 현재 있는 mValue의 값을 return받을 수 있다.

    public int describeContents() {        return 0;    }    public void writeToParcel(Parcel parcel, int flags) {        parcel.writeInt(mValue);    }

이것은 Parcelable안에 있는 describeContents()를 상속받아 만든 것이다.또한 writeToParcel 역시 Parcelable안에 있는 것을 상속받아 만든 것이고, mValue를 writeInt한다.

    private static NotificationType getMemberForValue(int value) {        for (NotificationType type : values()) {            if (type.mValue == value) {                return type;            }        }        return null;    }

NotificationType에 대해 입력받은 value와 일치를 하면 type을 return한다.

    public static final Parcelable.Creator<NotificationType> CREATOR = new Parcelable.Creator<NotificationType>() {        public NotificationType createFromParcel(Parcel parcel) {            int value = parcel.readInt();            return getMemberForValue(value);        }        public NotificationType[] newArray(int size) {            return new NotificationType[size];        }    };

Parcelable에 대한 Creator 부분이다.

CustomFormatters

===============

    public static final class AddedTextFormatter implements Formatter {        @Override        public void format(AccessibilityEvent event, Utterance utterance) {            StringBuilder text = SpeechRule.getEventText(event);            int begIndex = event.getFromIndex();            int endIndex = begIndex + event.getAddedCount();            utterance.getText().append(text.subSequence(begIndex, endIndex));        }    }

일단 AddedTextFormatter라는 이름의 nested class이다. 이 안에서 format이라는 method를 선언하여 정의하였다. AccessibilityEvent에서 getFromIndex() 라는 메소드가 처음 문장에 대한 index 값을 얻어온다. 따라서 그것이 beginIndex가 된다. 그리고 getAddedCount()는 추가된 문자의 길이를 반환한다. 따라서 시작문자의 위치와 추가된 문자의 갯수를 얻어오면 그것이 전체 Index길이가 된다. 그것을 위치값으로 하여 SpeechRule에서 얻어온 text에 대해 맞는 문자라 인식하고 utterance에 추가해서 넣으면 된다.

    public static final class RemovedTextFormatter implements Formatter {        private static final String TEXT_REMOVED = "@com.google.android.marvin.talkback:string/value_text_removed";        @Override        public void format(AccessibilityEvent event, Utterance utterance) {            CharSequence beforeText = event.getBeforeText();            int begIndex = event.getFromIndex();            int endIndex = begIndex + event.getRemovedCount();            StringBuilder utteranceText = utterance.getText();            utteranceText.append(beforeText.subSequence(begIndex, endIndex));            utteranceText.append(SPACE);            utteranceText.append(SpeechRule.getStringResource(TEXT_REMOVED));        }    }

간단한 annotation에는 Text가 제거된 utterance를 return한다고 되어있다.앞의 AddedTextFormatter와 동일하지만 뒤에 @com.google.android.marvin.talkback:string/value_text_removed라는 문구를 추가해서 붙이게 된다. 이것이 있으면 제거되는 resource(string)를 참고한다. 찾아봤더니 "deleted"라는 문구가 나온다.

    public static final class ReplacedTextFormatter implements Formatter {        private static final String TEXT_REPLACED = "@com.google.android.marvin.talkback:string/template_text_replaced";        @Override        public void format(AccessibilityEvent event, Utterance utterance) {            StringBuilder utteranceText = utterance.getText();            CharSequence removedText = event.getBeforeText();            int beforeBegIndex = event.getFromIndex();            int beforeEndIndex = beforeBegIndex + event.getRemovedCount();            utteranceText.append(removedText.subSequence(beforeBegIndex, beforeEndIndex));            utteranceText.append(SPACE);            utteranceText.append(SpeechRule.getStringResource(TEXT_REPLACED));            utteranceText.append(SPACE);            StringBuilder addedText = SpeechRule.getEventText(event);            int addedBegIndex = event.getFromIndex();            int addedEndIndex = addedBegIndex + event.getAddedCount();            utteranceText.append(addedText.subSequence(addedBegIndex, addedEndIndex));        }    }

간단한 annotation을 참고하니 Text가 변경된 utterance를 return한다고 되어있다.이것 역시 앞에는 AddedTextFormat과 동일하지만 뒤에 @com.google.android.marvin.talkback:string/template_text_replaced라는 문구를 붙여서 내보내고, 그 뒤에 replace할 문구를 덧붙여서 내보낸다. 그 결과 앞 문장과 뒷 문장 사이에 "replaced with"라는 문구가 삽입되게 된다. 이러한 포맷으로 맞춰주는 역할을 한다고 보면 된다.

    public static final class NotificationFormatter implements Formatter {        // NOT INCLUDED in android.jar (NOT accessible via API)        // com.android.mms.R#stat_notify_sms_failed        private static final int ICON_SMS = 0x7F020036;        // com.android.mms.R#stat_notify_sms_failed        private static final int ICON_SMS_FAILED = 0x7f020035;        // com.android.mms.R#stat_notify_sms_failed        private static final int ICON_PLAY = 0x7F020042;        // com.android.mms.R#stat_notify_sms_failed        private static final int ICON_USB = 0x01080239;        // INCLUDED in android.jar (accessible via API)        private static final int ICON_MISSED_CALL = android.R.drawable.stat_notify_missed_call;        private static final int ICON_MUTE = android.R.drawable.stat_notify_call_mute;        private static final int ICON_CHAT = android.R.drawable.stat_notify_chat;        private static final int ICON_ERROR = android.R.drawable.stat_notify_error;        private static final int ICON_MORE = android.R.drawable.stat_notify_more;        private static final int ICON_SDCARD = android.R.drawable.stat_notify_sdcard;        private static final int ICON_SDCARD_USB = android.R.drawable.stat_notify_sdcard_usb;        private static final int ICON_SYNC = android.R.drawable.stat_notify_sync;        private static final int ICON_SYNC_NOANIM = android.R.drawable.stat_notify_sync_noanim;        private static final int ICON_VOICEMAIL = android.R.drawable.stat_notify_voicemail;        private static final int ICON_PHONE_CALL = android.R.drawable.stat_sys_phone_call;        @Override        public void format(AccessibilityEvent event, Utterance utterance) {            Parcelable parcelable = event.getParcelableData();            if (!(parcelable instanceof Notification)) {                return;            }            // special case since the notification appears several            // times while the phone call goes through different phases            // resulting in multiple announcement of the fact that a phone            // call is in progress which the user is already aware of            int icon = ((Notification) parcelable).icon;            if (icon == ICON_PHONE_CALL) {                return;  // ignore this notification            }            StringBuilder utteranceText = utterance.getText();            // use the event text if such otherwise use the icon            if (!event.getText().isEmpty()) {                utteranceText.append(SpeechRule.getEventText(event));                return;            }            NotificationType type = null;            switch (icon) {                case ICON_SMS:                    type = NotificationType.TEXT_MESSAGE;                    break;                case ICON_SMS_FAILED:                    type = NotificationType.TEXT_MESSAGE_FAILED;                    break;                case ICON_USB:                    type = NotificationType.USB_CONNECTED;                    break;                case ICON_MISSED_CALL:                    type = NotificationType.MISSED_CALL;                    break;                case ICON_MUTE:                    type = NotificationType.MUTE;                    break;                case ICON_CHAT:                    type = NotificationType.CHAT;                    break;                case ICON_ERROR:                    type = NotificationType.ERROR;                    break;                case ICON_MORE:                    type = NotificationType.MORE;                    break;                case ICON_SDCARD:                    type = NotificationType.SDCARD;                    break;                case ICON_SDCARD_USB:                    type = NotificationType.SDCARD_USB;                    break;                case ICON_SYNC:                    type = NotificationType.SYNC;                    break;                case ICON_SYNC_NOANIM:                    type = NotificationType.SYNC_NOANIM;                    break;                case ICON_VOICEMAIL:                    type = NotificationType.VOICEMAIL;                    break;                case ICON_PLAY:                    type = NotificationType.PLAY;                    break;                default:                    type = NotificationType.STATUS_NOTIFICATION;            }            CharSequence typeText = TalkBackService.asContext().getResources().getString(                    type.getValue());            utteranceText.append(SpeechRule.getEventText(event));            utteranceText.append(SPACE);            utteranceText.append(typeText);        }    }

static 변수들이 대다수 선언되어 있어서 조금 복잡해 보이지만, 전부 리소스를 링크하는 역할 뿐이다. Notification에 이러한 리소스를 사용하기 위해서이다.(이 리소스는 전부 android.jar안에 탑재되어 있는 것이므로 별도의 첨부는 필요없다.) 이것 역시 format을 맞춰주기 위해서 뒤에 text를 append해 주는 역할을 한다.CustomFormatters class는 이름 그대로 format을 맞춰주는 class이다. utterance에 대한 보조적 역할만 담당하고 있고, 코드도 대부분 append외에는 없으니 어렵지 않게 정리하면 될 듯 하다.

Filter(Interface), Formatter(Interface), Utterance

======================================

public interface Filter {    boolean accept(AccessibilityEvent event);}

이 인터페이스는 주석에 의하면 다음과 같이 설명이 되어 있다."이 인터페이스는 Filter를 작성하기 위한 contract를 정의합니다.Filter는 AccessibilityEvent를 수락하거나 거부합니다."

public interface Formatter {    public void format(AccessibilityEvent event, Utterance utterance);}

이 인터페이스는 주석에 의하면 다음과 같이 설명이 되어 있다."이 인터페이스는 Formatter를 작성하기 위한 contract를 정의합니다.Formatter는 AccessibilityEvent로부터 정형화된 Utterance를 채웁니다."--- 이상 interface 인 것들만 정리하였다 --Utterance에 대해서 정리를 하면 다음과 같다.

    private Utterance() {       /* do nothing - reducing constructor visibility */     }

디폴트 생성자는 private이다. 아무것도 하지 않으며, 오히려 생성자에 대한 폭을 줄이는데 쓰이고 있다.

    public static Utterance obtain() {        return obtain("");    }

캐시된 인스턴스를 반환하는 경우 이미 이용가능한 것이거나, 새 인스턴스를 사용할 수 있도록 한다.

    public static Utterance obtain(String text) {        synchronized (sPoolLock) {            if (sPool != null) {                Utterance utterance = sPool;                sPool = sPool.mNext;                sPoolSize--;                utterance.mNext = null;                utterance.mIsInPool = false;                return utterance;            }            return new Utterance();        }    }

캐시된 인스턴스를 반환하는 경우 이미 이용가능한 것이거나, 새 인스턴스를 사용할 수 있도록 한다. 그리고 그것을 입력받은 Text로 설정해준다.

    public StringBuilder getText() {        return mText;    }

StringBuilder type의 mText가 위에 선언되어 있는데, 그 변수의 값을 return해주는 getter 메소드이다.

    public HashMap<String, Object> getMetadata() {        return mMetadata;    }

HashMap<String, Object> Type의 mMetadata를 return해 주는 getter 메소드이다.

    public void recycle() {        if (mIsInPool) {            return;        }        clear();        synchronized (sPoolLock) {            if (sPoolSize <= MAX_POOL_SIZE) {                mNext = sPool;                sPool = this;                mIsInPool = true;                sPoolSize++;            }        }    }

인스턴스를 재사용할 수 있게 한다. synchronized하게 sPoolLock을 이용한다. sPoolSize는 하나 더 늘어나게 된다.

    private void clear() {        mText.delete(0, mText.length());        mMetadata.clear();    }

Utterance 내의 mText와 mMetadata를 초기화 시켜주는 역할을 한다.

Overview, TalkBackService

=====================

AndroidManifest.xml 파일을 보면 가장 먼저 구동되는 Application이 어떤 것인지 알 수 있다.

manifest 안에  기본적으로 주고 있는 permission도

android.permission.READ_PHONE_STATE

android.permission.TALKBACK_AS_NOTIFICATION_STATE

이 두 종류가 있다.(참고사항)

 

이제 기본적으로 구동되는 Flow를 살펴보자.

Service 종류의 .TalkBackService를 호출하게 되는데, 이 Service는 TalkBackService.java 파일 안에 기술되어 있다.

당연히 이 안에는 TalkBackService 라는 이름을 가진 class가 정의되어 있고, 이 class는 또한 AccessibilityService를 상속받아서 작성된 것이다.

 

** AccessibilityService에 대하여..

AccessibilityService는 AccessibilityEvent가 발생되었을 때, Background에서 실행이 되고, System으로부터 callback을 받는다. 사용자 인터페이스에 의하여 상태가 변화되었을 때 이벤트가 발생된다. 이를테면 focus가 바뀌거나, 버튼이 클릭되었을 때를 말하는 것이다.

accessibility serivce의 lifecycle은 system에 의해 독점적으로 관리된다. accessibility service의 시작과 끝은 사용자의 조작에 의해 디바이스 셋팅에서 enable하고 disable함으로서 전환이 된다.

System에 bind가 된 뒤에 service는 onServiceConnected()를 호출한다. 이 메소드를 통해 binding setup을 원하는 대로 클라이언트에 의해 재정의할 수 있다. accessibility service는 setServiceInfo(AccessibilityServiceInfo)를 호출함으로써, AccessibilityServiceInfo를 통해 설정이 된다. 당신은 이 메소드를 호출함으로써 언제든지 service configuration을 바꿀 수 있다. 하지만  onServiceConnected()를 재정의하는 것이 좋다.

 

    @Override    public void onCreate() {        super.onCreate();

        mTts = new TextToSpeech(getApplicationContext(), new TextToSpeech.OnInitListener() {            @Override            public void onInit(int status) {                mTts.addEarcon(getString(R.string.earcon_progress),                        "com.google.android.marvin.talkback", R.raw.progress);            }        });

        sContext = this;        mSpeechRuleProcessor = new SpeechRuleProcessor(this, R.raw.speechstrategy);

        mCommandInterfaceBroadcastReceiver = new CommandInterfaceBroadcastReceiver();        registerReceiver(mCommandInterfaceBroadcastReceiver, sCommandInterfaceIntentFilter);

        mInCallWatcher = new InCallWatcher();        TelephonyManager telephonyManager = ((TelephonyManager) TalkBackService.asContext()                .getSystemService(Context.TELEPHONY_SERVICE));        telephonyManager.listen(mInCallWatcher, PhoneStateListener.LISTEN_CALL_STATE);    }

 우선 처음으로 onCreate()를 호출하면, TextToSpeech 객체를 생성하는 데 이때 해당 컨텍스트 내에 onInit()을 활용하여 생성한다. onInit()내에서는 com.google.android.marvin.talkback 패키지안에 있는, R.raw.progress라는 sound raw 파일을 R.string.earcon_progress에 연결시켜 놓았다.(earcon_progress는 [PROGRESS] 이다.)

 sContext는 현재 객체의 흐름을 가지고있고.

mSpeechRuleProcessor라는 이름으로 SpeechRuleProcessor객체가 생성되었다.(이것은 또한 R.raw.speechstrategy에 연결되어있는데, 이것은 speechstrategy.xml 파일과 연결되어 있다 -> 각 이벤트별 행동에 따른 지침을 저장한 파일)

registerReceiver를 통해 com.google.android.marvin.talkback.ACTION_ANNOUNCE_STATUS_SUMMARY_COMMAND를 mCommandInterfaceBroadcastReceiver의 BroadcastReceiver로 등록한다.

mInCallWatcher는 PhoneStateListener를 상속받아 만든 객체로서, Call에 대한 상태값의 변화에 따른 이벤트를 잡기 위해 선언되었다.

 telephonyManager는 연결된 Phone의 종류를 받기 위해 사용된다. 이것을 사용하기위해 객체를 선언하였다. 또한 LISTEN_CALL_STATE상태로 설정한다.(Call state의 변화를 감지하기 위해)

 

    @Override    public void onDestroy() {        super.onDestroy();

        unregisterReceiver(mCommandInterfaceBroadcastReceiver);

        TelephonyManager telephonyManager = ((TelephonyManager) TalkBackService.asContext()                .getSystemService(Context.TELEPHONY_SERVICE));        telephonyManager.listen(mInCallWatcher, PhoneStateListener.LISTEN_NONE);

        mTts.shutdown();    }

onDestroy에서는 딱 세가지로 처리를 나눈다.

1 Receiver를 unregister하고(등록해제)

2 telephonyManager에서 LISTEN_NONE으로 상태를 설정하고(Call state의 변화 감지를 중지하기 위해),

3 mTts를 shutdown시킨다(TextToSpeech 종료).

 

    @Override    public void onServiceConnected() {        AccessibilityServiceInfo info = new AccessibilityServiceInfo();        info.eventTypes = AccessibilityEvent.TYPES_ALL_MASK;        info.feedbackType = AccessibilityServiceInfo.FEEDBACK_SPOKEN;        info.notificationTimeout = 0;        info.flags = AccessibilityServiceInfo.DEFAULT;        setServiceInfo(info);    }

맨위에서 설명했듯이 System에 바인드 될 시점에 Service가 호출하는 녀석이다.이 때에 AccessibilityServiceInfo에 대한 정보를 설정해 준다.

    @Override    public void onAccessibilityEvent(AccessibilityEvent event) {        if (event == null) {            Log.e(LOG_TAG, "Received null accessibility event.");            return;        }        synchronized (mEventQueue) {            if (!mInCallWatcher.mIsInCall) {                // not in call - process all events                enqueueEventLocked(event, true);                sendSpeakMessageLocked(QUEUING_MODE_AUTO_COMPUTE_FROM_EVENT_CONTEXT);             } else {                // in call - cache all notifications without enforcing event queue size                if (event.getEventType() == AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED) {                    enqueueEventLocked(event, false);                       }            }        }        return;    }

이것은 AccessibilityEvent가 발생하면 호출되는 메소드이다. event가 없으면 넘어가고, 그렇지 않고 mEvenetQueue에 event가 존재한다면 그것을 읽어들여서 CallWatcher가 그것을 받아서 판단하면서 Call일 경우와 그렇지 않을 경우와 다르게 동작을 하여 음성으로 서비스를 한다. synchronized로 선언이 되어 있기 때문에 Queue가 두개 이상이 중복되어 함께 처리될 수 없으며 순차적으로 처리를 한다.

    @Override    public void onInterrupt() {        mTts.stop();    }

이것은 Interrupt가 발생했을 때, TTS를 중지한다는 뜻이다.

    private void enqueueEventLocked(AccessibilityEvent event, boolean enforceSize) {        AccessibilityEvent current = clone(event);        ArrayList<AccessibilityEvent> eventQueue = mEventQueue;        int lastIndex = eventQueue.size() - 1;        if (lastIndex >-1) {            AccessibilityEvent last = eventQueue.get(lastIndex);            if (current.getEventType() == last.getEventType()) {                eventQueue.set(lastIndex, current);                return;            }        }        eventQueue.add(current);        if (enforceSize) {            enforceEventQueueSize();        }    }    private void enforceEventQueueSize() {        ArrayList<AccessibilityEvent> eventQueue = mEventQueue;        while (eventQueue.size() >EVENT_QUEUE_MAX_SIZE) {            eventQueue.remove(0);        }    }

Enqueue라는 Event를 발생시키는 메소드이다. mEventQueue를 가져다가 집어넣는 역할을 한다.eventQueue.set(lastIndex, current); <- 이 구문을 통해서 eventQueue에 추가를 하는 것이다.그리고enforceSize값으로 EventQueue size를 조정한다. eventQueue의 size가 EVENT_QUEUE_MAX_SIZE보다 크면 remove과정으로 queue를 하나씩 삭제한다.리브로쿠폰 : W39FLX8S

    public void sendSpeakMessageLocked(int queueMode) {        Handler handler = mSpeechHandler;        handler.removeMessages(MESSAGE_TYPE_SPEAK);        Message message = handler.obtainMessage(MESSAGE_TYPE_SPEAK);        message.arg1 = queueMode;        handler.sendMessageDelayed(message, EVENT_TIMEOUT);    }

이 메소드는 speech handler에 SpeakMessage를 보내는 역할을 한다. 파라메터로 보낸 queueMode에 따라보낸다.

    private void processAndRecycleEvent(AccessibilityEvent event, int queueMode) {        Utterance utterance = Utterance.obtain();        if (mSpeechRuleProcessor.processEvent(event, utterance)) {            HashMap<String, Object> metadata = utterance.getMetadata();            if (metadata.containsKey(Utterance.KEY_METADATA_QUEUING)) {                // speech rules queue mode overrides the default TalkBack behavior                // the case is safe since SpeechRule did the preprocessing                queueMode = (Integer) metadata.get(Utterance.KEY_METADATA_QUEUING);            } else if (queueMode == QUEUING_MODE_AUTO_COMPUTE_FROM_EVENT_CONTEXT) {                // if we are asked to compute the queue mode, we do so                queueMode = (mLastEventType == event.getEventType() ? QUEUING_MODE_INTERRUPT                    : QUEUING_MODE_QUEUE);            }            if (isEarcon(utterance)) {                String earcon = utterance.getText().toString();                // earcons always use QUEUING_MODE_QUEUE                mTts.playEarcon(earcon, QUEUING_MODE_QUEUE, null);            } else {                               mLastEventType = event.getEventType();                cleanUpAndSpeak(utterance, queueMode);            }            utterance.recycle();            event.recycle();        }    }

이 메소드에서는 Utterance라는 객체를 이용하여 활용을 한다. 활용하기 위하여 obtain 메소드를 이용.Utterance 하나를 얻어온다.utterance에서 metadata를 얻어와서 저장한다. 그다음 Utterance.KEY_METADATA_QUEUING에 있는대로("queuing") metadata의 key를 얻어온다. 만약 key가 존재하면 그것을 HashMap의 value를 구해서 queueMode에 Integer로 변환해서 넣는다. 하지만 queuing이라는 KEY가 없는데, queueMode가 QUEUING_MODE_AUTO_COMPUTE_FROM_EVENT_CONTEXT값이라면 eventType을 구해서 queueMode에 넣는다. lastEventType과 getEventType의 값이 같다면 QUEUING_MODE_INTERRUPT를, 다르다면 QUEUING_MODE_QUEUE를 queueMode에 넣는다.utterance의 값을 얻어올 때 값이 isEarcon으로 검사할때 true라면, utterance에서 text값을 얻어온다. 그것을 earcon에 저장한 후 playEarcon으로 TTS를 작동시킨다.하지만 earcon이 아닐 때는 mLastEventType에 eventType을 저장한다. 그리고 cleanUpAndSpeak를 동작시킨다. 추후에 나오지만 TTS에 대해 speak() 메소드를 동작시키는 메소드이다.그리고 마지막에 recycle()함으로써 Back instance를 재사용할 수 있게 한다.

 

    static Context asContext() {        return sContext;    }

TalkBack class는 Context이다. 따라서 편리한 control을 위해 Context를 리턴할 수 있는 메소드도 잇다.(static)

    static void updateNotificationCache(NotificationType type, CharSequence text) {        // if the cache has the notification - remove, otherwise add it        if (!sNotificationCache.removeNotification(type, text.toString())) {            sNotificationCache.addNotification(type, text.toString());        }    }

updateNotificationCache는 기본적으로 remove를 하려고 한다. 성공하면 제거만 실행하고, 실패하면 Notification을 해당 type에 맞게 추가하는 action을 취한다.

    private AccessibilityEvent clone(AccessibilityEvent event) {        AccessibilityEvent clone = AccessibilityEvent.obtain();        clone.setAddedCount(event.getAddedCount());        clone.setBeforeText(event.getBeforeText());        clone.setChecked(event.isChecked());        clone.setClassName(event.getClassName());        clone.setContentDescription(event.getContentDescription());        clone.setCurrentItemIndex(event.getCurrentItemIndex());        clone.setEventTime(event.getEventTime());        clone.setEventType(event.getEventType());        clone.setFromIndex(event.getFromIndex());        clone.setFullScreen(event.isFullScreen());        clone.setItemCount(event.getItemCount());        clone.setPackageName(event.getPackageName());        clone.setParcelableData(event.getParcelableData());        clone.setPassword(event.isPassword());        clone.setRemovedCount(event.getRemovedCount());        clone.getText().clear();        clone.getText().addAll(event.getText());        return clone;    }

clone()은 AccessibilityEvent에 대해서 복제하고, 그 복제한 것을 return한다.

    private void cleanUpAndSpeak(Utterance utterance, int queueMode) {        String text = cleanUpString(utterance.getText().toString());        mTts.speak(text, queueMode, null);    }

cleanUp이 의미하는 것은 utterance로 얻어온 String에 특수문자나, 일부 등록된 표현들을 제거하기 위해 필요하다. 그 이후에 mTts의 값을 speak()로 읽어준다.

    private boolean isEarcon(Utterance utterance) {        StringBuilder text = utterance.getText();        if (text.length() >0) {            return (text.charAt(0) == OPEN_SQUARE_BRACKET &&                    text.charAt(text.length() - 1) == CLOSE_SQUARE_BRACKET);        } else {            return false;        }    }

Earcon인지 판별하는 메소드. 원리는 간단하다. utterance에서 text를 얻어온다음, 그 text를 맨 앞과 뒤를 잘라서 [와 ]으로 되어있다면 true를 return하고, 만약 그렇지 않다면 false를 return하는 것이다.

    Handler mSpeechHandler = new Handler() {        @Override        public void handleMessage(Message message) {            ArrayList<AccessibilityEvent> eventQueue = mEventQueue;            ArrayList<AccessibilityEvent> events = mTempEventList;            synchronized (eventQueue) {                // pick the last events while holding a lock                events.addAll(eventQueue);                eventQueue.clear();            }            // now process all events and clean up            Iterator<AccessibilityEvent> iterator = events.iterator();            while (iterator.hasNext()) {                AccessibilityEvent event = iterator.next();                int queueMode = message.arg1;                processAndRecycleEvent(event, queueMode);                iterator.remove();            }        }    };

mSpeechHandler를 Handler 객체로 선언한다. ArrayList로 eventQueue와 events 두개를 선언. eventQueue에 대해서 synchronized하게 events에 add하고 eventQueue를 clear하는 과정을 처리한다.또한 events를 iterator화를 시키고, 그 iterator를 loop돌면서 processAndRecycleEvent()메소드를 실행한다. iterator가 다 소진될 때 까지 말이다.(이것은 그 해당 event에 대한 처리와 함께 재사용할 것에 대해서 설정해주는 메소드이다.)

    class CommandInterfaceBroadcastReceiver extends BroadcastReceiver {        /**         * {@inheritDoc BroadcastReceiver#onReceive(Context, Intent)}         *          * @throws SecurityException if the user does not have         *             com.google.android.marvin.talkback.         *             SEND_INTENT_BROADCAST_COMMANDS_TO_TALKBACK permission.         */        @Override        public void onReceive(Context context, Intent intent) {            verifyCallerPermission(context, SEND_INTENT_BROADCAST_COMMANDS_TO_TALKBACK);            if (intent.getAction().equals(ACTION_ANNOUNCE_STATUS_SUMMARY_COMMAND)) {                Utterance utterance = Utterance.obtain();                utterance.getMetadata().put("temper", "RUDE");                StringBuilder utteranceBuilder = utterance.getText();                utteranceBuilder.append(getResources().getString(                        R.string.value_notification_summary));                utteranceBuilder.append(SPACE);                utteranceBuilder.append(sNotificationCache.getFormattedSummary());                cleanUpAndSpeak(utterance, QUEUING_MODE_INTERRUPT);            }            // other intent commands go here ...        }        /**         * Verifies if the context of a caller has a certain permission.         *         * @param context the {@link Context}.         * @param permissionName The permission name.         */        private void verifyCallerPermission(Context context, String permissionName) {            int permissionState = context.checkPermission(permissionName, android.os.Process                    .myPid(), 0);            if (permissionState != PackageManager.PERMISSION_GRANTED) {                String message = "Permission denied - " + permissionName;                Log.e(LOG_TAG, message);                throw new SecurityException(message);            }        }    }

CommandInterfaceBroadcastReceiver는 Nested Class이다이 클래스에서는 첫번째, onReceive()에서 접근 권한에 대한 verify 부분을 담당한다. utterance에 대한 metadata를 얻어와서 temper키에 RUDE라는 값을 넣고 utteranceBuilder를 이용하여 읽어야 할 값에 대하여 settting해 준다.또한 verifyCallerPermission()에서는 Permission을 체크하고 만약 PERMISSION_GRANTED와 일치하지 않을 때는 Permission denied ~~라는 메세지를 뿌려주고, exception을 throw한다.

    class InCallWatcher extends PhoneStateListener {        boolean mIsInCall = false;        @Override        public void onCallStateChanged(int state, String incomingNumber) {            if (state != TelephonyManager.CALL_STATE_IDLE) {                // a call has started, so interrupt all the stuff that                // speaks and stuff scheduled for speaking                mIsInCall = true;                mTts.speak("", QUEUING_MODE_INTERRUPT, null);                mSpeechHandler.removeMessages(MESSAGE_TYPE_SPEAK);            } else {                mIsInCall = false;                // we can speak now so announce all cached events with                // no interruption                synchronized (mSpeechHandler) {                    sendSpeakMessageLocked(QUEUING_MODE_QUEUE);                }            }        }    }

InCallWatcher는 Nested Class이다.이 클래스에서는 onCallStateChanged()메소드를 호출함으로써 시작되는데, 이 때 state가 TelephonyManager.CALL_STATE_IDLE상태인지 체크하고 아니라면 Call 상태이기 때문에 QUEUING_MODE_INTERRUPT로 speak가 진행된다. 그리고 message는 remove되고 종결된다.반대로 state가 TelephonyManager.CALL_STATE_IDLE가 아니라면 mIsInCall(Call인지 아닌지)아닌상태이므로 mSpeechHandler에 대해 synchronized하게 실행을 한다. 즉, 지난 메세지는 cancel하고 진행을 계속 하게 된다.


반응형

댓글1