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.internal.inputmethod;
18
19import android.content.pm.ApplicationInfo;
20import android.content.pm.ResolveInfo;
21import android.content.pm.ServiceInfo;
22import android.test.InstrumentationTestCase;
23import android.test.suitebuilder.annotation.SmallTest;
24import android.view.inputmethod.InputMethodInfo;
25import android.view.inputmethod.InputMethodSubtype;
26import android.view.inputmethod.InputMethodSubtype.InputMethodSubtypeBuilder;
27
28import com.android.internal.inputmethod.InputMethodSubtypeSwitchingController.ControllerImpl;
29import com.android.internal.inputmethod.InputMethodSubtypeSwitchingController.ImeSubtypeListItem;
30
31import java.util.ArrayList;
32import java.util.Arrays;
33import java.util.List;
34
35public class InputMethodSubtypeSwitchingControllerTest extends InstrumentationTestCase {
36    private static final String DUMMY_PACKAGE_NAME = "dummy package name";
37    private static final String DUMMY_IME_LABEL = "dummy ime label";
38    private static final String DUMMY_SETTING_ACTIVITY_NAME = "";
39    private static final boolean DUMMY_IS_AUX_IME = false;
40    private static final boolean DUMMY_FORCE_DEFAULT = false;
41    private static final int DUMMY_IS_DEFAULT_RES_ID = 0;
42    private static final String SYSTEM_LOCALE = "en_US";
43    private static final int NOT_A_SUBTYPE_ID = InputMethodUtils.NOT_A_SUBTYPE_ID;
44
45    private static InputMethodSubtype createDummySubtype(final String locale) {
46        final InputMethodSubtypeBuilder builder = new InputMethodSubtypeBuilder();
47        return builder.setSubtypeNameResId(0)
48                .setSubtypeIconResId(0)
49                .setSubtypeLocale(locale)
50                .setIsAsciiCapable(true)
51                .build();
52    }
53
54    private static void addDummyImeSubtypeListItems(List<ImeSubtypeListItem> items,
55            String imeName, String imeLabel, List<String> subtypeLocales,
56            boolean supportsSwitchingToNextInputMethod) {
57        final ResolveInfo ri = new ResolveInfo();
58        final ServiceInfo si = new ServiceInfo();
59        final ApplicationInfo ai = new ApplicationInfo();
60        ai.packageName = DUMMY_PACKAGE_NAME;
61        ai.enabled = true;
62        si.applicationInfo = ai;
63        si.enabled = true;
64        si.packageName = DUMMY_PACKAGE_NAME;
65        si.name = imeName;
66        si.exported = true;
67        si.nonLocalizedLabel = imeLabel;
68        ri.serviceInfo = si;
69        List<InputMethodSubtype> subtypes = null;
70        if (subtypeLocales != null) {
71            subtypes = new ArrayList<>();
72            for (String subtypeLocale : subtypeLocales) {
73                subtypes.add(createDummySubtype(subtypeLocale));
74            }
75        }
76        final InputMethodInfo imi = new InputMethodInfo(ri, DUMMY_IS_AUX_IME,
77                DUMMY_SETTING_ACTIVITY_NAME, subtypes, DUMMY_IS_DEFAULT_RES_ID,
78                DUMMY_FORCE_DEFAULT, supportsSwitchingToNextInputMethod);
79        if (subtypes == null) {
80            items.add(new ImeSubtypeListItem(imeName, null /* variableName */, imi,
81                    NOT_A_SUBTYPE_ID, null, SYSTEM_LOCALE));
82        } else {
83            for (int i = 0; i < subtypes.size(); ++i) {
84                final String subtypeLocale = subtypeLocales.get(i);
85                items.add(new ImeSubtypeListItem(imeName, subtypeLocale, imi, i, subtypeLocale,
86                        SYSTEM_LOCALE));
87            }
88        }
89    }
90
91    private static ImeSubtypeListItem createDummyItem(String imeName,
92            String subtypeName, String subtypeLocale, int subtypeIndex, String systemLocale) {
93        final ResolveInfo ri = new ResolveInfo();
94        final ServiceInfo si = new ServiceInfo();
95        final ApplicationInfo ai = new ApplicationInfo();
96        ai.packageName = DUMMY_PACKAGE_NAME;
97        ai.enabled = true;
98        si.applicationInfo = ai;
99        si.enabled = true;
100        si.packageName = DUMMY_PACKAGE_NAME;
101        si.name = imeName;
102        si.exported = true;
103        si.nonLocalizedLabel = DUMMY_IME_LABEL;
104        ri.serviceInfo = si;
105        ArrayList<InputMethodSubtype> subtypes = new ArrayList<>();
106        subtypes.add(new InputMethodSubtypeBuilder()
107                .setSubtypeNameResId(0)
108                .setSubtypeIconResId(0)
109                .setSubtypeLocale(subtypeLocale)
110                .setIsAsciiCapable(true)
111                .build());
112        final InputMethodInfo imi = new InputMethodInfo(ri, DUMMY_IS_AUX_IME,
113                DUMMY_SETTING_ACTIVITY_NAME, subtypes, DUMMY_IS_DEFAULT_RES_ID,
114                DUMMY_FORCE_DEFAULT, true /* supportsSwitchingToNextInputMethod */);
115        return new ImeSubtypeListItem(imeName, subtypeName, imi, subtypeIndex, subtypeLocale,
116                systemLocale);
117    }
118
119    private static List<ImeSubtypeListItem> createEnabledImeSubtypes() {
120        final List<ImeSubtypeListItem> items = new ArrayList<>();
121        addDummyImeSubtypeListItems(items, "LatinIme", "LatinIme", Arrays.asList("en_US", "fr"),
122                true /* supportsSwitchingToNextInputMethod*/);
123        addDummyImeSubtypeListItems(items, "switchUnawareLatinIme", "switchUnawareLatinIme",
124                Arrays.asList("en_UK", "hi"),
125                false /* supportsSwitchingToNextInputMethod*/);
126        addDummyImeSubtypeListItems(items, "subtypeUnawareIme", "subtypeUnawareIme", null,
127                false /* supportsSwitchingToNextInputMethod*/);
128        addDummyImeSubtypeListItems(items, "JapaneseIme", "JapaneseIme", Arrays.asList("ja_JP"),
129                true /* supportsSwitchingToNextInputMethod*/);
130        addDummyImeSubtypeListItems(items, "switchUnawareJapaneseIme", "switchUnawareJapaneseIme",
131                Arrays.asList("ja_JP"), false /* supportsSwitchingToNextInputMethod*/);
132        return items;
133    }
134
135    private static List<ImeSubtypeListItem> createDisabledImeSubtypes() {
136        final List<ImeSubtypeListItem> items = new ArrayList<>();
137        addDummyImeSubtypeListItems(items,
138                "UnknownIme", "UnknownIme",
139                Arrays.asList("en_US", "hi"),
140                true /* supportsSwitchingToNextInputMethod*/);
141        addDummyImeSubtypeListItems(items,
142                "UnknownSwitchingUnawareIme", "UnknownSwitchingUnawareIme",
143                Arrays.asList("en_US"),
144                false /* supportsSwitchingToNextInputMethod*/);
145        addDummyImeSubtypeListItems(items, "UnknownSubtypeUnawareIme",
146                "UnknownSubtypeUnawareIme", null,
147                false /* supportsSwitchingToNextInputMethod*/);
148        return items;
149    }
150
151    private void assertNextInputMethod(final ControllerImpl controller,
152            final boolean onlyCurrentIme, final ImeSubtypeListItem currentItem,
153            final ImeSubtypeListItem nextItem, final ImeSubtypeListItem prevItem) {
154        InputMethodSubtype subtype = null;
155        if (currentItem.mSubtypeName != null) {
156            subtype = createDummySubtype(currentItem.mSubtypeName.toString());
157        }
158        final ImeSubtypeListItem nextIme = controller.getNextInputMethod(onlyCurrentIme,
159                currentItem.mImi, subtype, true /* forward */);
160        assertEquals(nextItem, nextIme);
161        final ImeSubtypeListItem prevIme = controller.getNextInputMethod(onlyCurrentIme,
162                currentItem.mImi, subtype, false /* forward */);
163        assertEquals(prevItem, prevIme);
164    }
165
166    private void assertRotationOrder(final ControllerImpl controller,
167            final boolean onlyCurrentIme,
168            final ImeSubtypeListItem... expectedRotationOrderOfImeSubtypeList) {
169        final int N = expectedRotationOrderOfImeSubtypeList.length;
170        for (int i = 0; i < N; i++) {
171            final int currentIndex = i;
172            final int prevIndex = (currentIndex + N - 1) % N;
173            final int nextIndex = (currentIndex + 1) % N;
174            final ImeSubtypeListItem currentItem =
175                    expectedRotationOrderOfImeSubtypeList[currentIndex];
176            final ImeSubtypeListItem nextItem = expectedRotationOrderOfImeSubtypeList[nextIndex];
177            final ImeSubtypeListItem prevItem = expectedRotationOrderOfImeSubtypeList[prevIndex];
178            assertNextInputMethod(controller, onlyCurrentIme, currentItem, nextItem, prevItem);
179        }
180    }
181
182    private void onUserAction(final ControllerImpl controller,
183            final ImeSubtypeListItem subtypeListItem) {
184        InputMethodSubtype subtype = null;
185        if (subtypeListItem.mSubtypeName != null) {
186            subtype = createDummySubtype(subtypeListItem.mSubtypeName.toString());
187        }
188        controller.onUserActionLocked(subtypeListItem.mImi, subtype);
189    }
190
191    @SmallTest
192    public void testControllerImpl() throws Exception {
193        final List<ImeSubtypeListItem> disabledItems = createDisabledImeSubtypes();
194        final ImeSubtypeListItem disabledIme_en_US = disabledItems.get(0);
195        final ImeSubtypeListItem disabledIme_hi = disabledItems.get(1);
196        final ImeSubtypeListItem disabledSwitchingUnawareIme = disabledItems.get(2);
197        final ImeSubtypeListItem disabledSubtypeUnawareIme = disabledItems.get(3);
198
199        final List<ImeSubtypeListItem> enabledItems = createEnabledImeSubtypes();
200        final ImeSubtypeListItem latinIme_en_US = enabledItems.get(0);
201        final ImeSubtypeListItem latinIme_fr = enabledItems.get(1);
202        final ImeSubtypeListItem switchingUnawarelatinIme_en_UK = enabledItems.get(2);
203        final ImeSubtypeListItem switchingUnawarelatinIme_hi = enabledItems.get(3);
204        final ImeSubtypeListItem subtypeUnawareIme = enabledItems.get(4);
205        final ImeSubtypeListItem japaneseIme_ja_JP = enabledItems.get(5);
206        final ImeSubtypeListItem switchUnawareJapaneseIme_ja_JP = enabledItems.get(6);
207
208        final ControllerImpl controller = ControllerImpl.createFrom(
209                null /* currentInstance */, enabledItems);
210
211        // switching-aware loop
212        assertRotationOrder(controller, false /* onlyCurrentIme */,
213                latinIme_en_US, latinIme_fr, japaneseIme_ja_JP);
214
215        // switching-unaware loop
216        assertRotationOrder(controller, false /* onlyCurrentIme */,
217                switchingUnawarelatinIme_en_UK, switchingUnawarelatinIme_hi, subtypeUnawareIme,
218                switchUnawareJapaneseIme_ja_JP);
219
220        // test onlyCurrentIme == true
221        assertRotationOrder(controller, true /* onlyCurrentIme */,
222                latinIme_en_US, latinIme_fr);
223        assertRotationOrder(controller, true /* onlyCurrentIme */,
224                switchingUnawarelatinIme_en_UK, switchingUnawarelatinIme_hi);
225        assertNextInputMethod(controller, true /* onlyCurrentIme */,
226                subtypeUnawareIme, null, null);
227        assertNextInputMethod(controller, true /* onlyCurrentIme */,
228                japaneseIme_ja_JP, null, null);
229        assertNextInputMethod(controller, true /* onlyCurrentIme */,
230                switchUnawareJapaneseIme_ja_JP, null, null);
231
232        // Make sure that disabled IMEs are not accepted.
233        assertNextInputMethod(controller, false /* onlyCurrentIme */,
234                disabledIme_en_US, null, null);
235        assertNextInputMethod(controller, false /* onlyCurrentIme */,
236                disabledIme_hi, null, null);
237        assertNextInputMethod(controller, false /* onlyCurrentIme */,
238                disabledSwitchingUnawareIme, null, null);
239        assertNextInputMethod(controller, false /* onlyCurrentIme */,
240                disabledSubtypeUnawareIme, null, null);
241        assertNextInputMethod(controller, true /* onlyCurrentIme */,
242                disabledIme_en_US, null, null);
243        assertNextInputMethod(controller, true /* onlyCurrentIme */,
244                disabledIme_hi, null, null);
245        assertNextInputMethod(controller, true /* onlyCurrentIme */,
246                disabledSwitchingUnawareIme, null, null);
247        assertNextInputMethod(controller, true /* onlyCurrentIme */,
248                disabledSubtypeUnawareIme, null, null);
249    }
250
251    @SmallTest
252    public void testControllerImplWithUserAction() throws Exception {
253        final List<ImeSubtypeListItem> enabledItems = createEnabledImeSubtypes();
254        final ImeSubtypeListItem latinIme_en_US = enabledItems.get(0);
255        final ImeSubtypeListItem latinIme_fr = enabledItems.get(1);
256        final ImeSubtypeListItem switchingUnawarelatinIme_en_UK = enabledItems.get(2);
257        final ImeSubtypeListItem switchingUnawarelatinIme_hi = enabledItems.get(3);
258        final ImeSubtypeListItem subtypeUnawareIme = enabledItems.get(4);
259        final ImeSubtypeListItem japaneseIme_ja_JP = enabledItems.get(5);
260        final ImeSubtypeListItem switchUnawareJapaneseIme_ja_JP = enabledItems.get(6);
261
262        final ControllerImpl controller = ControllerImpl.createFrom(
263                null /* currentInstance */, enabledItems);
264
265        // === switching-aware loop ===
266        assertRotationOrder(controller, false /* onlyCurrentIme */,
267                latinIme_en_US, latinIme_fr, japaneseIme_ja_JP);
268        // Then notify that a user did something for latinIme_fr.
269        onUserAction(controller, latinIme_fr);
270        assertRotationOrder(controller, false /* onlyCurrentIme */,
271                latinIme_fr, latinIme_en_US, japaneseIme_ja_JP);
272        // Then notify that a user did something for latinIme_fr again.
273        onUserAction(controller, latinIme_fr);
274        assertRotationOrder(controller, false /* onlyCurrentIme */,
275                latinIme_fr, latinIme_en_US, japaneseIme_ja_JP);
276        // Then notify that a user did something for japaneseIme_ja_JP.
277        onUserAction(controller, latinIme_fr);
278        assertRotationOrder(controller, false /* onlyCurrentIme */,
279                japaneseIme_ja_JP, latinIme_fr, latinIme_en_US);
280        // Check onlyCurrentIme == true.
281        assertNextInputMethod(controller, true /* onlyCurrentIme */,
282                japaneseIme_ja_JP, null, null);
283        assertRotationOrder(controller, true /* onlyCurrentIme */,
284                latinIme_fr, latinIme_en_US);
285        assertRotationOrder(controller, true /* onlyCurrentIme */,
286                latinIme_en_US, latinIme_fr);
287
288        // === switching-unaware loop ===
289        assertRotationOrder(controller, false /* onlyCurrentIme */,
290                switchingUnawarelatinIme_en_UK, switchingUnawarelatinIme_hi, subtypeUnawareIme,
291                switchUnawareJapaneseIme_ja_JP);
292        // User action should be ignored for switching unaware IMEs.
293        onUserAction(controller, switchingUnawarelatinIme_hi);
294        assertRotationOrder(controller, false /* onlyCurrentIme */,
295                switchingUnawarelatinIme_en_UK, switchingUnawarelatinIme_hi, subtypeUnawareIme,
296                switchUnawareJapaneseIme_ja_JP);
297        // User action should be ignored for switching unaware IMEs.
298        onUserAction(controller, switchUnawareJapaneseIme_ja_JP);
299        assertRotationOrder(controller, false /* onlyCurrentIme */,
300                switchingUnawarelatinIme_en_UK, switchingUnawarelatinIme_hi, subtypeUnawareIme,
301                switchUnawareJapaneseIme_ja_JP);
302        // Check onlyCurrentIme == true.
303        assertRotationOrder(controller, true /* onlyCurrentIme */,
304                switchingUnawarelatinIme_en_UK, switchingUnawarelatinIme_hi);
305        assertNextInputMethod(controller, true /* onlyCurrentIme */,
306                subtypeUnawareIme, null, null);
307        assertNextInputMethod(controller, true /* onlyCurrentIme */,
308                switchUnawareJapaneseIme_ja_JP, null, null);
309
310        // Rotation order should be preserved when created with the same subtype list.
311        final List<ImeSubtypeListItem> sameEnabledItems = createEnabledImeSubtypes();
312        final ControllerImpl newController = ControllerImpl.createFrom(controller,
313                sameEnabledItems);
314        assertRotationOrder(newController, false /* onlyCurrentIme */,
315                japaneseIme_ja_JP, latinIme_fr, latinIme_en_US);
316        assertRotationOrder(newController, false /* onlyCurrentIme */,
317                switchingUnawarelatinIme_en_UK, switchingUnawarelatinIme_hi, subtypeUnawareIme,
318                switchUnawareJapaneseIme_ja_JP);
319
320        // Rotation order should be initialized when created with a different subtype list.
321        final List<ImeSubtypeListItem> differentEnabledItems = Arrays.asList(
322                latinIme_en_US, latinIme_fr, switchingUnawarelatinIme_en_UK,
323                switchUnawareJapaneseIme_ja_JP);
324        final ControllerImpl anotherController = ControllerImpl.createFrom(controller,
325                differentEnabledItems);
326        assertRotationOrder(anotherController, false /* onlyCurrentIme */,
327                latinIme_en_US, latinIme_fr);
328        assertRotationOrder(anotherController, false /* onlyCurrentIme */,
329                switchingUnawarelatinIme_en_UK, switchUnawareJapaneseIme_ja_JP);
330    }
331
332    @SmallTest
333    public void testImeSubtypeListItem() throws Exception {
334        final List<ImeSubtypeListItem> items = new ArrayList<>();
335        addDummyImeSubtypeListItems(items, "LatinIme", "LatinIme",
336                Arrays.asList("en_US", "fr", "en", "en_uk", "enn", "e", "EN_US"),
337                true /* supportsSwitchingToNextInputMethod*/);
338        final ImeSubtypeListItem item_en_US = items.get(0);
339        final ImeSubtypeListItem item_fr = items.get(1);
340        final ImeSubtypeListItem item_en = items.get(2);
341        final ImeSubtypeListItem item_enn = items.get(3);
342        final ImeSubtypeListItem item_e = items.get(4);
343        final ImeSubtypeListItem item_EN_US = items.get(5);
344
345        assertTrue(item_en_US.mIsSystemLocale);
346        assertFalse(item_fr.mIsSystemLocale);
347        assertFalse(item_en.mIsSystemLocale);
348        assertFalse(item_en.mIsSystemLocale);
349        assertFalse(item_enn.mIsSystemLocale);
350        assertFalse(item_e.mIsSystemLocale);
351        assertFalse(item_EN_US.mIsSystemLocale);
352
353        assertTrue(item_en_US.mIsSystemLanguage);
354        assertFalse(item_fr.mIsSystemLanguage);
355        assertTrue(item_en.mIsSystemLanguage);
356        assertFalse(item_enn.mIsSystemLocale);
357        assertFalse(item_e.mIsSystemLocale);
358        assertFalse(item_EN_US.mIsSystemLocale);
359    }
360
361    @SmallTest
362    public void testImeSubtypeListComparator() throws Exception {
363        {
364            final List<ImeSubtypeListItem> items = Arrays.asList(
365                    // Subtypes of IME "X".
366                    // Subtypes that has the same locale of the system's.
367                    createDummyItem("X", "E", "en_US", 0, "en_US"),
368                    createDummyItem("X", "Z", "en_US", 3, "en_US"),
369                    createDummyItem("X", "", "en_US", 6, "en_US"),
370                    // Subtypes that has the same language of the system's.
371                    createDummyItem("X", "E", "en", 1, "en_US"),
372                    createDummyItem("X", "Z", "en", 4, "en_US"),
373                    createDummyItem("X", "", "en", 7, "en_US"),
374                    // Subtypes that has different language than the system's.
375                    createDummyItem("X", "A", "hi_IN", 27, "en_US"),
376                    createDummyItem("X", "E", "ja", 2, "en_US"),
377                    createDummyItem("X", "Z", "ja", 5, "en_US"),
378                    createDummyItem("X", "", "ja", 8, "en_US"),
379
380                    // Subtypes of IME "Y".
381                    // Subtypes that has the same locale of the system's.
382                    createDummyItem("Y", "E", "en_US", 9, "en_US"),
383                    createDummyItem("Y", "Z", "en_US", 12, "en_US"),
384                    createDummyItem("Y", "", "en_US", 15, "en_US"),
385                    // Subtypes that has the same language of the system's.
386                    createDummyItem("Y", "E", "en", 10, "en_US"),
387                    createDummyItem("Y", "Z", "en", 13, "en_US"),
388                    createDummyItem("Y", "", "en", 16, "en_US"),
389                    // Subtypes that has different language than the system's.
390                    createDummyItem("Y", "A", "hi_IN", 28, "en_US"),
391                    createDummyItem("Y", "E", "ja", 11, "en_US"),
392                    createDummyItem("Y", "Z", "ja", 14, "en_US"),
393                    createDummyItem("Y", "", "ja", 17, "en_US"),
394
395                    // Subtypes of IME "".
396                    // Subtypes that has the same locale of the system's.
397                    createDummyItem("", "E", "en_US", 18, "en_US"),
398                    createDummyItem("", "Z", "en_US", 21, "en_US"),
399                    createDummyItem("", "", "en_US", 24, "en_US"),
400                    // Subtypes that has the same language of the system's.
401                    createDummyItem("", "E", "en", 19, "en_US"),
402                    createDummyItem("", "Z", "en", 22, "en_US"),
403                    createDummyItem("", "", "en", 25, "en_US"),
404                    // Subtypes that has different language than the system's.
405                    createDummyItem("", "A", "hi_IN", 29, "en_US"),
406                    createDummyItem("", "E", "ja", 20, "en_US"),
407                    createDummyItem("", "Z", "ja", 23, "en_US"),
408                    createDummyItem("", "", "ja", 26, "en_US"));
409
410            // Ensure {@link java.lang.Comparable#compareTo} contracts are satisfied.
411            for (int i = 0; i < items.size(); ++i) {
412                final ImeSubtypeListItem item1 = items.get(i);
413                // Ensures sgn(x.compareTo(y)) == -sgn(y.compareTo(x)).
414                assertTrue(item1 + " has the same order of itself", item1.compareTo(item1) == 0);
415                // Ensures (x.compareTo(y) > 0 && y.compareTo(z) > 0) implies x.compareTo(z) > 0.
416                for (int j = i + 1; j < items.size(); ++j) {
417                    final ImeSubtypeListItem item2 = items.get(j);
418                    // Ensures sgn(x.compareTo(y)) == -sgn(y.compareTo(x)).
419                    assertTrue(item1 + " is less than " + item2, item1.compareTo(item2) < 0);
420                    assertTrue(item2 + " is greater than " + item1, item2.compareTo(item1) > 0);
421                }
422            }
423        }
424
425        {
426            // Following two items have the same priority.
427            final ImeSubtypeListItem nonSystemLocale1 =
428                    createDummyItem("X", "A", "ja_JP", 0, "en_US");
429            final ImeSubtypeListItem nonSystemLocale2 =
430                    createDummyItem("X", "A", "hi_IN", 1, "en_US");
431            assertTrue(nonSystemLocale1.compareTo(nonSystemLocale2) == 0);
432            assertTrue(nonSystemLocale2.compareTo(nonSystemLocale1) == 0);
433            // But those aren't equal to each other.
434            assertFalse(nonSystemLocale1.equals(nonSystemLocale2));
435            assertFalse(nonSystemLocale2.equals(nonSystemLocale1));
436        }
437    }
438}
439