PersistentDataStore.java revision abb12ed4f2fe5843d5f23a1b3d29ade4f9da76e0
1/*
2 * Copyright (C) 2012 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.server.input;
18
19import com.android.internal.inputmethod.InputMethodSubtypeHandle;
20import com.android.internal.util.ArrayUtils;
21import com.android.internal.util.FastXmlSerializer;
22import com.android.internal.util.XmlUtils;
23
24import org.xmlpull.v1.XmlPullParser;
25import org.xmlpull.v1.XmlPullParserException;
26import org.xmlpull.v1.XmlSerializer;
27
28import android.annotation.Nullable;
29import android.view.Surface;
30import android.hardware.input.TouchCalibration;
31import android.text.TextUtils;
32import android.util.ArrayMap;
33import android.util.AtomicFile;
34import android.util.Slog;
35import android.util.Xml;
36
37import java.io.BufferedInputStream;
38import java.io.BufferedOutputStream;
39import java.io.File;
40import java.io.FileNotFoundException;
41import java.io.FileOutputStream;
42import java.io.IOException;
43import java.io.InputStream;
44import java.io.PrintWriter;
45import java.nio.charset.StandardCharsets;
46import java.util.ArrayList;
47import java.util.Arrays;
48import java.util.Collections;
49import java.util.HashMap;
50import java.util.List;
51import java.util.Map;
52import java.util.Set;
53
54import libcore.io.IoUtils;
55import libcore.util.Objects;
56
57/**
58 * Manages persistent state recorded by the input manager service as an XML file.
59 * Caller must acquire lock on the data store before accessing it.
60 *
61 * File format:
62 * <code>
63 * &lt;input-mananger-state>
64 *   &lt;input-devices>
65 *     &lt;input-device descriptor="xxxxx" keyboard-layout="yyyyy" />
66 *   &gt;input-devices>
67 * &gt;/input-manager-state>
68 * </code>
69 */
70final class PersistentDataStore {
71    static final String TAG = "InputManager";
72
73    // Input device state by descriptor.
74    private final HashMap<String, InputDeviceState> mInputDevices =
75            new HashMap<String, InputDeviceState>();
76    private final AtomicFile mAtomicFile;
77
78    // True if the data has been loaded.
79    private boolean mLoaded;
80
81    // True if there are changes to be saved.
82    private boolean mDirty;
83
84    public PersistentDataStore() {
85        mAtomicFile = new AtomicFile(new File("/data/system/input-manager-state.xml"));
86    }
87
88    public void saveIfNeeded() {
89        if (mDirty) {
90            save();
91            mDirty = false;
92        }
93    }
94
95    public TouchCalibration getTouchCalibration(String inputDeviceDescriptor, int surfaceRotation) {
96        InputDeviceState state = getInputDeviceState(inputDeviceDescriptor, false);
97        if (state == null) {
98            return TouchCalibration.IDENTITY;
99        }
100
101        TouchCalibration cal = state.getTouchCalibration(surfaceRotation);
102        if (cal == null) {
103            return TouchCalibration.IDENTITY;
104        }
105        return cal;
106    }
107
108    public boolean setTouchCalibration(String inputDeviceDescriptor, int surfaceRotation, TouchCalibration calibration) {
109        InputDeviceState state = getInputDeviceState(inputDeviceDescriptor, true);
110
111        if (state.setTouchCalibration(surfaceRotation, calibration)) {
112            setDirty();
113            return true;
114        }
115
116        return false;
117    }
118
119    public String getCurrentKeyboardLayout(String inputDeviceDescriptor) {
120        InputDeviceState state = getInputDeviceState(inputDeviceDescriptor, false);
121        return state != null ? state.getCurrentKeyboardLayout() : null;
122    }
123
124    public boolean setCurrentKeyboardLayout(String inputDeviceDescriptor,
125            String keyboardLayoutDescriptor) {
126        InputDeviceState state = getInputDeviceState(inputDeviceDescriptor, true);
127        if (state.setCurrentKeyboardLayout(keyboardLayoutDescriptor)) {
128            setDirty();
129            return true;
130        }
131        return false;
132    }
133
134    public String[] getKeyboardLayouts(String inputDeviceDescriptor) {
135        InputDeviceState state = getInputDeviceState(inputDeviceDescriptor, false);
136        if (state == null) {
137            return (String[])ArrayUtils.emptyArray(String.class);
138        }
139        return state.getKeyboardLayouts();
140    }
141    public String getKeyboardLayout(String inputDeviceDescriptor,
142            InputMethodSubtypeHandle imeHandle) {
143        InputDeviceState state = getInputDeviceState(inputDeviceDescriptor, false);
144        if (state == null) {
145            return null;
146        }
147        return state.getKeyboardLayout(imeHandle);
148    }
149
150    public boolean setKeyboardLayout(String inputDeviceDescriptor,
151            InputMethodSubtypeHandle imeHandle, String keyboardLayoutDescriptor) {
152        InputDeviceState state = getInputDeviceState(inputDeviceDescriptor, true);
153        if (state.setKeyboardLayout(imeHandle, keyboardLayoutDescriptor)) {
154            setDirty();
155            return true;
156        }
157        return false;
158    }
159
160    public boolean addKeyboardLayout(String inputDeviceDescriptor, String keyboardLayoutDescriptor) {
161        InputDeviceState state = getInputDeviceState(inputDeviceDescriptor, true);
162        if (state.addKeyboardLayout(keyboardLayoutDescriptor)) {
163            setDirty();
164            return true;
165        }
166        return false;
167    }
168
169    public boolean removeKeyboardLayout(String inputDeviceDescriptor,
170            String keyboardLayoutDescriptor) {
171        InputDeviceState state = getInputDeviceState(inputDeviceDescriptor, true);
172        if (state.removeKeyboardLayout(keyboardLayoutDescriptor)) {
173            setDirty();
174            return true;
175        }
176        return false;
177    }
178
179    public boolean switchKeyboardLayout(String inputDeviceDescriptor,
180            InputMethodSubtypeHandle imeHandle) {
181        InputDeviceState state = getInputDeviceState(inputDeviceDescriptor, false);
182        if (state != null && state.switchKeyboardLayout(imeHandle)) {
183            setDirty();
184            return true;
185        }
186        return false;
187    }
188
189    public boolean removeUninstalledKeyboardLayouts(Set<String> availableKeyboardLayouts) {
190        boolean changed = false;
191        for (InputDeviceState state : mInputDevices.values()) {
192            if (state.removeUninstalledKeyboardLayouts(availableKeyboardLayouts)) {
193                changed = true;
194            }
195        }
196        if (changed) {
197            setDirty();
198            return true;
199        }
200        return false;
201    }
202
203    private InputDeviceState getInputDeviceState(String inputDeviceDescriptor,
204            boolean createIfAbsent) {
205        loadIfNeeded();
206        InputDeviceState state = mInputDevices.get(inputDeviceDescriptor);
207        if (state == null && createIfAbsent) {
208            state = new InputDeviceState();
209            mInputDevices.put(inputDeviceDescriptor, state);
210            setDirty();
211        }
212        return state;
213    }
214
215    private void loadIfNeeded() {
216        if (!mLoaded) {
217            load();
218            mLoaded = true;
219        }
220    }
221
222    private void setDirty() {
223        mDirty = true;
224    }
225
226    private void clearState() {
227        mInputDevices.clear();
228    }
229
230    private void load() {
231        clearState();
232
233        final InputStream is;
234        try {
235            is = mAtomicFile.openRead();
236        } catch (FileNotFoundException ex) {
237            return;
238        }
239
240        XmlPullParser parser;
241        try {
242            parser = Xml.newPullParser();
243            parser.setInput(new BufferedInputStream(is), StandardCharsets.UTF_8.name());
244            loadFromXml(parser);
245        } catch (IOException ex) {
246            Slog.w(InputManagerService.TAG, "Failed to load input manager persistent store data.", ex);
247            clearState();
248        } catch (XmlPullParserException ex) {
249            Slog.w(InputManagerService.TAG, "Failed to load input manager persistent store data.", ex);
250            clearState();
251        } finally {
252            IoUtils.closeQuietly(is);
253        }
254    }
255
256    private void save() {
257        final FileOutputStream os;
258        try {
259            os = mAtomicFile.startWrite();
260            boolean success = false;
261            try {
262                XmlSerializer serializer = new FastXmlSerializer();
263                serializer.setOutput(new BufferedOutputStream(os), StandardCharsets.UTF_8.name());
264                saveToXml(serializer);
265                serializer.flush();
266                success = true;
267            } finally {
268                if (success) {
269                    mAtomicFile.finishWrite(os);
270                } else {
271                    mAtomicFile.failWrite(os);
272                }
273            }
274        } catch (IOException ex) {
275            Slog.w(InputManagerService.TAG, "Failed to save input manager persistent store data.", ex);
276        }
277    }
278
279    private void loadFromXml(XmlPullParser parser)
280            throws IOException, XmlPullParserException {
281        XmlUtils.beginDocument(parser, "input-manager-state");
282        final int outerDepth = parser.getDepth();
283        while (XmlUtils.nextElementWithin(parser, outerDepth)) {
284            if (parser.getName().equals("input-devices")) {
285                loadInputDevicesFromXml(parser);
286            }
287        }
288    }
289
290    private void loadInputDevicesFromXml(XmlPullParser parser)
291            throws IOException, XmlPullParserException {
292        final int outerDepth = parser.getDepth();
293        while (XmlUtils.nextElementWithin(parser, outerDepth)) {
294            if (parser.getName().equals("input-device")) {
295                String descriptor = parser.getAttributeValue(null, "descriptor");
296                if (descriptor == null) {
297                    throw new XmlPullParserException(
298                            "Missing descriptor attribute on input-device.");
299                }
300                if (mInputDevices.containsKey(descriptor)) {
301                    throw new XmlPullParserException("Found duplicate input device.");
302                }
303
304                InputDeviceState state = new InputDeviceState();
305                state.loadFromXml(parser);
306                mInputDevices.put(descriptor, state);
307            }
308        }
309    }
310
311    private void saveToXml(XmlSerializer serializer) throws IOException {
312        serializer.startDocument(null, true);
313        serializer.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
314        serializer.startTag(null, "input-manager-state");
315        serializer.startTag(null, "input-devices");
316        for (Map.Entry<String, InputDeviceState> entry : mInputDevices.entrySet()) {
317            final String descriptor = entry.getKey();
318            final InputDeviceState state = entry.getValue();
319            serializer.startTag(null, "input-device");
320            serializer.attribute(null, "descriptor", descriptor);
321            state.saveToXml(serializer);
322            serializer.endTag(null, "input-device");
323        }
324        serializer.endTag(null, "input-devices");
325        serializer.endTag(null, "input-manager-state");
326        serializer.endDocument();
327    }
328
329    public void dump(PrintWriter pw, String prefix) {
330        pw.println(prefix + "PersistentDataStore");
331        pw.println(prefix + "  mLoaded=" + mLoaded);
332        pw.println(prefix + "  mDirty=" + mDirty);
333        pw.println(prefix + "  InputDeviceStates:");
334        int i = 0;
335        for (Map.Entry<String, InputDeviceState> entry : mInputDevices.entrySet()) {
336            pw.println(prefix + "    " + i++ + ": " + entry.getKey());
337            entry.getValue().dump(pw, prefix + "      ");
338        }
339    }
340
341    private static final class InputDeviceState {
342        private static final String[] CALIBRATION_NAME = { "x_scale",
343                "x_ymix", "x_offset", "y_xmix", "y_scale", "y_offset" };
344
345        private TouchCalibration[] mTouchCalibration = new TouchCalibration[4];
346        @Nullable
347        private String mCurrentKeyboardLayout;
348        private List<String> mUnassociatedKeyboardLayouts = new ArrayList<>();
349        private ArrayMap<InputMethodSubtypeHandle, String> mKeyboardLayouts = new ArrayMap<>();
350
351        public TouchCalibration getTouchCalibration(int surfaceRotation) {
352            try {
353                return mTouchCalibration[surfaceRotation];
354            } catch (ArrayIndexOutOfBoundsException ex) {
355                Slog.w(InputManagerService.TAG, "Cannot get touch calibration.", ex);
356                return null;
357            }
358        }
359
360        public boolean setTouchCalibration(int surfaceRotation, TouchCalibration calibration) {
361            try {
362                if (!calibration.equals(mTouchCalibration[surfaceRotation])) {
363                    mTouchCalibration[surfaceRotation] = calibration;
364                    return true;
365                }
366                return false;
367            } catch (ArrayIndexOutOfBoundsException ex) {
368                Slog.w(InputManagerService.TAG, "Cannot set touch calibration.", ex);
369                return false;
370            }
371        }
372
373        @Nullable
374        public String getCurrentKeyboardLayout() {
375            return mCurrentKeyboardLayout;
376        }
377
378        public boolean setCurrentKeyboardLayout(String keyboardLayout) {
379            if (Objects.equal(mCurrentKeyboardLayout, keyboardLayout)) {
380                return false;
381            }
382            addKeyboardLayout(keyboardLayout);
383            mCurrentKeyboardLayout = keyboardLayout;
384            return true;
385        }
386
387        public String[] getKeyboardLayouts() {
388            if (mUnassociatedKeyboardLayouts.isEmpty()) {
389                return (String[])ArrayUtils.emptyArray(String.class);
390            }
391            return mUnassociatedKeyboardLayouts.toArray(
392                    new String[mUnassociatedKeyboardLayouts.size()]);
393        }
394
395        public String getKeyboardLayout(InputMethodSubtypeHandle handle) {
396            return mKeyboardLayouts.get(handle);
397        }
398
399        public boolean setKeyboardLayout(InputMethodSubtypeHandle imeHandle,
400                String keyboardLayout) {
401            String existingLayout = mKeyboardLayouts.get(imeHandle);
402            if (TextUtils.equals(existingLayout, keyboardLayout)) {
403                return false;
404            }
405            mKeyboardLayouts.put(imeHandle, keyboardLayout);
406            return true;
407        }
408
409        public boolean addKeyboardLayout(String keyboardLayout) {
410            int index = Collections.binarySearch(
411                    mUnassociatedKeyboardLayouts, keyboardLayout);
412            if (index >= 0) {
413                return false;
414            }
415            mUnassociatedKeyboardLayouts.add(-index - 1, keyboardLayout);
416            if (mCurrentKeyboardLayout == null) {
417                mCurrentKeyboardLayout = keyboardLayout;
418            }
419            return true;
420        }
421
422        public boolean removeKeyboardLayout(String keyboardLayout) {
423            int index = Collections.binarySearch(mUnassociatedKeyboardLayouts, keyboardLayout);
424            if (index < 0) {
425                return false;
426            }
427            mUnassociatedKeyboardLayouts.remove(index);
428            updateCurrentKeyboardLayoutIfRemoved(keyboardLayout, index);
429            return true;
430        }
431
432        private void updateCurrentKeyboardLayoutIfRemoved(
433                String removedKeyboardLayout, int removedIndex) {
434            if (Objects.equal(mCurrentKeyboardLayout, removedKeyboardLayout)) {
435                if (!mUnassociatedKeyboardLayouts.isEmpty()) {
436                    int index = removedIndex;
437                    if (index == mUnassociatedKeyboardLayouts.size()) {
438                        index = 0;
439                    }
440                    mCurrentKeyboardLayout = mUnassociatedKeyboardLayouts.get(index);
441                } else {
442                    mCurrentKeyboardLayout = null;
443                }
444            }
445        }
446
447        public boolean switchKeyboardLayout(InputMethodSubtypeHandle imeHandle) {
448            final String layout = mKeyboardLayouts.get(imeHandle);
449            if (!TextUtils.equals(mCurrentKeyboardLayout, layout)) {
450                mCurrentKeyboardLayout = layout;
451                return true;
452            }
453            return false;
454        }
455
456        public boolean removeUninstalledKeyboardLayouts(Set<String> availableKeyboardLayouts) {
457            boolean changed = false;
458            for (int i = mUnassociatedKeyboardLayouts.size(); i-- > 0; ) {
459                String keyboardLayout = mUnassociatedKeyboardLayouts.get(i);
460                if (!availableKeyboardLayouts.contains(keyboardLayout)) {
461                    Slog.i(TAG, "Removing uninstalled keyboard layout " + keyboardLayout);
462                    mUnassociatedKeyboardLayouts.remove(i);
463                    updateCurrentKeyboardLayoutIfRemoved(keyboardLayout, i);
464                    changed = true;
465                }
466            }
467            return changed;
468        }
469
470        public void loadFromXml(XmlPullParser parser)
471                throws IOException, XmlPullParserException {
472            final int outerDepth = parser.getDepth();
473            while (XmlUtils.nextElementWithin(parser, outerDepth)) {
474                if (parser.getName().equals("keyboard-layout")) {
475                    String descriptor = parser.getAttributeValue(null, "descriptor");
476                    if (descriptor == null) {
477                        throw new XmlPullParserException(
478                                "Missing descriptor attribute on keyboard-layout.");
479                    }
480
481                    String current = parser.getAttributeValue(null, "current");
482                    if (current != null && current.equals("true")) {
483                        if (mCurrentKeyboardLayout != null) {
484                            throw new XmlPullParserException(
485                                    "Found multiple current keyboard layouts.");
486                        }
487                        mCurrentKeyboardLayout = descriptor;
488                    }
489
490                    String inputMethodId = parser.getAttributeValue(null, "input-method-id");
491                    String inputMethodSubtypeId =
492                        parser.getAttributeValue(null, "input-method-subtype-id");
493                    if (inputMethodId == null && inputMethodSubtypeId != null
494                            || inputMethodId != null && inputMethodSubtypeId == null) {
495                        throw new XmlPullParserException(
496                                "Found an incomplete input method description");
497                    }
498
499                    if (inputMethodSubtypeId != null) {
500                        InputMethodSubtypeHandle handle = new InputMethodSubtypeHandle(
501                                inputMethodId, Integer.parseInt(inputMethodSubtypeId));
502                        if (mKeyboardLayouts.containsKey(handle)) {
503                            throw new XmlPullParserException(
504                                    "Found duplicate subtype to keyboard layout mapping: "
505                                    + handle);
506                        }
507                        mKeyboardLayouts.put(handle, descriptor);
508                    } else {
509                        if (mUnassociatedKeyboardLayouts.contains(descriptor)) {
510                            throw new XmlPullParserException(
511                                    "Found duplicate unassociated keyboard layout: " + descriptor);
512                        }
513                        mUnassociatedKeyboardLayouts.add(descriptor);
514                    }
515                } else if (parser.getName().equals("calibration")) {
516                    String format = parser.getAttributeValue(null, "format");
517                    String rotation = parser.getAttributeValue(null, "rotation");
518                    int r = -1;
519
520                    if (format == null) {
521                        throw new XmlPullParserException(
522                                "Missing format attribute on calibration.");
523                    }
524                    if (!format.equals("affine")) {
525                        throw new XmlPullParserException(
526                                "Unsupported format for calibration.");
527                    }
528                    if (rotation != null) {
529                        try {
530                            r = stringToSurfaceRotation(rotation);
531                        } catch (IllegalArgumentException e) {
532                            throw new XmlPullParserException(
533                                    "Unsupported rotation for calibration.");
534                        }
535                    }
536
537                    float[] matrix = TouchCalibration.IDENTITY.getAffineTransform();
538                    int depth = parser.getDepth();
539                    while (XmlUtils.nextElementWithin(parser, depth)) {
540                        String tag = parser.getName().toLowerCase();
541                        String value = parser.nextText();
542
543                        for (int i = 0; i < matrix.length && i < CALIBRATION_NAME.length; i++) {
544                            if (tag.equals(CALIBRATION_NAME[i])) {
545                                matrix[i] = Float.parseFloat(value);
546                                break;
547                            }
548                        }
549                    }
550
551                    if (r == -1) {
552                        // Assume calibration applies to all rotations
553                        for (r = 0; r < mTouchCalibration.length; r++) {
554                            mTouchCalibration[r] = new TouchCalibration(matrix[0],
555                                matrix[1], matrix[2], matrix[3], matrix[4], matrix[5]);
556                        }
557                    } else {
558                        mTouchCalibration[r] = new TouchCalibration(matrix[0],
559                            matrix[1], matrix[2], matrix[3], matrix[4], matrix[5]);
560                    }
561                }
562            }
563
564            // Maintain invariant that layouts are sorted.
565            Collections.sort(mUnassociatedKeyboardLayouts);
566
567            // Maintain invariant that there is always a current keyboard layout unless
568            // there are none installed.
569            if (mCurrentKeyboardLayout == null && !mUnassociatedKeyboardLayouts.isEmpty()) {
570                mCurrentKeyboardLayout = mUnassociatedKeyboardLayouts.get(0);
571            }
572        }
573
574        public void saveToXml(XmlSerializer serializer) throws IOException {
575            for (String layout : mUnassociatedKeyboardLayouts) {
576                serializer.startTag(null, "keyboard-layout");
577                serializer.attribute(null, "descriptor", layout);
578                serializer.endTag(null, "keyboard-layout");
579            }
580
581            final int N = mKeyboardLayouts.size();
582            for (int i = 0; i < N; i++) {
583                final InputMethodSubtypeHandle handle = mKeyboardLayouts.keyAt(i);
584                final String layout = mKeyboardLayouts.valueAt(i);
585                serializer.startTag(null, "keyboard-layout");
586                serializer.attribute(null, "descriptor", layout);
587                serializer.attribute(null, "input-method-id", handle.getInputMethodId());
588                serializer.attribute(null, "input-method-subtype-id",
589                        Integer.toString(handle.getSubtypeId()));
590                if (layout.equals(mCurrentKeyboardLayout)) {
591                    serializer.attribute(null, "current", "true");
592                }
593                serializer.endTag(null, "keyboard-layout");
594            }
595
596            for (int i = 0; i < mTouchCalibration.length; i++) {
597                if (mTouchCalibration[i] != null) {
598                    String rotation = surfaceRotationToString(i);
599                    float[] transform = mTouchCalibration[i].getAffineTransform();
600
601                    serializer.startTag(null, "calibration");
602                    serializer.attribute(null, "format", "affine");
603                    serializer.attribute(null, "rotation", rotation);
604                    for (int j = 0; j < transform.length && j < CALIBRATION_NAME.length; j++) {
605                        serializer.startTag(null, CALIBRATION_NAME[j]);
606                        serializer.text(Float.toString(transform[j]));
607                        serializer.endTag(null, CALIBRATION_NAME[j]);
608                    }
609                    serializer.endTag(null, "calibration");
610                }
611            }
612        }
613
614        private void dump(final PrintWriter pw, final String prefix) {
615            pw.println(prefix + "CurrentKeyboardLayout=" + mCurrentKeyboardLayout);
616            pw.println(prefix + "UnassociatedKeyboardLayouts=" + mUnassociatedKeyboardLayouts);
617            pw.println(prefix + "TouchCalibration=" + Arrays.toString(mTouchCalibration));
618            pw.println(prefix + "Subtype to Layout Mappings:");
619            final int N = mKeyboardLayouts.size();
620            if (N != 0) {
621                for (int i = 0; i < N; i++) {
622                    pw.println(prefix + "  " + mKeyboardLayouts.keyAt(i) + ": "
623                            + mKeyboardLayouts.valueAt(i));
624                }
625            } else {
626                pw.println(prefix + "  <none>");
627            }
628        }
629
630        private static String surfaceRotationToString(int surfaceRotation) {
631            switch (surfaceRotation) {
632                case Surface.ROTATION_0:   return "0";
633                case Surface.ROTATION_90:  return "90";
634                case Surface.ROTATION_180: return "180";
635                case Surface.ROTATION_270: return "270";
636            }
637            throw new IllegalArgumentException("Unsupported surface rotation value" + surfaceRotation);
638        }
639
640        private static int stringToSurfaceRotation(String s) {
641            if ("0".equals(s)) {
642                return Surface.ROTATION_0;
643            }
644            if ("90".equals(s)) {
645                return Surface.ROTATION_90;
646            }
647            if ("180".equals(s)) {
648                return Surface.ROTATION_180;
649            }
650            if ("270".equals(s)) {
651                return Surface.ROTATION_270;
652            }
653            throw new IllegalArgumentException("Unsupported surface rotation string '" + s + "'");
654        }
655    }
656}
657