SentenceLevelAdapter.java revision 86f36003fd4397143bd37938dda029e5707634af
1/*
2 * Copyright (C) 2014 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
17package com.android.inputmethod.latin.spellcheck;
18
19import android.content.res.Resources;
20import android.view.textservice.SentenceSuggestionsInfo;
21import android.view.textservice.SuggestionsInfo;
22import android.view.textservice.TextInfo;
23
24import com.android.inputmethod.compat.TextInfoCompatUtils;
25import com.android.inputmethod.latin.Constants;
26import com.android.inputmethod.latin.settings.SpacingAndPunctuations;
27import com.android.inputmethod.latin.utils.RunInLocale;
28
29import java.util.ArrayList;
30import java.util.Locale;
31
32/**
33 * This code is mostly lifted directly from android.service.textservice.SpellCheckerService in
34 * the framework; maybe that should be protected instead, so that implementers don't have to
35 * rewrite everything for any small change.
36 */
37public class SentenceLevelAdapter {
38    public static final SentenceSuggestionsInfo[] EMPTY_SENTENCE_SUGGESTIONS_INFOS =
39            new SentenceSuggestionsInfo[] {};
40    private static final SuggestionsInfo EMPTY_SUGGESTIONS_INFO = new SuggestionsInfo(0, null);
41    /**
42     * Container for split TextInfo parameters
43     */
44    public static class SentenceWordItem {
45        public final TextInfo mTextInfo;
46        public final int mStart;
47        public final int mLength;
48        public SentenceWordItem(TextInfo ti, int start, int end) {
49            mTextInfo = ti;
50            mStart = start;
51            mLength = end - start;
52        }
53    }
54
55    /**
56     * Container for originally queried TextInfo and parameters
57     */
58    public static class SentenceTextInfoParams {
59        final TextInfo mOriginalTextInfo;
60        final ArrayList<SentenceWordItem> mItems;
61        final int mSize;
62        public SentenceTextInfoParams(TextInfo ti, ArrayList<SentenceWordItem> items) {
63            mOriginalTextInfo = ti;
64            mItems = items;
65            mSize = items.size();
66        }
67    }
68
69    private static class WordIterator {
70        private final SpacingAndPunctuations mSpacingAndPunctuations;
71        public WordIterator(final Resources res, final Locale locale) {
72            final RunInLocale<SpacingAndPunctuations> job
73                    = new RunInLocale<SpacingAndPunctuations>() {
74                @Override
75                protected SpacingAndPunctuations job(final Resources res) {
76                    return new SpacingAndPunctuations(res);
77                }
78            };
79            mSpacingAndPunctuations = job.runInLocale(res, locale);
80        }
81
82        public int getEndOfWord(final CharSequence sequence, int index) {
83            final int length = sequence.length();
84            index = index < 0 ? 0 : Character.offsetByCodePoints(sequence, index, 1);
85            while (index < length) {
86                final int codePoint = Character.codePointAt(sequence, index);
87                if (mSpacingAndPunctuations.isWordSeparator(codePoint)) {
88                    // If it's a period, we want to stop here only if it's followed by another
89                    // word separator. In all other cases we stop here.
90                    if (Constants.CODE_PERIOD == codePoint) {
91                        final int indexOfNextCodePoint =
92                                index + Character.charCount(Constants.CODE_PERIOD);
93                        if (indexOfNextCodePoint < length
94                                && mSpacingAndPunctuations.isWordSeparator(
95                                        Character.codePointAt(sequence, indexOfNextCodePoint))) {
96                            return index;
97                        }
98                    } else {
99                        return index;
100                    }
101                }
102                index += Character.charCount(codePoint);
103            }
104            return index;
105        }
106
107        public int getBeginningOfNextWord(final CharSequence sequence, int index) {
108            final int length = sequence.length();
109            if (index >= length) {
110                return -1;
111            }
112            index = index < 0 ? 0 : Character.offsetByCodePoints(sequence, index, 1);
113            while (index < length) {
114                final int codePoint = Character.codePointAt(sequence, index);
115                if (!mSpacingAndPunctuations.isWordSeparator(codePoint)) {
116                    return index;
117                }
118                index += Character.charCount(codePoint);
119            }
120            return -1;
121        }
122    }
123
124    private final WordIterator mWordIterator;
125    public SentenceLevelAdapter(final Resources res, final Locale locale) {
126        mWordIterator = new WordIterator(res, locale);
127    }
128
129    public SentenceTextInfoParams getSplitWords(TextInfo originalTextInfo) {
130        final WordIterator wordIterator = mWordIterator;
131        final CharSequence originalText =
132                TextInfoCompatUtils.getCharSequenceOrString(originalTextInfo);
133        final int cookie = originalTextInfo.getCookie();
134        final int start = -1;
135        final int end = originalText.length();
136        final ArrayList<SentenceWordItem> wordItems = new ArrayList<SentenceWordItem>();
137        int wordStart = wordIterator.getBeginningOfNextWord(originalText, start);
138        int wordEnd = wordIterator.getEndOfWord(originalText, wordStart);
139        while (wordStart <= end && wordEnd != -1 && wordStart != -1) {
140            if (wordEnd >= start && wordEnd > wordStart) {
141                CharSequence subSequence = originalText.subSequence(wordStart, wordEnd).toString();
142                final TextInfo ti = TextInfoCompatUtils.newInstance(subSequence, 0,
143                        subSequence.length(), cookie, subSequence.hashCode());
144                wordItems.add(new SentenceWordItem(ti, wordStart, wordEnd));
145            }
146            wordStart = wordIterator.getBeginningOfNextWord(originalText, wordEnd);
147            if (wordStart == -1) {
148                break;
149            }
150            wordEnd = wordIterator.getEndOfWord(originalText, wordStart);
151        }
152        return new SentenceTextInfoParams(originalTextInfo, wordItems);
153    }
154
155    public static SentenceSuggestionsInfo reconstructSuggestions(
156            SentenceTextInfoParams originalTextInfoParams, SuggestionsInfo[] results) {
157        if (results == null || results.length == 0) {
158            return null;
159        }
160        if (originalTextInfoParams == null) {
161            return null;
162        }
163        final int originalCookie = originalTextInfoParams.mOriginalTextInfo.getCookie();
164        final int originalSequence =
165                originalTextInfoParams.mOriginalTextInfo.getSequence();
166
167        final int querySize = originalTextInfoParams.mSize;
168        final int[] offsets = new int[querySize];
169        final int[] lengths = new int[querySize];
170        final SuggestionsInfo[] reconstructedSuggestions = new SuggestionsInfo[querySize];
171        for (int i = 0; i < querySize; ++i) {
172            final SentenceWordItem item = originalTextInfoParams.mItems.get(i);
173            SuggestionsInfo result = null;
174            for (int j = 0; j < results.length; ++j) {
175                final SuggestionsInfo cur = results[j];
176                if (cur != null && cur.getSequence() == item.mTextInfo.getSequence()) {
177                    result = cur;
178                    result.setCookieAndSequence(originalCookie, originalSequence);
179                    break;
180                }
181            }
182            offsets[i] = item.mStart;
183            lengths[i] = item.mLength;
184            reconstructedSuggestions[i] = result != null ? result : EMPTY_SUGGESTIONS_INFO;
185        }
186        return new SentenceSuggestionsInfo(reconstructedSuggestions, offsets, lengths);
187    }
188}
189