DreamService.java revision f434d23a16b34e7815049db29bbcd0cd8a4e4862
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 */
16package android.service.dreams;
17
18import java.io.FileDescriptor;
19import java.io.PrintWriter;
20
21import android.annotation.SdkConstant;
22import android.annotation.SdkConstant.SdkConstantType;
23import android.app.Service;
24import android.content.Intent;
25import android.graphics.PixelFormat;
26import android.graphics.drawable.ColorDrawable;
27import android.os.Handler;
28import android.os.IBinder;
29import android.os.ServiceManager;
30import android.util.Slog;
31import android.view.ActionMode;
32import android.view.KeyEvent;
33import android.view.Menu;
34import android.view.MenuItem;
35import android.view.MotionEvent;
36import android.view.View;
37import android.view.ViewGroup;
38import android.view.Window;
39import android.view.WindowManager;
40import android.view.WindowManagerGlobal;
41import android.view.WindowManager.LayoutParams;
42import android.view.accessibility.AccessibilityEvent;
43
44import com.android.internal.policy.PolicyManager;
45
46/**
47 * Extend this class to implement a custom Dream (displayed to the user as a "Sleep Mode").
48 *
49 * <p>Dreams are interactive screensavers launched when a charging device is idle, or docked in a
50 * desk dock. Dreams provide another modality for apps to express themselves, tailored for
51 * an exhibition/lean-back experience.</p>
52 *
53 * <p>The Dream lifecycle is as follows:</p>
54 * <ul>
55 *   <li>onAttachedToWindow</li>
56 *   <li>onDreamingStarted</li>
57 *   <li>onDreamingStopped</li>
58 *   <li>onDetachedFromWindow</li>
59 * </ul>
60 *
61 * <p>In addition, onCreate and onDestroy (from the Service interface) will also be called, but
62 * initialization and teardown should be done by overriding the hooks above.</p>
63 *
64 * <p>To be available to the system, Dreams should be declared in the manifest as follows:</p>
65 * <pre>
66 * &lt;service
67 *     android:name=".MyDream"
68 *     android:exported="true"
69 *     android:icon="@drawable/my_icon"
70 *     android:label="@string/my_dream_label" >
71 *
72 *     &lt;intent-filter>
73 *         &lt;action android:name="android.service.dreams.DreamService" />
74 *         &lt;category android:name="android.intent.category.DEFAULT" />
75 *     &lt;/intent-filter>
76 *
77 *     &lt;!-- Point to additional information for this dream (optional) -->
78 *     &lt;meta-data
79 *         android:name="android.service.dream"
80 *         android:resource="@xml/my_dream" />
81 * &lt;/service>
82 * </pre>
83 * <p>If specified, additional information for the dream is defined using the
84 * <code>&lt;{@link android.R.styleable#Dream dream}&gt;</code> element.  For example:</p>
85 * <pre>
86 * (in res/xml/my_dream.xml)
87 *
88 * &lt;dream xmlns:android="http://schemas.android.com/apk/res/android"
89 *     android:settingsActivity="com.example.app/.MyDreamSettingsActivity" />
90 * </pre>
91 */
92public class DreamService extends Service implements Window.Callback {
93    private final String TAG = DreamService.class.getSimpleName() + "[" + getClass().getSimpleName() + "]";
94
95    /**
96     * The name of the dream manager service.
97     * @hide
98     */
99    public static final String DREAM_SERVICE = "dreams";
100
101    /**
102     * The {@link Intent} that must be declared as handled by the service.
103     */
104    @SdkConstant(SdkConstantType.SERVICE_ACTION)
105    public static final String SERVICE_INTERFACE =
106            "android.service.dreams.DreamService";
107
108    /**
109     * Name under which a Dream publishes information about itself.
110     * This meta-data must reference an XML resource containing
111     * a <code>&lt;{@link android.R.styleable#Dream dream}&gt;</code>
112     * tag.
113     */
114    public static final String DREAM_META_DATA = "android.service.dream";
115
116    private final Handler mHandler = new Handler();
117    private IBinder mWindowToken;
118    private Window mWindow;
119    private WindowManager mWindowManager;
120    private IDreamManager mSandman;
121    private boolean mInteractive = false;
122    private boolean mLowProfile = true;
123    private boolean mFullscreen = false;
124    private boolean mScreenBright = true;
125    private boolean mFinished;
126
127    private boolean mDebug = false;
128
129    /**
130     * @hide
131     */
132    public void setDebug(boolean dbg) {
133        mDebug = dbg;
134    }
135
136    // begin Window.Callback methods
137    /** {@inheritDoc} */
138    @Override
139    public boolean dispatchKeyEvent(KeyEvent event) {
140        // TODO: create more flexible version of mInteractive that allows use of KEYCODE_BACK
141        if (!mInteractive) {
142            if (mDebug) Slog.v(TAG, "Finishing on keyEvent");
143            safelyFinish();
144            return true;
145        } else if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
146            if (mDebug) Slog.v(TAG, "Finishing on back key");
147            safelyFinish();
148            return true;
149        }
150        return mWindow.superDispatchKeyEvent(event);
151    }
152
153    /** {@inheritDoc} */
154    @Override
155    public boolean dispatchKeyShortcutEvent(KeyEvent event) {
156        if (!mInteractive) {
157            if (mDebug) Slog.v(TAG, "Finishing on keyShortcutEvent");
158            safelyFinish();
159            return true;
160        }
161        return mWindow.superDispatchKeyShortcutEvent(event);
162    }
163
164    /** {@inheritDoc} */
165    @Override
166    public boolean dispatchTouchEvent(MotionEvent event) {
167        // TODO: create more flexible version of mInteractive that allows clicks
168        // but finish()es on any other kind of activity
169        if (!mInteractive) {
170            if (mDebug) Slog.v(TAG, "Finishing on touchEvent");
171            safelyFinish();
172            return true;
173        }
174        return mWindow.superDispatchTouchEvent(event);
175    }
176
177    /** {@inheritDoc} */
178    @Override
179    public boolean dispatchTrackballEvent(MotionEvent event) {
180        if (!mInteractive) {
181            if (mDebug) Slog.v(TAG, "Finishing on trackballEvent");
182            safelyFinish();
183            return true;
184        }
185        return mWindow.superDispatchTrackballEvent(event);
186    }
187
188    /** {@inheritDoc} */
189    @Override
190    public boolean dispatchGenericMotionEvent(MotionEvent event) {
191        if (!mInteractive) {
192            if (mDebug) Slog.v(TAG, "Finishing on genericMotionEvent");
193            safelyFinish();
194            return true;
195        }
196        return mWindow.superDispatchGenericMotionEvent(event);
197    }
198
199    /** {@inheritDoc} */
200    @Override
201    public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
202        return false;
203    }
204
205    /** {@inheritDoc} */
206    @Override
207    public View onCreatePanelView(int featureId) {
208        return null;
209    }
210
211    /** {@inheritDoc} */
212    @Override
213    public boolean onCreatePanelMenu(int featureId, Menu menu) {
214        return false;
215    }
216
217    /** {@inheritDoc} */
218    @Override
219    public boolean onPreparePanel(int featureId, View view, Menu menu) {
220        return false;
221    }
222
223    /** {@inheritDoc} */
224    @Override
225    public boolean onMenuOpened(int featureId, Menu menu) {
226        return false;
227    }
228
229    /** {@inheritDoc} */
230    @Override
231    public boolean onMenuItemSelected(int featureId, MenuItem item) {
232        return false;
233    }
234
235    /** {@inheritDoc} */
236    @Override
237    public void onWindowAttributesChanged(LayoutParams attrs) {
238    }
239
240    /** {@inheritDoc} */
241    @Override
242    public void onContentChanged() {
243    }
244
245    /** {@inheritDoc} */
246    @Override
247    public void onWindowFocusChanged(boolean hasFocus) {
248    }
249
250    /** {@inheritDoc} */
251    @Override
252    public void onAttachedToWindow() {
253    }
254
255    /** {@inheritDoc} */
256    @Override
257    public void onDetachedFromWindow() {
258    }
259
260    /** {@inheritDoc} */
261    @Override
262    public void onPanelClosed(int featureId, Menu menu) {
263    }
264
265    /** {@inheritDoc} */
266    @Override
267    public boolean onSearchRequested() {
268        return false;
269    }
270
271    /** {@inheritDoc} */
272    @Override
273    public ActionMode onWindowStartingActionMode(android.view.ActionMode.Callback callback) {
274        return null;
275    }
276
277    /** {@inheritDoc} */
278    @Override
279    public void onActionModeStarted(ActionMode mode) {
280    }
281
282    /** {@inheritDoc} */
283    @Override
284    public void onActionModeFinished(ActionMode mode) {
285    }
286    // end Window.Callback methods
287
288    // begin public api
289    /**
290     * Retrieves the current {@link android.view.WindowManager} for the dream.
291     * Behaves similarly to {@link android.app.Activity#getWindowManager()}.
292     *
293     * @return The current window manager, or null if the dream is not started.
294     */
295    public WindowManager getWindowManager() {
296        return mWindowManager;
297    }
298
299    /**
300     * Retrieves the current {@link android.view.Window} for the dream.
301     * Behaves similarly to {@link android.app.Activity#getWindow()}.
302     *
303     * @return The current window, or null if the dream is not started.
304     */
305    public Window getWindow() {
306        return mWindow;
307    }
308
309   /**
310     * Inflates a layout resource and set it to be the content view for this Dream.
311     * Behaves similarly to {@link android.app.Activity#setContentView(int)}.
312     *
313     * <p>Note: Requires a window, do not call before {@link #onAttachedToWindow()}</p>
314     *
315     * @param layoutResID Resource ID to be inflated.
316     *
317     * @see #setContentView(android.view.View)
318     * @see #setContentView(android.view.View, android.view.ViewGroup.LayoutParams)
319     */
320    public void setContentView(int layoutResID) {
321        getWindow().setContentView(layoutResID);
322    }
323
324    /**
325     * Sets a view to be the content view for this Dream.
326     * Behaves similarly to {@link android.app.Activity#setContentView(android.view.View)},
327     * including using {@link ViewGroup.LayoutParams#MATCH_PARENT} as the layout height and width of the view.
328     *
329     * <p>Note: Requires a window, do not call before {@link #onAttachedToWindow()}</p>
330     * @param view The desired content to display.
331     *
332     * @see #setContentView(int)
333     * @see #setContentView(android.view.View, android.view.ViewGroup.LayoutParams)
334     */
335    public void setContentView(View view) {
336        getWindow().setContentView(view);
337    }
338
339    /**
340     * Sets a view to be the content view for this Dream.
341     * Behaves similarly to
342     * {@link android.app.Activity#setContentView(android.view.View, android.view.ViewGroup.LayoutParams)}.
343     *
344     * <p>Note: Requires a window, do not call before {@link #onAttachedToWindow()}</p>
345     *
346     * @param view The desired content to display.
347     * @param params Layout parameters for the view.
348     *
349     * @see #setContentView(android.view.View)
350     * @see #setContentView(int)
351     */
352    public void setContentView(View view, ViewGroup.LayoutParams params) {
353        getWindow().setContentView(view, params);
354    }
355
356    /**
357     * Adds a view to the Dream's window, leaving other content views in place.
358     *
359     * <p>Note: Requires a window, do not call before {@link #onAttachedToWindow()}</p>
360     *
361     * @param view The desired content to display.
362     * @param params Layout parameters for the view.
363     */
364    public void addContentView(View view, ViewGroup.LayoutParams params) {
365        getWindow().addContentView(view, params);
366    }
367
368    /**
369     * Finds a view that was identified by the id attribute from the XML that
370     * was processed in {@link #onCreate}.
371     *
372     * <p>Note: Requires a window, do not call before {@link #onAttachedToWindow()}</p>
373     *
374     * @return The view if found or null otherwise.
375     */
376    public View findViewById(int id) {
377        return getWindow().findViewById(id);
378    }
379
380    /**
381     * Marks this dream as interactive to receive input events.
382     *
383     * <p>Non-interactive dreams (default) will dismiss on the first input event.</p>
384     *
385     * <p>Interactive dreams should call {@link #finish()} to dismiss themselves.</p>
386     *
387     * @param interactive True if this dream will handle input events.
388     */
389    public void setInteractive(boolean interactive) {
390        mInteractive = interactive;
391    }
392
393    /**
394     * Returns whether or not this dream is interactive.  Defaults to false.
395     *
396     * @see #setInteractive(boolean)
397     */
398    public boolean isInteractive() {
399        return mInteractive;
400    }
401
402    /**
403     * Sets View.SYSTEM_UI_FLAG_LOW_PROFILE on the content view.
404     *
405     * @param lowProfile True to set View.SYSTEM_UI_FLAG_LOW_PROFILE
406     * @hide There is no reason to have this -- dreams can set this flag
407     * on their own content view, and from there can actually do the
408     * correct interactions with it (seeing when it is cleared etc).
409     */
410    public void setLowProfile(boolean lowProfile) {
411        mLowProfile = lowProfile;
412        int flag = View.SYSTEM_UI_FLAG_LOW_PROFILE;
413        applySystemUiVisibilityFlags(mLowProfile ? flag : 0, flag);
414    }
415
416    /**
417     * Returns whether or not this dream is in low profile mode. Defaults to true.
418     *
419     * @see #setLowProfile(boolean)
420     * @hide
421     */
422    public boolean isLowProfile() {
423        return getSystemUiVisibilityFlagValue(View.SYSTEM_UI_FLAG_LOW_PROFILE, mLowProfile);
424    }
425
426    /**
427     * Controls {@link android.view.WindowManager.LayoutParams#FLAG_FULLSCREEN}
428     * on the dream's window.
429     *
430     * @param fullscreen If true, the fullscreen flag will be set; else it
431     * will be cleared.
432     */
433    public void setFullscreen(boolean fullscreen) {
434        mFullscreen = fullscreen;
435        int flag = WindowManager.LayoutParams.FLAG_FULLSCREEN;
436        applyWindowFlags(mFullscreen ? flag : 0, flag);
437    }
438
439    /**
440     * Returns whether or not this dream is in fullscreen mode. Defaults to false.
441     *
442     * @see #setFullscreen(boolean)
443     */
444    public boolean isFullscreen() {
445        return mFullscreen;
446    }
447
448    /**
449     * Marks this dream as keeping the screen bright while dreaming.
450     *
451     * @param screenBright True to keep the screen bright while dreaming.
452     */
453    public void setScreenBright(boolean screenBright) {
454        mScreenBright = screenBright;
455        int flag = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON;
456        applyWindowFlags(mScreenBright ? flag : 0, flag);
457    }
458
459    /**
460     * Returns whether or not this dream keeps the screen bright while dreaming. Defaults to false,
461     * allowing the screen to dim if necessary.
462     *
463     * @see #setScreenBright(boolean)
464     */
465    public boolean isScreenBright() {
466        return getWindowFlagValue(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON, mScreenBright);
467    }
468
469    /**
470     * Called when this Dream is constructed.
471     */
472    @Override
473    public void onCreate() {
474        if (mDebug) Slog.v(TAG, "onCreate() on thread " + Thread.currentThread().getId());
475        super.onCreate();
476    }
477
478    /**
479     * Called when the dream's window has been created and is visible and animation may now begin.
480     */
481    public void onDreamingStarted() {
482        if (mDebug) Slog.v(TAG, "onDreamingStarted()");
483        // hook for subclasses
484    }
485
486    /**
487     * Called when this Dream is stopped, either by external request or by calling finish(),
488     * before the window has been removed.
489     */
490    public void onDreamingStopped() {
491        if (mDebug) Slog.v(TAG, "onDreamingStopped()");
492        // hook for subclasses
493    }
494
495    /** {@inheritDoc} */
496    @Override
497    public final IBinder onBind(Intent intent) {
498        if (mDebug) Slog.v(TAG, "onBind() intent = " + intent);
499        return new DreamServiceWrapper();
500    }
501
502    /**
503     * Stops the dream, detaches from the window, and wakes up.
504     */
505    public final void finish() {
506        if (mDebug) Slog.v(TAG, "finish()");
507        finishInternal();
508    }
509
510    /** {@inheritDoc} */
511    @Override
512    public void onDestroy() {
513        if (mDebug) Slog.v(TAG, "onDestroy()");
514        // hook for subclasses
515
516        // Just in case destroy came in before detach, let's take care of that now
517        detach();
518
519        super.onDestroy();
520    }
521
522    // end public api
523
524    private void loadSandman() {
525        mSandman = IDreamManager.Stub.asInterface(ServiceManager.getService(DREAM_SERVICE));
526    }
527
528    /**
529     * Called by DreamController.stopDream() when the Dream is about to be unbound and destroyed.
530     *
531     * Must run on mHandler.
532     */
533    private final void detach() {
534        if (mWindow == null) {
535            // already detached!
536            return;
537        }
538
539        try {
540            onDreamingStopped();
541        } catch (Throwable t) {
542            Slog.w(TAG, "Crashed in onDreamingStopped()", t);
543            // we were going to stop anyway
544        }
545
546        if (mDebug) Slog.v(TAG, "detach(): Removing window from window manager");
547        try {
548            // force our window to be removed synchronously
549            mWindowManager.removeViewImmediate(mWindow.getDecorView());
550            // the following will print a log message if it finds any other leaked windows
551            WindowManagerGlobal.getInstance().closeAll(mWindowToken,
552                    this.getClass().getName(), "Dream");
553        } catch (Throwable t) {
554            Slog.w(TAG, "Crashed removing window view", t);
555        }
556
557        mWindow = null;
558        mWindowToken = null;
559    }
560
561    /**
562     * Called when the Dream is ready to be shown.
563     *
564     * Must run on mHandler.
565     *
566     * @param windowToken A window token that will allow a window to be created in the correct layer.
567     */
568    private final void attach(IBinder windowToken) {
569        if (mWindowToken != null) {
570            Slog.e(TAG, "attach() called when already attached with token=" + mWindowToken);
571            return;
572        }
573
574        if (mDebug) Slog.v(TAG, "Attached on thread " + Thread.currentThread().getId());
575
576        if (mSandman == null) {
577            loadSandman();
578        }
579        mWindowToken = windowToken;
580        mWindow = PolicyManager.makeNewWindow(this);
581        mWindow.setCallback(this);
582        mWindow.requestFeature(Window.FEATURE_NO_TITLE);
583        mWindow.setBackgroundDrawable(new ColorDrawable(0xFF000000));
584        mWindow.setFormat(PixelFormat.OPAQUE);
585
586        if (mDebug) Slog.v(TAG, String.format("Attaching window token: %s to window of type %s",
587                windowToken, WindowManager.LayoutParams.TYPE_DREAM));
588
589        WindowManager.LayoutParams lp = mWindow.getAttributes();
590        lp.type = WindowManager.LayoutParams.TYPE_DREAM;
591        lp.token = windowToken;
592        lp.windowAnimations = com.android.internal.R.style.Animation_Dream;
593        lp.flags |= ( WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
594                    | WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR
595                    | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
596                    | WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD
597                    | WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON
598                    | (mFullscreen ? WindowManager.LayoutParams.FLAG_FULLSCREEN : 0)
599                    | (mScreenBright ? WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON : 0)
600                    );
601        mWindow.setAttributes(lp);
602
603        if (mDebug) Slog.v(TAG, "Created and attached window: " + mWindow);
604
605        mWindow.setWindowManager(null, windowToken, "dream", true);
606        mWindowManager = mWindow.getWindowManager();
607
608        if (mDebug) Slog.v(TAG, "Window added on thread " + Thread.currentThread().getId());
609        try {
610            applySystemUiVisibilityFlags(
611                    (mLowProfile ? View.SYSTEM_UI_FLAG_LOW_PROFILE : 0),
612                    View.SYSTEM_UI_FLAG_LOW_PROFILE);
613            getWindowManager().addView(mWindow.getDecorView(), mWindow.getAttributes());
614        } catch (Throwable t) {
615            Slog.w(TAG, "Crashed adding window view", t);
616            safelyFinish();
617            return;
618        }
619
620        // start it up
621        mHandler.post(new Runnable() {
622            @Override
623            public void run() {
624                try {
625                    onDreamingStarted();
626                } catch (Throwable t) {
627                    Slog.w(TAG, "Crashed in onDreamingStarted()", t);
628                    safelyFinish();
629                }
630            }
631        });
632    }
633
634    private void safelyFinish() {
635        if (mDebug) Slog.v(TAG, "safelyFinish()");
636        try {
637            finish();
638        } catch (Throwable t) {
639            Slog.w(TAG, "Crashed in safelyFinish()", t);
640            finishInternal();
641            return;
642        }
643
644        if (!mFinished) {
645            Slog.w(TAG, "Bad dream, did not call super.finish()");
646            finishInternal();
647        }
648    }
649
650    private void finishInternal() {
651        if (mDebug) Slog.v(TAG, "finishInternal() mFinished = " + mFinished);
652        if (mFinished) return;
653        try {
654            mFinished = true;
655
656            if (mSandman != null) {
657                mSandman.finishSelf(mWindowToken);
658            } else {
659                Slog.w(TAG, "No dream manager found");
660            }
661            stopSelf(); // if launched via any other means
662
663        } catch (Throwable t) {
664            Slog.w(TAG, "Crashed in finishInternal()", t);
665        }
666    }
667
668    private boolean getWindowFlagValue(int flag, boolean defaultValue) {
669        return mWindow == null ? defaultValue : (mWindow.getAttributes().flags & flag) != 0;
670    }
671
672    private void applyWindowFlags(int flags, int mask) {
673        if (mWindow != null) {
674            WindowManager.LayoutParams lp = mWindow.getAttributes();
675            lp.flags = applyFlags(lp.flags, flags, mask);
676            mWindow.setAttributes(lp);
677            mWindowManager.updateViewLayout(mWindow.getDecorView(), lp);
678        }
679    }
680
681    private boolean getSystemUiVisibilityFlagValue(int flag, boolean defaultValue) {
682        View v = mWindow == null ? null : mWindow.getDecorView();
683        return v == null ? defaultValue : (v.getSystemUiVisibility() & flag) != 0;
684    }
685
686    private void applySystemUiVisibilityFlags(int flags, int mask) {
687        View v = mWindow == null ? null : mWindow.getDecorView();
688        if (v != null) {
689            v.setSystemUiVisibility(applyFlags(v.getSystemUiVisibility(), flags, mask));
690        }
691    }
692
693    private int applyFlags(int oldFlags, int flags, int mask) {
694        return (oldFlags&~mask) | (flags&mask);
695    }
696
697    @Override
698    protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
699        super.dump(fd, pw, args);
700
701        pw.print(TAG + ": ");
702        if (mWindowToken == null) {
703            pw.println("stopped");
704        } else {
705            pw.println("running (token=" + mWindowToken + ")");
706        }
707        pw.println("  window: " + mWindow);
708        pw.print("  flags:");
709        if (isInteractive()) pw.print(" interactive");
710        if (isLowProfile()) pw.print(" lowprofile");
711        if (isFullscreen()) pw.print(" fullscreen");
712        if (isScreenBright()) pw.print(" bright");
713        pw.println();
714    }
715
716    private class DreamServiceWrapper extends IDreamService.Stub {
717        public void attach(final IBinder windowToken) {
718            mHandler.post(new Runnable() {
719                @Override
720                public void run() {
721                    DreamService.this.attach(windowToken);
722                }
723            });
724        }
725        public void detach() {
726            mHandler.post(new Runnable() {
727                @Override
728                public void run() {
729                    DreamService.this.detach();
730                }
731            });
732        }
733    }
734
735}
736