blob: 8b783e880bbcc4642ad8dbff832c6bed8b291a1c [file] [log] [blame]
Andrei Popescu6fa29582009-06-19 14:54:09 +01001/*
2 * Copyright (C) 2009 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 android.webkit;
18
19import android.content.Context;
Andrei Popescu64b86a12009-09-15 20:34:18 +010020import android.graphics.Bitmap;
21import android.graphics.BitmapFactory;
Andrei Popescu3c946a1a2009-07-03 08:20:53 +010022import android.media.MediaPlayer;
23import android.media.MediaPlayer.OnPreparedListener;
Andrei Popescu64b86a12009-09-15 20:34:18 +010024import android.media.MediaPlayer.OnCompletionListener;
25import android.media.MediaPlayer.OnErrorListener;
26import android.net.http.EventHandler;
27import android.net.http.Headers;
28import android.net.http.RequestHandle;
29import android.net.http.RequestQueue;
30import android.net.http.SslCertificate;
31import android.net.http.SslError;
Andrei Popescu6fa29582009-06-19 14:54:09 +010032import android.net.Uri;
33import android.os.Bundle;
34import android.os.Handler;
35import android.os.Looper;
36import android.os.Message;
37import android.util.Log;
Andrei Popescu3c946a1a2009-07-03 08:20:53 +010038import android.view.MotionEvent;
Andrei Popescubf385d72009-09-18 18:59:52 +010039import android.view.Gravity;
Andrei Popescu6fa29582009-06-19 14:54:09 +010040import android.view.View;
Andrei Popescu3c946a1a2009-07-03 08:20:53 +010041import android.view.ViewGroup;
Patrick Scott0a5ce012009-07-02 08:56:10 -040042import android.webkit.ViewManager.ChildView;
Andrei Popescu3c946a1a2009-07-03 08:20:53 +010043import android.widget.AbsoluteLayout;
Andrei Popescubf385d72009-09-18 18:59:52 +010044import android.widget.FrameLayout;
Andrei Popescu64b86a12009-09-15 20:34:18 +010045import android.widget.ImageView;
Andrei Popescu6fa29582009-06-19 14:54:09 +010046import android.widget.MediaController;
47import android.widget.VideoView;
48
Andrei Popescu64b86a12009-09-15 20:34:18 +010049import java.io.ByteArrayOutputStream;
50import java.io.IOException;
Andrei Popescu6fa29582009-06-19 14:54:09 +010051import java.util.HashMap;
Andrei Popescu290c34a2009-09-17 15:55:24 +010052import java.util.Map;
Andrei Popescu6fa29582009-06-19 14:54:09 +010053
54/**
Andrei Popescu3c946a1a2009-07-03 08:20:53 +010055 * <p>Proxy for HTML5 video views.
Andrei Popescu6fa29582009-06-19 14:54:09 +010056 */
Andrei Popescu290c34a2009-09-17 15:55:24 +010057class HTML5VideoViewProxy extends Handler
58 implements MediaPlayer.OnPreparedListener,
59 MediaPlayer.OnCompletionListener {
Andrei Popescu6fa29582009-06-19 14:54:09 +010060 // Logging tag.
61 private static final String LOGTAG = "HTML5VideoViewProxy";
62
Andrei Popescu3c946a1a2009-07-03 08:20:53 +010063 // Message Ids for WebCore thread -> UI thread communication.
Andrei Popescu6fa29582009-06-19 14:54:09 +010064 private static final int INIT = 100;
65 private static final int PLAY = 101;
Andrei Popescu64b86a12009-09-15 20:34:18 +010066 private static final int SET_POSTER = 102;
Andrei Popescu290c34a2009-09-17 15:55:24 +010067 private static final int SEEK = 103;
68 private static final int PAUSE = 104;
Andrei Popescu6fa29582009-06-19 14:54:09 +010069
Andrei Popescu64b86a12009-09-15 20:34:18 +010070 // Message Ids to be handled on the WebCore thread
Andrei Popescu290c34a2009-09-17 15:55:24 +010071 private static final int PREPARED = 200;
72 private static final int ENDED = 201;
Andrei Popescu64b86a12009-09-15 20:34:18 +010073
Andrei Popescu290c34a2009-09-17 15:55:24 +010074 // The C++ MediaPlayerPrivateAndroid object.
75 int mNativePointer;
Andrei Popescu64b86a12009-09-15 20:34:18 +010076 // The handler for WebCore thread messages;
77 private Handler mWebCoreHandler;
Andrei Popescu3c946a1a2009-07-03 08:20:53 +010078 // The WebView instance that created this view.
79 private WebView mWebView;
80 // The ChildView instance used by the ViewManager.
81 private ChildView mChildView;
Andrei Popescu64b86a12009-09-15 20:34:18 +010082 // The poster image to be shown when the video is not playing.
83 private ImageView mPosterView;
84 // The poster downloader.
85 private PosterDownloader mPosterDownloader;
Andrei Popescu290c34a2009-09-17 15:55:24 +010086 // The seek position.
87 private int mSeekPosition;
Andrei Popescu64b86a12009-09-15 20:34:18 +010088 // A helper class to control the playback. This executes on the UI thread!
89 private static final class VideoPlayer {
90 // The proxy that is currently playing (if any).
91 private static HTML5VideoViewProxy mCurrentProxy;
92 // The VideoView instance. This is a singleton for now, at least until
93 // http://b/issue?id=1973663 is fixed.
94 private static VideoView mVideoView;
Andrei Popescubf385d72009-09-18 18:59:52 +010095 // The progress view.
96 private static View mProgressView;
97 // The container for the progress view and video view
98 private static FrameLayout mLayout;
Andrei Popescu3c946a1a2009-07-03 08:20:53 +010099
Andrei Popescu64b86a12009-09-15 20:34:18 +0100100 private static final WebChromeClient.CustomViewCallback mCallback =
101 new WebChromeClient.CustomViewCallback() {
102 public void onCustomViewHidden() {
103 // At this point the videoview is pretty much destroyed.
104 // It listens to SurfaceHolder.Callback.SurfaceDestroyed event
105 // which happens when the video view is detached from its parent
106 // view. This happens in the WebChromeClient before this method
107 // is invoked.
108 mCurrentProxy.playbackEnded();
109 mCurrentProxy = null;
Andrei Popescubf385d72009-09-18 18:59:52 +0100110 mLayout.removeView(mVideoView);
Andrei Popescu64b86a12009-09-15 20:34:18 +0100111 mVideoView = null;
Andrei Popescubf385d72009-09-18 18:59:52 +0100112 if (mProgressView != null) {
113 mLayout.removeView(mProgressView);
114 mProgressView = null;
115 }
116 mLayout = null;
Andrei Popescu64b86a12009-09-15 20:34:18 +0100117 }
118 };
Andrei Popescu3c946a1a2009-07-03 08:20:53 +0100119
Andrei Popescu290c34a2009-09-17 15:55:24 +0100120 public static void play(String url, int time, HTML5VideoViewProxy proxy,
121 WebChromeClient client) {
Andrei Popescu64b86a12009-09-15 20:34:18 +0100122 if (mCurrentProxy != null) {
123 // Some other video is already playing. Notify the caller that its playback ended.
124 proxy.playbackEnded();
Andrei Popescu3c946a1a2009-07-03 08:20:53 +0100125 return;
126 }
Andrei Popescu64b86a12009-09-15 20:34:18 +0100127 mCurrentProxy = proxy;
Andrei Popescubf385d72009-09-18 18:59:52 +0100128 // Create a FrameLayout that will contain the VideoView and the
129 // progress view (if any).
130 mLayout = new FrameLayout(proxy.getContext());
131 FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(
132 ViewGroup.LayoutParams.WRAP_CONTENT,
133 ViewGroup.LayoutParams.WRAP_CONTENT,
134 Gravity.CENTER);
Andrei Popescu64b86a12009-09-15 20:34:18 +0100135 mVideoView = new VideoView(proxy.getContext());
136 mVideoView.setWillNotDraw(false);
137 mVideoView.setMediaController(new MediaController(proxy.getContext()));
138 mVideoView.setVideoURI(Uri.parse(url));
Andrei Popescu290c34a2009-09-17 15:55:24 +0100139 mVideoView.setOnCompletionListener(proxy);
140 mVideoView.setOnPreparedListener(proxy);
141 mVideoView.seekTo(time);
Andrei Popescubf385d72009-09-18 18:59:52 +0100142 mLayout.addView(mVideoView, layoutParams);
143 mProgressView = client.getVideoLoadingProgressView();
144 if (mProgressView != null) {
145 mLayout.addView(mProgressView, layoutParams);
146 mProgressView.setVisibility(View.VISIBLE);
147 }
148 mLayout.setVisibility(View.VISIBLE);
Andrei Popescu64b86a12009-09-15 20:34:18 +0100149 mVideoView.start();
Andrei Popescubf385d72009-09-18 18:59:52 +0100150 client.onShowCustomView(mLayout, mCallback);
Andrei Popescu3c946a1a2009-07-03 08:20:53 +0100151 }
Andrei Popescu290c34a2009-09-17 15:55:24 +0100152
153 public static void seek(int time, HTML5VideoViewProxy proxy) {
154 if (mCurrentProxy == proxy && time >= 0 && mVideoView != null) {
155 mVideoView.seekTo(time);
156 }
157 }
158
159 public static void pause(HTML5VideoViewProxy proxy) {
160 if (mCurrentProxy == proxy && mVideoView != null) {
161 mVideoView.pause();
162 }
163 }
Andrei Popescubf385d72009-09-18 18:59:52 +0100164
165 public static void onPrepared() {
166 if (mProgressView != null) {
167 mProgressView.setVisibility(View.GONE);
168 mLayout.removeView(mProgressView);
169 mProgressView = null;
170 }
171 }
Andrei Popescu290c34a2009-09-17 15:55:24 +0100172 }
173
174 // A bunch event listeners for our VideoView
175 // MediaPlayer.OnPreparedListener
176 public void onPrepared(MediaPlayer mp) {
Andrei Popescubf385d72009-09-18 18:59:52 +0100177 VideoPlayer.onPrepared();
Andrei Popescu290c34a2009-09-17 15:55:24 +0100178 Message msg = Message.obtain(mWebCoreHandler, PREPARED);
179 Map<String, Object> map = new HashMap<String, Object>();
180 map.put("dur", new Integer(mp.getDuration()));
181 map.put("width", new Integer(mp.getVideoWidth()));
182 map.put("height", new Integer(mp.getVideoHeight()));
183 msg.obj = map;
184 mWebCoreHandler.sendMessage(msg);
185 }
186
187 // MediaPlayer.OnCompletionListener;
188 public void onCompletion(MediaPlayer mp) {
189 playbackEnded();
190 }
191
192 public void playbackEnded() {
193 Message msg = Message.obtain(mWebCoreHandler, ENDED);
194 mWebCoreHandler.sendMessage(msg);
Andrei Popescu64b86a12009-09-15 20:34:18 +0100195 }
Andrei Popescu3c946a1a2009-07-03 08:20:53 +0100196
Andrei Popescu64b86a12009-09-15 20:34:18 +0100197 // Handler for the messages from WebCore thread to the UI thread.
198 @Override
199 public void handleMessage(Message msg) {
200 // This executes on the UI thread.
201 switch (msg.what) {
202 case INIT: {
203 mPosterView = new ImageView(mWebView.getContext());
Andrei Popescubf385d72009-09-18 18:59:52 +0100204 WebChromeClient client = mWebView.getWebChromeClient();
205 if (client != null) {
206 Bitmap poster = client.getDefaultVideoPoster();
207 if (poster != null) {
208 mPosterView.setImageBitmap(poster);
209 }
210 }
Andrei Popescu64b86a12009-09-15 20:34:18 +0100211 mChildView.mView = mPosterView;
212 break;
Andrei Popescu3c946a1a2009-07-03 08:20:53 +0100213 }
Andrei Popescu64b86a12009-09-15 20:34:18 +0100214 case PLAY: {
215 String url = (String) msg.obj;
216 WebChromeClient client = mWebView.getWebChromeClient();
217 if (client != null) {
Andrei Popescu290c34a2009-09-17 15:55:24 +0100218 VideoPlayer.play(url, mSeekPosition, this, client);
Andrei Popescu64b86a12009-09-15 20:34:18 +0100219 }
220 break;
221 }
222 case SET_POSTER: {
223 Bitmap poster = (Bitmap) msg.obj;
224 mPosterView.setImageBitmap(poster);
225 break;
226 }
Andrei Popescu290c34a2009-09-17 15:55:24 +0100227 case SEEK: {
228 Integer time = (Integer) msg.obj;
229 mSeekPosition = time;
230 VideoPlayer.seek(mSeekPosition, this);
231 break;
232 }
233 case PAUSE: {
234 VideoPlayer.pause(this);
235 break;
236 }
Andrei Popescu64b86a12009-09-15 20:34:18 +0100237 }
238 }
239
Andrei Popescu64b86a12009-09-15 20:34:18 +0100240 // Everything below this comment executes on the WebCore thread, except for
241 // the EventHandler methods, which are called on the network thread.
242
243 // A helper class that knows how to download posters
244 private static final class PosterDownloader implements EventHandler {
245 // The request queue. This is static as we have one queue for all posters.
246 private static RequestQueue mRequestQueue;
247 private static int mQueueRefCount = 0;
248 // The poster URL
249 private String mUrl;
250 // The proxy we're doing this for.
251 private final HTML5VideoViewProxy mProxy;
252 // The poster bytes. We only touch this on the network thread.
253 private ByteArrayOutputStream mPosterBytes;
254 // The request handle. We only touch this on the WebCore thread.
255 private RequestHandle mRequestHandle;
256 // The response status code.
257 private int mStatusCode;
258 // The response headers.
259 private Headers mHeaders;
260 // The handler to handle messages on the WebCore thread.
261 private Handler mHandler;
262
263 public PosterDownloader(String url, HTML5VideoViewProxy proxy) {
264 mUrl = url;
265 mProxy = proxy;
266 mHandler = new Handler();
267 }
268 // Start the download. Called on WebCore thread.
269 public void start() {
270 retainQueue();
271 mRequestHandle = mRequestQueue.queueRequest(mUrl, "GET", null, this, null, 0);
272 }
273 // Cancel the download if active and release the queue. Called on WebCore thread.
274 public void cancelAndReleaseQueue() {
275 if (mRequestHandle != null) {
276 mRequestHandle.cancel();
277 mRequestHandle = null;
278 }
279 releaseQueue();
280 }
281 // EventHandler methods. Executed on the network thread.
282 public void status(int major_version,
283 int minor_version,
284 int code,
285 String reason_phrase) {
286 mStatusCode = code;
Andrei Popescu3c946a1a2009-07-03 08:20:53 +0100287 }
288
Andrei Popescu64b86a12009-09-15 20:34:18 +0100289 public void headers(Headers headers) {
290 mHeaders = headers;
291 }
292
293 public void data(byte[] data, int len) {
294 if (mPosterBytes == null) {
295 mPosterBytes = new ByteArrayOutputStream();
296 }
297 mPosterBytes.write(data, 0, len);
298 }
299
300 public void endData() {
301 if (mStatusCode == 200) {
302 if (mPosterBytes.size() > 0) {
303 Bitmap poster = BitmapFactory.decodeByteArray(
304 mPosterBytes.toByteArray(), 0, mPosterBytes.size());
305 if (poster != null) {
306 mProxy.doSetPoster(poster);
307 }
308 }
309 cleanup();
310 } else if (mStatusCode >= 300 && mStatusCode < 400) {
311 // We have a redirect.
312 mUrl = mHeaders.getLocation();
313 if (mUrl != null) {
314 mHandler.post(new Runnable() {
315 public void run() {
316 if (mRequestHandle != null) {
317 mRequestHandle.setupRedirect(mUrl, mStatusCode,
318 new HashMap<String, String>());
319 }
320 }
321 });
322 }
323 }
324 }
325
326 public void certificate(SslCertificate certificate) {
327 // Don't care.
328 }
329
330 public void error(int id, String description) {
331 cleanup();
332 }
333
334 public boolean handleSslErrorRequest(SslError error) {
335 // Don't care. If this happens, data() will never be called so
336 // mPosterBytes will never be created, so no need to call cleanup.
337 return false;
338 }
339 // Tears down the poster bytes stream. Called on network thread.
340 private void cleanup() {
341 if (mPosterBytes != null) {
342 try {
343 mPosterBytes.close();
344 } catch (IOException ignored) {
345 // Ignored.
346 } finally {
347 mPosterBytes = null;
348 }
349 }
350 }
351
352 // Queue management methods. Called on WebCore thread.
353 private void retainQueue() {
354 if (mRequestQueue == null) {
355 mRequestQueue = new RequestQueue(mProxy.getContext());
356 }
357 mQueueRefCount++;
358 }
359
360 private void releaseQueue() {
361 if (mQueueRefCount == 0) {
362 return;
363 }
364 if (--mQueueRefCount == 0) {
365 mRequestQueue.shutdown();
366 mRequestQueue = null;
367 }
Andrei Popescu3c946a1a2009-07-03 08:20:53 +0100368 }
369 }
Andrei Popescu6fa29582009-06-19 14:54:09 +0100370
371 /**
372 * Private constructor.
Andrei Popescu290c34a2009-09-17 15:55:24 +0100373 * @param webView is the WebView that hosts the video.
374 * @param nativePtr is the C++ pointer to the MediaPlayerPrivate object.
Andrei Popescu6fa29582009-06-19 14:54:09 +0100375 */
Andrei Popescu290c34a2009-09-17 15:55:24 +0100376 private HTML5VideoViewProxy(WebView webView, int nativePtr) {
Andrei Popescu6fa29582009-06-19 14:54:09 +0100377 // This handler is for the main (UI) thread.
378 super(Looper.getMainLooper());
Andrei Popescu3c946a1a2009-07-03 08:20:53 +0100379 // Save the WebView object.
380 mWebView = webView;
Andrei Popescu290c34a2009-09-17 15:55:24 +0100381 // Save the native ptr
382 mNativePointer = nativePtr;
Andrei Popescu64b86a12009-09-15 20:34:18 +0100383 // create the message handler for this thread
384 createWebCoreHandler();
Andrei Popescu6fa29582009-06-19 14:54:09 +0100385 }
386
Andrei Popescu64b86a12009-09-15 20:34:18 +0100387 private void createWebCoreHandler() {
388 mWebCoreHandler = new Handler() {
389 @Override
390 public void handleMessage(Message msg) {
391 switch (msg.what) {
Andrei Popescu290c34a2009-09-17 15:55:24 +0100392 case PREPARED: {
393 Map<String, Object> map = (Map<String, Object>) msg.obj;
394 Integer duration = (Integer) map.get("dur");
395 Integer width = (Integer) map.get("width");
396 Integer height = (Integer) map.get("height");
397 nativeOnPrepared(duration.intValue(), width.intValue(),
398 height.intValue(), mNativePointer);
399 break;
400 }
401 case ENDED:
402 nativeOnEnded(mNativePointer);
403 break;
Andrei Popescu3c946a1a2009-07-03 08:20:53 +0100404 }
Andrei Popescu64b86a12009-09-15 20:34:18 +0100405 }
406 };
Andrei Popescu6fa29582009-06-19 14:54:09 +0100407 }
408
Andrei Popescu64b86a12009-09-15 20:34:18 +0100409 private void doSetPoster(Bitmap poster) {
410 if (poster == null) {
411 return;
412 }
413 // Send the bitmap over to the UI thread.
414 Message message = obtainMessage(SET_POSTER);
415 message.obj = poster;
416 sendMessage(message);
417 }
418
419 public Context getContext() {
420 return mWebView.getContext();
421 }
422
423 // The public methods below are all called from WebKit only.
Andrei Popescu6fa29582009-06-19 14:54:09 +0100424 /**
425 * Play a video stream.
426 * @param url is the URL of the video stream.
Andrei Popescu6fa29582009-06-19 14:54:09 +0100427 */
Andrei Popescu3c946a1a2009-07-03 08:20:53 +0100428 public void play(String url) {
Andrei Popescu64b86a12009-09-15 20:34:18 +0100429 if (url == null) {
430 return;
431 }
Andrei Popescu6fa29582009-06-19 14:54:09 +0100432 Message message = obtainMessage(PLAY);
Andrei Popescu64b86a12009-09-15 20:34:18 +0100433 message.obj = url;
Andrei Popescu6fa29582009-06-19 14:54:09 +0100434 sendMessage(message);
435 }
436
Andrei Popescu64b86a12009-09-15 20:34:18 +0100437 /**
Andrei Popescu290c34a2009-09-17 15:55:24 +0100438 * Seek into the video stream.
439 * @param time is the position in the video stream.
440 */
441 public void seek(int time) {
442 Message message = obtainMessage(SEEK);
443 message.obj = new Integer(time);
444 sendMessage(message);
445 }
446
447 /**
448 * Pause the playback.
449 */
450 public void pause() {
451 Message message = obtainMessage(PAUSE);
452 sendMessage(message);
453 }
454
455 /**
Andrei Popescu64b86a12009-09-15 20:34:18 +0100456 * Create the child view that will cary the poster.
457 */
Andrei Popescu3c946a1a2009-07-03 08:20:53 +0100458 public void createView() {
459 mChildView = mWebView.mViewManager.createView();
460 sendMessage(obtainMessage(INIT));
461 }
462
Andrei Popescu64b86a12009-09-15 20:34:18 +0100463 /**
464 * Attach the poster view.
465 * @param x, y are the screen coordinates where the poster should be hung.
466 * @param width, height denote the size of the poster.
467 */
Andrei Popescu3c946a1a2009-07-03 08:20:53 +0100468 public void attachView(int x, int y, int width, int height) {
469 if (mChildView == null) {
470 return;
Patrick Scott0a5ce012009-07-02 08:56:10 -0400471 }
Andrei Popescu3c946a1a2009-07-03 08:20:53 +0100472 mChildView.attachView(x, y, width, height);
Patrick Scott0a5ce012009-07-02 08:56:10 -0400473 }
474
Andrei Popescu64b86a12009-09-15 20:34:18 +0100475 /**
476 * Remove the child view and, thus, the poster.
477 */
Andrei Popescu3c946a1a2009-07-03 08:20:53 +0100478 public void removeView() {
479 if (mChildView == null) {
480 return;
481 }
482 mChildView.removeView();
Andrei Popescu64b86a12009-09-15 20:34:18 +0100483 // This is called by the C++ MediaPlayerPrivate dtor.
484 // Cancel any active poster download.
485 if (mPosterDownloader != null) {
486 mPosterDownloader.cancelAndReleaseQueue();
487 }
488 }
489
490 /**
491 * Load the poster image.
492 * @param url is the URL of the poster image.
493 */
494 public void loadPoster(String url) {
495 if (url == null) {
496 return;
497 }
498 // Cancel any active poster download.
499 if (mPosterDownloader != null) {
500 mPosterDownloader.cancelAndReleaseQueue();
501 }
502 // Load the poster asynchronously
503 mPosterDownloader = new PosterDownloader(url, this);
504 mPosterDownloader.start();
Patrick Scott0a5ce012009-07-02 08:56:10 -0400505 }
506
Andrei Popescu6fa29582009-06-19 14:54:09 +0100507 /**
Andrei Popescu3c946a1a2009-07-03 08:20:53 +0100508 * The factory for HTML5VideoViewProxy instances.
Andrei Popescu6fa29582009-06-19 14:54:09 +0100509 * @param webViewCore is the WebViewCore that is requesting the proxy.
510 *
Andrei Popescu3c946a1a2009-07-03 08:20:53 +0100511 * @return a new HTML5VideoViewProxy object.
Andrei Popescu6fa29582009-06-19 14:54:09 +0100512 */
Andrei Popescu290c34a2009-09-17 15:55:24 +0100513 public static HTML5VideoViewProxy getInstance(WebViewCore webViewCore, int nativePtr) {
514 return new HTML5VideoViewProxy(webViewCore.getWebView(), nativePtr);
Andrei Popescu6fa29582009-06-19 14:54:09 +0100515 }
Andrei Popescu290c34a2009-09-17 15:55:24 +0100516
517 private native void nativeOnPrepared(int duration, int width, int height, int nativePointer);
518 private native void nativeOnEnded(int nativePointer);
Andrei Popescu6fa29582009-06-19 14:54:09 +0100519}