ShutdownThread.java revision 9bb765448df43d41e0a3edb7de1d1641c9251c35
1/*
2 * Copyright (C) 2008 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
17
18package com.android.server.power;
19
20import android.app.ActivityManagerNative;
21import android.app.AlertDialog;
22import android.app.Dialog;
23import android.app.IActivityManager;
24import android.app.ProgressDialog;
25import android.bluetooth.BluetoothAdapter;
26import android.bluetooth.IBluetoothManager;
27import android.media.AudioAttributes;
28import android.nfc.NfcAdapter;
29import android.nfc.INfcAdapter;
30import android.content.BroadcastReceiver;
31import android.content.Context;
32import android.content.DialogInterface;
33import android.content.Intent;
34import android.content.IntentFilter;
35import android.os.Handler;
36import android.os.PowerManager;
37import android.os.RemoteException;
38import android.os.ServiceManager;
39import android.os.SystemClock;
40import android.os.SystemProperties;
41import android.os.UserHandle;
42import android.os.UserManager;
43import android.os.Vibrator;
44import android.os.SystemVibrator;
45import android.os.storage.IMountService;
46import android.os.storage.IMountShutdownObserver;
47import android.system.ErrnoException;
48import android.system.Os;
49
50import com.android.internal.telephony.ITelephony;
51import com.android.server.pm.PackageManagerService;
52
53import android.util.Log;
54import android.view.WindowManager;
55
56import java.io.BufferedReader;
57import java.io.File;
58import java.io.FileReader;
59import java.io.IOException;
60
61public final class ShutdownThread extends Thread {
62    // constants
63    private static final String TAG = "ShutdownThread";
64    private static final int PHONE_STATE_POLL_SLEEP_MSEC = 500;
65    // maximum time we wait for the shutdown broadcast before going on.
66    private static final int MAX_BROADCAST_TIME = 10*1000;
67    private static final int MAX_SHUTDOWN_WAIT_TIME = 20*1000;
68    private static final int MAX_RADIO_WAIT_TIME = 12*1000;
69    private static final int MAX_UNCRYPT_WAIT_TIME = 15*60*1000;
70
71    // length of vibration before shutting down
72    private static final int SHUTDOWN_VIBRATE_MS = 500;
73
74    // state tracking
75    private static Object sIsStartedGuard = new Object();
76    private static boolean sIsStarted = false;
77
78    // uncrypt status file
79    private static final String UNCRYPT_STATUS_FILE = "/cache/recovery/uncrypt_status";
80
81    private static boolean mReboot;
82    private static boolean mRebootSafeMode;
83    private static String mRebootReason;
84
85    // Provides shutdown assurance in case the system_server is killed
86    public static final String SHUTDOWN_ACTION_PROPERTY = "sys.shutdown.requested";
87
88    // Indicates whether we are rebooting into safe mode
89    public static final String REBOOT_SAFEMODE_PROPERTY = "persist.sys.safemode";
90
91    // static instance of this thread
92    private static final ShutdownThread sInstance = new ShutdownThread();
93
94    private static final AudioAttributes VIBRATION_ATTRIBUTES = new AudioAttributes.Builder()
95            .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
96            .setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION)
97            .build();
98
99    private final Object mActionDoneSync = new Object();
100    private boolean mActionDone;
101    private Context mContext;
102    private PowerManager mPowerManager;
103    private PowerManager.WakeLock mCpuWakeLock;
104    private PowerManager.WakeLock mScreenWakeLock;
105    private Handler mHandler;
106
107    private static AlertDialog sConfirmDialog;
108    private ProgressDialog mProgressDialog;
109
110    private ShutdownThread() {
111    }
112
113    /**
114     * Request a clean shutdown, waiting for subsystems to clean up their
115     * state etc.  Must be called from a Looper thread in which its UI
116     * is shown.
117     *
118     * @param context Context used to display the shutdown progress dialog.
119     * @param confirm true if user confirmation is needed before shutting down.
120     */
121    public static void shutdown(final Context context, boolean confirm) {
122        mReboot = false;
123        mRebootSafeMode = false;
124        shutdownInner(context, confirm);
125    }
126
127    static void shutdownInner(final Context context, boolean confirm) {
128        // ensure that only one thread is trying to power down.
129        // any additional calls are just returned
130        synchronized (sIsStartedGuard) {
131            if (sIsStarted) {
132                Log.d(TAG, "Request to shutdown already running, returning.");
133                return;
134            }
135        }
136
137        final int longPressBehavior = context.getResources().getInteger(
138                        com.android.internal.R.integer.config_longPressOnPowerBehavior);
139        final int resourceId = mRebootSafeMode
140                ? com.android.internal.R.string.reboot_safemode_confirm
141                : (longPressBehavior == 2
142                        ? com.android.internal.R.string.shutdown_confirm_question
143                        : com.android.internal.R.string.shutdown_confirm);
144
145        Log.d(TAG, "Notifying thread to start shutdown longPressBehavior=" + longPressBehavior);
146
147        if (confirm) {
148            final CloseDialogReceiver closer = new CloseDialogReceiver(context);
149            if (sConfirmDialog != null) {
150                sConfirmDialog.dismiss();
151            }
152            sConfirmDialog = new AlertDialog.Builder(context)
153                    .setTitle(mRebootSafeMode
154                            ? com.android.internal.R.string.reboot_safemode_title
155                            : com.android.internal.R.string.power_off)
156                    .setMessage(resourceId)
157                    .setPositiveButton(com.android.internal.R.string.yes, new DialogInterface.OnClickListener() {
158                        public void onClick(DialogInterface dialog, int which) {
159                            beginShutdownSequence(context);
160                        }
161                    })
162                    .setNegativeButton(com.android.internal.R.string.no, null)
163                    .create();
164            closer.dialog = sConfirmDialog;
165            sConfirmDialog.setOnDismissListener(closer);
166            sConfirmDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG);
167            sConfirmDialog.show();
168        } else {
169            beginShutdownSequence(context);
170        }
171    }
172
173    private static class CloseDialogReceiver extends BroadcastReceiver
174            implements DialogInterface.OnDismissListener {
175        private Context mContext;
176        public Dialog dialog;
177
178        CloseDialogReceiver(Context context) {
179            mContext = context;
180            IntentFilter filter = new IntentFilter(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
181            context.registerReceiver(this, filter);
182        }
183
184        @Override
185        public void onReceive(Context context, Intent intent) {
186            dialog.cancel();
187        }
188
189        public void onDismiss(DialogInterface unused) {
190            mContext.unregisterReceiver(this);
191        }
192    }
193
194    /**
195     * Request a clean shutdown, waiting for subsystems to clean up their
196     * state etc.  Must be called from a Looper thread in which its UI
197     * is shown.
198     *
199     * @param context Context used to display the shutdown progress dialog.
200     * @param reason code to pass to the kernel (e.g. "recovery"), or null.
201     * @param confirm true if user confirmation is needed before shutting down.
202     */
203    public static void reboot(final Context context, String reason, boolean confirm) {
204        mReboot = true;
205        mRebootSafeMode = false;
206        mRebootReason = reason;
207        shutdownInner(context, confirm);
208    }
209
210    /**
211     * Request a reboot into safe mode.  Must be called from a Looper thread in which its UI
212     * is shown.
213     *
214     * @param context Context used to display the shutdown progress dialog.
215     * @param confirm true if user confirmation is needed before shutting down.
216     */
217    public static void rebootSafeMode(final Context context, boolean confirm) {
218        UserManager um = (UserManager) context.getSystemService(Context.USER_SERVICE);
219        if (um.hasUserRestriction(UserManager.DISALLOW_SAFE_BOOT)) {
220            return;
221        }
222
223        mReboot = true;
224        mRebootSafeMode = true;
225        mRebootReason = null;
226        shutdownInner(context, confirm);
227    }
228
229    private static void beginShutdownSequence(Context context) {
230        synchronized (sIsStartedGuard) {
231            if (sIsStarted) {
232                Log.d(TAG, "Shutdown sequence already running, returning.");
233                return;
234            }
235            sIsStarted = true;
236        }
237
238        // throw up an indeterminate system dialog to indicate radio is
239        // shutting down.
240        ProgressDialog pd = new ProgressDialog(context);
241        if (PowerManager.REBOOT_RECOVERY.equals(mRebootReason)) {
242            pd.setTitle(context.getText(com.android.internal.R.string.reboot_to_recovery_title));
243        } else {
244            pd.setTitle(context.getText(com.android.internal.R.string.power_off));
245        }
246        pd.setMessage(context.getText(com.android.internal.R.string.shutdown_progress));
247        pd.setIndeterminate(true);
248        pd.setCancelable(false);
249        pd.getWindow().setType(WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG);
250
251        pd.show();
252
253        sInstance.mProgressDialog = pd;
254        sInstance.mContext = context;
255        sInstance.mPowerManager = (PowerManager)context.getSystemService(Context.POWER_SERVICE);
256
257        // make sure we never fall asleep again
258        sInstance.mCpuWakeLock = null;
259        try {
260            sInstance.mCpuWakeLock = sInstance.mPowerManager.newWakeLock(
261                    PowerManager.PARTIAL_WAKE_LOCK, TAG + "-cpu");
262            sInstance.mCpuWakeLock.setReferenceCounted(false);
263            sInstance.mCpuWakeLock.acquire();
264        } catch (SecurityException e) {
265            Log.w(TAG, "No permission to acquire wake lock", e);
266            sInstance.mCpuWakeLock = null;
267        }
268
269        // also make sure the screen stays on for better user experience
270        sInstance.mScreenWakeLock = null;
271        if (sInstance.mPowerManager.isScreenOn()) {
272            try {
273                sInstance.mScreenWakeLock = sInstance.mPowerManager.newWakeLock(
274                        PowerManager.FULL_WAKE_LOCK, TAG + "-screen");
275                sInstance.mScreenWakeLock.setReferenceCounted(false);
276                sInstance.mScreenWakeLock.acquire();
277            } catch (SecurityException e) {
278                Log.w(TAG, "No permission to acquire wake lock", e);
279                sInstance.mScreenWakeLock = null;
280            }
281        }
282
283        // start the thread that initiates shutdown
284        sInstance.mHandler = new Handler() {
285        };
286        sInstance.start();
287    }
288
289    void actionDone() {
290        synchronized (mActionDoneSync) {
291            mActionDone = true;
292            mActionDoneSync.notifyAll();
293        }
294    }
295
296    /**
297     * Makes sure we handle the shutdown gracefully.
298     * Shuts off power regardless of radio and bluetooth state if the alloted time has passed.
299     */
300    public void run() {
301        BroadcastReceiver br = new BroadcastReceiver() {
302            @Override public void onReceive(Context context, Intent intent) {
303                // We don't allow apps to cancel this, so ignore the result.
304                actionDone();
305            }
306        };
307
308        /*
309         * Write a system property in case the system_server reboots before we
310         * get to the actual hardware restart. If that happens, we'll retry at
311         * the beginning of the SystemServer startup.
312         */
313        {
314            String reason = (mReboot ? "1" : "0") + (mRebootReason != null ? mRebootReason : "");
315            SystemProperties.set(SHUTDOWN_ACTION_PROPERTY, reason);
316        }
317
318        /*
319         * If we are rebooting into safe mode, write a system property
320         * indicating so.
321         */
322        if (mRebootSafeMode) {
323            SystemProperties.set(REBOOT_SAFEMODE_PROPERTY, "1");
324        }
325
326        Log.i(TAG, "Sending shutdown broadcast...");
327
328        // First send the high-level shut down broadcast.
329        mActionDone = false;
330        Intent intent = new Intent(Intent.ACTION_SHUTDOWN);
331        intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
332        mContext.sendOrderedBroadcastAsUser(intent,
333                UserHandle.ALL, null, br, mHandler, 0, null, null);
334
335        final long endTime = SystemClock.elapsedRealtime() + MAX_BROADCAST_TIME;
336        synchronized (mActionDoneSync) {
337            while (!mActionDone) {
338                long delay = endTime - SystemClock.elapsedRealtime();
339                if (delay <= 0) {
340                    Log.w(TAG, "Shutdown broadcast timed out");
341                    break;
342                }
343                try {
344                    mActionDoneSync.wait(delay);
345                } catch (InterruptedException e) {
346                }
347            }
348        }
349
350        Log.i(TAG, "Shutting down activity manager...");
351
352        final IActivityManager am =
353            ActivityManagerNative.asInterface(ServiceManager.checkService("activity"));
354        if (am != null) {
355            try {
356                am.shutdown(MAX_BROADCAST_TIME);
357            } catch (RemoteException e) {
358            }
359        }
360
361        Log.i(TAG, "Shutting down package manager...");
362
363        final PackageManagerService pm = (PackageManagerService)
364            ServiceManager.getService("package");
365        if (pm != null) {
366            pm.shutdown();
367        }
368
369        // Shutdown radios.
370        shutdownRadios(MAX_RADIO_WAIT_TIME);
371
372        // Shutdown MountService to ensure media is in a safe state
373        IMountShutdownObserver observer = new IMountShutdownObserver.Stub() {
374            public void onShutDownComplete(int statusCode) throws RemoteException {
375                Log.w(TAG, "Result code " + statusCode + " from MountService.shutdown");
376                actionDone();
377            }
378        };
379
380        Log.i(TAG, "Shutting down MountService");
381
382        // Set initial variables and time out time.
383        mActionDone = false;
384        final long endShutTime = SystemClock.elapsedRealtime() + MAX_SHUTDOWN_WAIT_TIME;
385        synchronized (mActionDoneSync) {
386            try {
387                final IMountService mount = IMountService.Stub.asInterface(
388                        ServiceManager.checkService("mount"));
389                if (mount != null) {
390                    mount.shutdown(observer);
391                } else {
392                    Log.w(TAG, "MountService unavailable for shutdown");
393                }
394            } catch (Exception e) {
395                Log.e(TAG, "Exception during MountService shutdown", e);
396            }
397            while (!mActionDone) {
398                long delay = endShutTime - SystemClock.elapsedRealtime();
399                if (delay <= 0) {
400                    Log.w(TAG, "Shutdown wait timed out");
401                    break;
402                }
403                try {
404                    mActionDoneSync.wait(delay);
405                } catch (InterruptedException e) {
406                }
407            }
408        }
409
410        // If it's to reboot into recovery, invoke uncrypt via init service.
411        if (mRebootReason.equals(PowerManager.REBOOT_RECOVERY)) {
412            uncrypt();
413        }
414
415        rebootOrShutdown(mContext, mReboot, mRebootReason);
416    }
417
418    private void prepareUncryptProgress() {
419        // Reset the dialog message to show the decrypt process.
420        mHandler.post(new Runnable() {
421            @Override
422            public void run() {
423                if (mProgressDialog != null) {
424                    mProgressDialog.dismiss();
425                }
426                // It doesn't work to change the style of the existing
427                // one. Have to create a new one.
428                ProgressDialog pd = new ProgressDialog(mContext);
429
430                pd.setTitle(mContext.getText(
431                        com.android.internal.R.string.reboot_to_recovery_title));
432                pd.setMessage(mContext.getText(
433                        com.android.internal.R.string.reboot_to_recovery_progress));
434                pd.setIndeterminate(false);
435                pd.setMax(100);
436                pd.setCancelable(false);
437                pd.getWindow().setType(WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG);
438                pd.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
439                pd.setProgressNumberFormat(null);
440                pd.setProgress(0);
441
442                mProgressDialog = pd;
443                mProgressDialog.show();
444            }
445        });
446    }
447
448    private void setUncryptProgress(final int progress) {
449        mHandler.post(new Runnable() {
450            @Override
451            public void run() {
452                if (mProgressDialog != null) {
453                    mProgressDialog.setProgress(progress);
454                }
455            }
456        });
457    }
458
459    private void shutdownRadios(int timeout) {
460        // If a radio is wedged, disabling it may hang so we do this work in another thread,
461        // just in case.
462        final long endTime = SystemClock.elapsedRealtime() + timeout;
463        final boolean[] done = new boolean[1];
464        Thread t = new Thread() {
465            public void run() {
466                boolean nfcOff;
467                boolean bluetoothOff;
468                boolean radioOff;
469
470                final INfcAdapter nfc =
471                        INfcAdapter.Stub.asInterface(ServiceManager.checkService("nfc"));
472                final ITelephony phone =
473                        ITelephony.Stub.asInterface(ServiceManager.checkService("phone"));
474                final IBluetoothManager bluetooth =
475                        IBluetoothManager.Stub.asInterface(ServiceManager.checkService(
476                                BluetoothAdapter.BLUETOOTH_MANAGER_SERVICE));
477
478                try {
479                    nfcOff = nfc == null ||
480                             nfc.getState() == NfcAdapter.STATE_OFF;
481                    if (!nfcOff) {
482                        Log.w(TAG, "Turning off NFC...");
483                        nfc.disable(false); // Don't persist new state
484                    }
485                } catch (RemoteException ex) {
486                Log.e(TAG, "RemoteException during NFC shutdown", ex);
487                    nfcOff = true;
488                }
489
490                try {
491                    bluetoothOff = bluetooth == null || !bluetooth.isEnabled();
492                    if (!bluetoothOff) {
493                        Log.w(TAG, "Disabling Bluetooth...");
494                        bluetooth.disable(false);  // disable but don't persist new state
495                    }
496                } catch (RemoteException ex) {
497                    Log.e(TAG, "RemoteException during bluetooth shutdown", ex);
498                    bluetoothOff = true;
499                }
500
501                try {
502                    radioOff = phone == null || !phone.needMobileRadioShutdown();
503                    if (!radioOff) {
504                        Log.w(TAG, "Turning off cellular radios...");
505                        phone.shutdownMobileRadios();
506                    }
507                } catch (RemoteException ex) {
508                    Log.e(TAG, "RemoteException during radio shutdown", ex);
509                    radioOff = true;
510                }
511
512                Log.i(TAG, "Waiting for NFC, Bluetooth and Radio...");
513
514                while (SystemClock.elapsedRealtime() < endTime) {
515                    if (!bluetoothOff) {
516                        try {
517                            bluetoothOff = !bluetooth.isEnabled();
518                        } catch (RemoteException ex) {
519                            Log.e(TAG, "RemoteException during bluetooth shutdown", ex);
520                            bluetoothOff = true;
521                        }
522                        if (bluetoothOff) {
523                            Log.i(TAG, "Bluetooth turned off.");
524                        }
525                    }
526                    if (!radioOff) {
527                        try {
528                            radioOff = !phone.needMobileRadioShutdown();
529                        } catch (RemoteException ex) {
530                            Log.e(TAG, "RemoteException during radio shutdown", ex);
531                            radioOff = true;
532                        }
533                        if (radioOff) {
534                            Log.i(TAG, "Radio turned off.");
535                        }
536                    }
537                    if (!nfcOff) {
538                        try {
539                            nfcOff = nfc.getState() == NfcAdapter.STATE_OFF;
540                        } catch (RemoteException ex) {
541                            Log.e(TAG, "RemoteException during NFC shutdown", ex);
542                            nfcOff = true;
543                        }
544                        if (nfcOff) {
545                            Log.i(TAG, "NFC turned off.");
546                        }
547                    }
548
549                    if (radioOff && bluetoothOff && nfcOff) {
550                        Log.i(TAG, "NFC, Radio and Bluetooth shutdown complete.");
551                        done[0] = true;
552                        break;
553                    }
554                    SystemClock.sleep(PHONE_STATE_POLL_SLEEP_MSEC);
555                }
556            }
557        };
558
559        t.start();
560        try {
561            t.join(timeout);
562        } catch (InterruptedException ex) {
563        }
564        if (!done[0]) {
565            Log.w(TAG, "Timed out waiting for NFC, Radio and Bluetooth shutdown.");
566        }
567    }
568
569    /**
570     * Do not call this directly. Use {@link #reboot(Context, String, boolean)}
571     * or {@link #shutdown(Context, boolean)} instead.
572     *
573     * @param context Context used to vibrate or null without vibration
574     * @param reboot true to reboot or false to shutdown
575     * @param reason reason for reboot
576     */
577    public static void rebootOrShutdown(final Context context, boolean reboot, String reason) {
578        if (reboot) {
579            Log.i(TAG, "Rebooting, reason: " + reason);
580            PowerManagerService.lowLevelReboot(reason);
581            Log.e(TAG, "Reboot failed, will attempt shutdown instead");
582        } else if (SHUTDOWN_VIBRATE_MS > 0 && context != null) {
583            // vibrate before shutting down
584            Vibrator vibrator = new SystemVibrator(context);
585            try {
586                vibrator.vibrate(SHUTDOWN_VIBRATE_MS, VIBRATION_ATTRIBUTES);
587            } catch (Exception e) {
588                // Failure to vibrate shouldn't interrupt shutdown.  Just log it.
589                Log.w(TAG, "Failed to vibrate during shutdown.", e);
590            }
591
592            // vibrator is asynchronous so we need to wait to avoid shutting down too soon.
593            try {
594                Thread.sleep(SHUTDOWN_VIBRATE_MS);
595            } catch (InterruptedException unused) {
596            }
597        }
598
599        // Shutdown power
600        Log.i(TAG, "Performing low-level shutdown...");
601        PowerManagerService.lowLevelShutdown();
602    }
603
604    private void uncrypt() {
605        Log.i(TAG, "Calling uncrypt and monitoring the progress...");
606
607        // Update the ProcessDialog message and style.
608        sInstance.prepareUncryptProgress();
609
610        final boolean[] done = new boolean[1];
611        done[0] = false;
612        Thread t = new Thread() {
613            @Override
614            public void run() {
615                // Create the status pipe file to communicate with /system/bin/uncrypt.
616                new File(UNCRYPT_STATUS_FILE).delete();
617                try {
618                    Os.mkfifo(UNCRYPT_STATUS_FILE, 0600);
619                } catch (ErrnoException e) {
620                    Log.w(TAG, "ErrnoException when creating named pipe \"" + UNCRYPT_STATUS_FILE +
621                            "\": " + e.getMessage());
622                }
623
624                SystemProperties.set("ctl.start", "uncrypt");
625
626                // Read the status from the pipe.
627                try (BufferedReader reader = new BufferedReader(
628                        new FileReader(UNCRYPT_STATUS_FILE))) {
629
630                    int last_status = Integer.MIN_VALUE;
631                    while (true) {
632                        String str = reader.readLine();
633                        try {
634                            int status = Integer.parseInt(str);
635
636                            // Avoid flooding the log with the same message.
637                            if (status == last_status && last_status != Integer.MIN_VALUE) {
638                                continue;
639                            }
640                            last_status = status;
641
642                            if (status >= 0 && status < 100) {
643                                // Update status
644                                Log.d(TAG, "uncrypt read status: " + status);
645                                sInstance.setUncryptProgress(status);
646                            } else if (status == 100) {
647                                Log.d(TAG, "uncrypt successfully finished.");
648                                sInstance.setUncryptProgress(status);
649                                break;
650                            } else {
651                                // Error in /system/bin/uncrypt. Or it's rebooting to recovery
652                                // to perform other operations (e.g. factory reset).
653                                Log.d(TAG, "uncrypt failed with status: " + status);
654                                break;
655                            }
656                        } catch (NumberFormatException unused) {
657                            Log.d(TAG, "uncrypt invalid status received: " + str);
658                            break;
659                        }
660                    }
661                } catch (IOException unused) {
662                    Log.w(TAG, "IOException when reading \"" + UNCRYPT_STATUS_FILE + "\".");
663                }
664                done[0] = true;
665            }
666        };
667        t.start();
668
669        try {
670            t.join(MAX_UNCRYPT_WAIT_TIME);
671        } catch (InterruptedException unused) {
672        }
673        if (!done[0]) {
674            Log.w(TAG, "Timed out waiting for uncrypt.");
675        }
676    }
677}
678