download.c

Go to the documentation of this file.
00001 /*
00002  * File Name: download.c
00003  */
00004 
00005 /*
00006  * This file is part of erbrowser.
00007  *
00008  * erbrowser is free software: you can redistribute it and/or modify
00009  * it under the terms of the GNU General Public License as published by
00010  * the Free Software Foundation, either version 2 of the License, or
00011  * (at your option) any later version.
00012  *
00013  * erbrowser is distributed in the hope that it will be useful,
00014  * but WITHOUT ANY WARRANTY; without even the implied warranty of
00015  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
00016  * GNU General Public License for more details.
00017  *
00018  * You should have received a copy of the GNU General Public License
00019  * along with this program. If not, see <http://www.gnu.org/licenses/>.
00020  */
00021 
00022 /**
00023  * Copyright (C) 2006, 2007 Apple Inc.
00024  * Copyright (C) 2007 Alp Toker <alp@atoker.com>
00025  * Copyright (C) 2008 Collabora Ltd.
00026  * Copyright (C) 2009 iRex Technologies B.V.
00027  * All rights reserved.
00028  */
00029 
00030 //----------------------------------------------------------------------------
00031 // Include Files
00032 //----------------------------------------------------------------------------
00033 
00034 #include "config.h"
00035 
00036 // system include files, between < >
00037 #include <glib.h>
00038 #include <gtk/gtk.h>
00039 #include <webkit/webkit.h>
00040 
00041 // ereader include files, between < >
00042 #include <libermetadb/ermetadb.h>
00043 
00044 // local include files, between " "
00045 #include "log.h"
00046 #include "download.h"
00047 #include "i18n.h"
00048 #include "ipc.h"
00049 #include "view.h"
00050 
00051 #define UNUSED(x) (void)(x)
00052 
00053 
00054 //----------------------------------------------------------------------------
00055 // Type Declarations
00056 //----------------------------------------------------------------------------
00057 
00058 typedef struct _DowloadDialog {
00059     BrowserWindow *browser_window;
00060     WebKitDownload *download;
00061     GtkWidget *window;
00062     GtkWidget *progress_bar;         
00063     GtkWidget *button;
00064     GtkWidget *uri_label;
00065 } DownloadDialog;
00066 
00067 
00068 //----------------------------------------------------------------------------
00069 // Global Constants
00070 //----------------------------------------------------------------------------
00071 
00072 const gchar *download_folder   = "Downloads";
00073 const gint   box_spacing       = 12;  // in pixels
00074 const gint   uri_label_width   = 500; // in pixels
00075 const gint   progress_interval = 2;   // in seconds
00076 
00077 
00078 //----------------------------------------------------------------------------
00079 // Static Variables
00080 //---------------------------------------------------------------------------- 
00081 
00082 static guint g_update_progress = 0;
00083 
00084 
00085 //============================================================================
00086 // Local Function Definitions
00087 //============================================================================
00088 
00089 static void download_error_cb       (WebKitDownload *download,
00090                                      guint domain,
00091                                      WebKitDownloadError error,
00092                                      const gchar *message,
00093                                      DownloadDialog *dialog);
00094 
00095 static void download_button_cancel_cb(GtkWidget *button, DownloadDialog *dialog);
00096 
00097 static void download_set_ok         (DownloadDialog *dialog, const gchar *message);
00098 
00099 static void download_destroy_cb     (GtkWidget *widget, DownloadDialog *dialog);
00100 
00101 static void download_response_cb    (GtkWidget *window, gint response, DownloadDialog *dialog);
00102 
00103 static gboolean is_local_file       (const gchar *uri);
00104 
00105 static gboolean update_progress_cb  (gpointer data);
00106 
00107 static void add_file_to_metadata    (const gchar *dirname, const gchar *filename);
00108 
00109 static void add_folder_to_metadata  (const gchar *dirname, const gchar *foldername);
00110 
00111 static void download_status_changed_cb(WebKitDownload *download, GParamSpec *pspec, DownloadDialog *dialog);
00112 
00113 static gchar *make_unique_filename(const gchar *dirname, const gchar *fn);
00114 
00115 
00116 //============================================================================
00117 // Functions Implementation
00118 //============================================================================
00119 
00120 gboolean download_requested_cb(WebKitWebView *web_view,
00121                                WebKitDownload *download,
00122                                BrowserWindow *browser_window)
00123 {
00124     LOGPRINTF("entry window [%p]", browser_window);
00125     
00126     DownloadDialog *dialog = NULL;
00127 
00128     if (is_local_file(webkit_download_get_uri(download))) 
00129     {
00130         LOGPRINTF("You could open the file [%s] without transferring it", webkit_download_get_uri(download));
00131         
00132         gchar *title;
00133         title = g_strdup_printf(_("Unable to open %s"), webkit_download_get_uri(download));
00134         view_show_error(title, _("The web browser is unable to display the document. "
00135                                  "Since it is on the SD card, you can open it from Documents."));
00136         g_free(title);
00137         return FALSE;
00138     }
00139     
00140     if (!g_mountpoint)
00141     {
00142         ERRORPRINTF("No mountpoint, cannot download");
00143         
00144         // Webkit will continue loading which ends in an error
00145         // so stop here. This may be an error in Webkit. 
00146         webkit_web_view_stop_loading(web_view);
00147 
00148         GtkWidget *widget;    
00149         widget = gtk_message_dialog_new(GTK_WINDOW(browser_window->window),
00150                                     GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT,
00151                                     GTK_MESSAGE_WARNING,
00152                                     GTK_BUTTONS_OK,
00153                                     _("The download cannot be started because there is no SD card in this device. "
00154                                       "Please insert a card and try again."));
00155     
00156         gtk_dialog_run(GTK_DIALOG(widget));
00157         gtk_widget_destroy(widget);
00158             
00159         return FALSE;
00160     }
00161     
00162 
00163     // check uri
00164     gchar *extension = g_strrstr(webkit_download_get_suggested_filename(download),".");
00165     if (extension && (strncmp(extension, ".acsm", 5) == 0))
00166     {
00167         LOGPRINTF("found .acsm file, download and open with Adobe Fullfilment");
00168         browser_window->open_download = TRUE;
00169     }
00170         
00171     // create path for download
00172     gchar *download_path;
00173 #if MACHINE_IS_DR800SG || MACHINE_IS_DR800SW
00174     if (browser_window->open_download)
00175     {
00176         LOGPRINTF("open_download, set temp path");
00177         download_path = g_strdup(g_get_tmp_dir());
00178     }
00179     else
00180 #endif        
00181     {
00182         download_path = g_build_path("/", g_mountpoint, download_folder, NULL);
00183     
00184         // create and add download directory when necessary
00185         if (!g_file_test(download_path, G_FILE_TEST_IS_DIR))
00186         {
00187             if (g_mkdir_with_parents(download_path, 0777) >= 0)
00188             {
00189                 add_folder_to_metadata(g_mountpoint, download_folder);
00190             }
00191         }
00192         
00193         if (!g_file_test(download_path, G_FILE_TEST_IS_DIR))
00194         {
00195             ERRORPRINTF("Failed to create %s, error [%d] [%s]", download_path, errno, g_strerror(errno));
00196 
00197             // Webkit will continue loading which ends in an error
00198             // so stop here. This may be an error in Webkit. 
00199             webkit_web_view_stop_loading(web_view);
00200             
00201             GtkWidget *widget;    
00202             widget = gtk_message_dialog_new(GTK_WINDOW(browser_window->window),
00203                                         GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT,
00204                                         GTK_MESSAGE_WARNING,
00205                                         GTK_BUTTONS_OK,
00206                                         _("An error has occurred while saving the file to the SD card. It is possible the card is full or locked."));
00207 
00208             gtk_dialog_run(GTK_DIALOG(widget));
00209             gtk_widget_destroy(widget);
00210             
00211             g_free(download_path);
00212             return FALSE;
00213         }
00214     }
00215     
00216     // create download filename
00217     gchar *filename = make_unique_filename(download_path, webkit_download_get_suggested_filename(download));
00218 
00219     dialog = g_new0(DownloadDialog, 1);
00220     dialog->browser_window = browser_window;
00221     dialog->download       = g_object_ref(download);
00222     
00223     // object hierarchy:
00224     //     dialog
00225     //       |    
00226     dialog->window = gtk_dialog_new_with_buttons(_("Download Progress"),
00227                          GTK_WINDOW(browser_window->window), 
00228                          GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT | GTK_DIALOG_NO_SEPARATOR,
00229                          NULL);
00230     gtk_box_set_spacing(GTK_BOX(GTK_DIALOG(dialog->window)->vbox), box_spacing);
00231     //       |
00232     //       |-- uri_label (label)
00233     //       |    
00234     gchar *message;
00235     dialog->uri_label = gtk_label_new(NULL);
00236     message = g_strdup_printf(_("Downloading '%s'..."), filename);
00237     gtk_label_set_label(GTK_LABEL(dialog->uri_label), message);
00238     gtk_label_set_line_wrap(GTK_LABEL(dialog->uri_label), TRUE);
00239     gtk_widget_set_size_request(dialog->uri_label, uri_label_width, -1);
00240     gtk_box_pack_start(GTK_BOX(GTK_DIALOG(dialog->window)->vbox), dialog->uri_label, FALSE, FALSE, 0);
00241     g_free(message);
00242     //       |
00243     //       |-- progress_bar (progress bar)
00244     //       |    
00245     dialog->progress_bar = gtk_progress_bar_new();         
00246     gtk_progress_bar_set_orientation(GTK_PROGRESS_BAR(dialog->progress_bar), GTK_PROGRESS_LEFT_TO_RIGHT);
00247     gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(dialog->progress_bar), 0.0); 
00248     gtk_box_pack_start(GTK_BOX(GTK_DIALOG(dialog->window)->vbox), dialog->progress_bar, FALSE, FALSE, 0);
00249     //       |
00250     //       |-- button_box (hbutton box)
00251     //       |    
00252     GtkWidget *button_box = gtk_hbutton_box_new();
00253     GTK_DIALOG(dialog->window)->action_area = button_box;
00254     gtk_button_box_set_layout(GTK_BUTTON_BOX(button_box), GTK_BUTTONBOX_END);
00255     dialog->button = gtk_button_new_from_stock(GTK_STOCK_CANCEL);
00256     g_signal_connect(G_OBJECT(dialog->button), "clicked", G_CALLBACK(download_button_cancel_cb), dialog);
00257     gtk_box_pack_start(GTK_BOX(button_box), dialog->button, FALSE, FALSE, 0);
00258     gtk_box_pack_start(GTK_BOX(GTK_DIALOG(dialog->window)->vbox), button_box, FALSE, FALSE, 0);
00259     
00260     gtk_widget_show_all(GTK_WIDGET(dialog->window));
00261 
00262     // create URI for download target
00263     gchar *local_uri = NULL;
00264     local_uri = g_strconcat("file://", filename, NULL);
00265     
00266     LOGPRINTF("Download file to [%s]", local_uri);
00267     
00268     webkit_download_set_destination_uri(dialog->download, local_uri);
00269     g_free(local_uri);
00270     g_free(download_path);
00271     g_free(filename);
00272     
00273     g_signal_connect(dialog->window, "close",               G_CALLBACK(download_destroy_cb), dialog);
00274     g_signal_connect(dialog->window, "response",            G_CALLBACK(download_response_cb), dialog);
00275     
00276     g_signal_connect(G_OBJECT(download), "error",           G_CALLBACK(download_error_cb), dialog);
00277     g_signal_connect(G_OBJECT(download), "notify::status",  G_CALLBACK(download_status_changed_cb), dialog);
00278     
00279     // schedule next update after n seconds
00280     g_update_progress = g_timeout_add_seconds(progress_interval, update_progress_cb, dialog);
00281     
00282     ipc_sys_bg_busy(TRUE);
00283 
00284     return TRUE;
00285 }
00286 
00287 
00288 //============================================================================
00289 // Local Functions Implementation
00290 //============================================================================
00291 
00292 static void download_status_changed_cb(WebKitDownload *download, GParamSpec *pspec, DownloadDialog *dialog)
00293 {
00294     UNUSED(download);
00295     UNUSED(pspec);
00296     LOGPRINTF("entry");
00297     
00298     WebKitDownloadStatus status = webkit_download_get_status(dialog->download);
00299     
00300     switch (status)        
00301     {
00302     case WEBKIT_DOWNLOAD_STATUS_CREATED:
00303         LOGPRINTF("status WEBKIT_DOWNLOAD_STATUS_CREATED");
00304         break;
00305 
00306     case WEBKIT_DOWNLOAD_STATUS_STARTED:
00307         LOGPRINTF("status WEBKIT_DOWNLOAD_STATUS_STARTED");
00308         update_progress_cb(dialog);
00309         break;
00310 
00311     case WEBKIT_DOWNLOAD_STATUS_FINISHED:
00312         {
00313             LOGPRINTF("status WEBKIT_DOWNLOAD_STATUS_FINISHED");
00314             
00315             const gchar *uri = webkit_download_get_destination_uri(dialog->download);
00316             const gchar *filename = NULL;
00317             if (is_local_file(uri))
00318             {
00319                 filename = uri + strlen("file://");
00320             }
00321             
00322             if (filename)
00323             {
00324 #if MACHINE_IS_DR800SG || MACHINE_IS_DR800SW
00325                 if (dialog->browser_window->open_download)
00326                 {
00327                     gtk_dialog_response(GTK_DIALOG(dialog->window), GTK_RESPONSE_CLOSE);
00328 
00329                     gchar *cmd_line = g_strdup_printf("/usr/bin/adobe-fulfill --acsm %s", filename);
00330                     LOGPRINTF("auto open: start task [%s]", cmd_line);
00331                     ipc_sys_start_task(cmd_line, NULL, "Adobe DRM fullfillment", NULL, NULL);
00332                     g_free(cmd_line);
00333                 }
00334                 else
00335 #endif                    
00336                 {
00337                     gchar *dirname = g_path_get_dirname(filename);
00338                     gchar *basename = g_path_get_basename(filename);
00339                 
00340                     add_file_to_metadata(dirname, basename);
00341                     
00342                     // TODO: fix this properly, interface with indexer
00343                     gchar *extension = g_strrstr(basename, ".");
00344                     gchar *tag = NULL;
00345                     if (extension && (strncasecmp(extension, ".np", 3) == 0))
00346                     {
00347                         tag = _("News");
00348                     }
00349                     else
00350                     {
00351                         tag = _("Books");
00352                     }    
00353                     
00354                     gchar *message = g_strdup_printf(
00355                                             _("'%s' has successfully downloaded and can be found in %s."),
00356                                             basename, tag);
00357                     download_set_ok(dialog, message);
00358                     
00359                     g_free(message);
00360                     g_free(dirname);
00361                     g_free(basename);
00362                 }
00363             }
00364             else
00365             {
00366                 LOGPRINTF("Failed to get uri of downloaded file");
00367             }
00368             
00369             if (g_update_progress)
00370             {
00371                 LOGPRINTF("stop update progress timer");
00372                 g_source_remove(g_update_progress);
00373                 g_update_progress = 0;
00374             }
00375             
00376             ipc_sys_bg_busy(FALSE);
00377         }
00378         break;
00379 
00380     case WEBKIT_DOWNLOAD_STATUS_CANCELLED:
00381         LOGPRINTF("status WEBKIT_DOWNLOAD_STATUS_CANCELLED");
00382         WARNPRINTF("download state [%d]", status);
00383         break;
00384                 
00385     case WEBKIT_DOWNLOAD_STATUS_ERROR:
00386         // should be handled by error handler
00387         LOGPRINTF("status WEBKIT_DOWNLOAD_STATUS_ERROR");
00388         WARNPRINTF("download state [%d]", status);
00389         break;
00390                 
00391     default:
00392         WARNPRINTF("unknown download state [%d]", status);
00393         break;
00394     }
00395     
00396     LOGPRINTF("leave");
00397 }
00398 
00399 
00400 static void download_error_cb(WebKitDownload *download,
00401                                   guint domain,
00402                                   WebKitDownloadError error,
00403                                   const gchar *message,
00404                                   DownloadDialog *dialog)
00405 {
00406     UNUSED(download);
00407     UNUSED(domain);
00408     UNUSED(message);    
00409     LOGPRINTF("entry: error [%d] message [%s]", error, message);
00410 
00411     switch (error)
00412     {
00413     case WEBKIT_DOWNLOAD_ERROR_CANCELLED_BY_USER:            
00414         download_set_ok(dialog, _("The download has been canceled."));
00415         break;
00416                 
00417     case WEBKIT_DOWNLOAD_ERROR_DESTINATION:
00418         download_set_ok(dialog, _("An error has occurred while saving the file to the SD card. It is possible the card is full or locked."));
00419         break;
00420                 
00421     case WEBKIT_DOWNLOAD_ERROR_NETWORK:
00422         download_set_ok(dialog, _("A network error has occurred and the download was not completed. Please check your network connection and try again."));
00423         break;
00424                 
00425     default:
00426         WARNPRINTF("unknown download error [%d]", error);
00427         break;
00428     }
00429     
00430     if (g_update_progress)
00431     {
00432         LOGPRINTF("stop update progress timer");
00433         g_source_remove(g_update_progress);
00434         g_update_progress = 0;
00435     }
00436     
00437     ipc_sys_bg_busy(FALSE);
00438 }
00439 
00440 
00441 static gboolean update_progress_cb(gpointer data)
00442 {
00443     DownloadDialog *dialog = (DownloadDialog*) data;
00444     if (dialog && GTK_IS_PROGRESS_BAR(dialog->progress_bar)) 
00445     {
00446         gdouble progress = webkit_download_get_progress(dialog->download);
00447         LOGPRINTF("progress [%f]", progress);
00448         gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(dialog->progress_bar), progress);
00449     }
00450     
00451     // please call again
00452     return TRUE;
00453 }
00454 
00455 
00456 static void add_file_to_metadata(const gchar *dirname, const gchar *filename)
00457 {
00458     LOGPRINTF("entry");
00459 
00460     struct stat stats;
00461     gchar *buffer = g_strdup_printf("%s/%s", dirname, filename);
00462     int error = stat(buffer, &stats);
00463     g_free(buffer);
00464     if (error == -1)
00465     {
00466         ERRORPRINTF("Failed to add metadata for %s, error [%d] [%s]", filename, errno, g_strerror(errno));
00467         return;
00468     }
00469 
00470     // TODO: fix this properly, interface with indexer
00471     gchar *extension = g_strrstr(filename, ".");
00472     const char* tag = "book";
00473     if (extension && (strncasecmp(extension, ".np", 3) == 0))
00474     {
00475         tag = "news";
00476     }
00477 
00478     // TODO remove hardcoded path
00479     erMetadb metadb = ermetadb_global_open("/media/mmcblk0p1", FALSE);
00480     if (metadb == NULL) {
00481         ERRORPRINTF("Opening metadb: returned");
00482         return;
00483     }   
00484     int ret = ermetadb_global_add_file(metadb, dirname, filename, stats.st_size, stats.st_mtime, filename, NULL, tag);
00485     if (ret != ER_OK) {
00486         ERRORPRINTF("adding document, returned %d", ret);
00487     }
00488 
00489     ermetadb_close(metadb);
00490 }
00491 
00492 
00493 static void add_folder_to_metadata(const gchar *dirname, const gchar *foldername) 
00494 {
00495     LOGPRINTF("entry");
00496     
00497     // TODO remove hardcoded path
00498     erMetadb metadb = ermetadb_global_open("/media/mmcblk0p1", FALSE);
00499     if (metadb == NULL) {
00500         ERRORPRINTF("Opening metadb: returned");
00501         return;
00502     }
00503     gint64 time_modified = 0;
00504     int ret = ermetadb_global_add_folder(metadb, dirname, foldername, time_modified);
00505     if (ret != ER_OK) {
00506         ERRORPRINTF("adding document, returned %d", ret);
00507     }
00508 
00509     ermetadb_close(metadb);
00510 }
00511 
00512 
00513 static void download_button_cancel_cb(GtkWidget *button, DownloadDialog *dialog)
00514 {
00515     UNUSED(button);
00516     LOGPRINTF("entry");
00517 
00518     gint rc;
00519     GtkWidget *widget = NULL;
00520     gchar *message;
00521     const gchar *filename = webkit_download_get_suggested_filename(dialog->download);
00522     
00523     message = g_strdup_printf( _("Do you wish to stop downloading '%s'?"), filename);
00524     
00525     widget = gtk_message_dialog_new(GTK_WINDOW(dialog->window),
00526                                     GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT,
00527                                     GTK_MESSAGE_WARNING,
00528                                     GTK_BUTTONS_YES_NO,
00529                                     message);
00530     rc = gtk_dialog_run(GTK_DIALOG(widget));
00531       
00532     if (rc == GTK_RESPONSE_OK ||
00533         rc == GTK_RESPONSE_YES )
00534     {
00535         LOGPRINTF("User cancelled download");
00536         webkit_download_cancel(dialog->download);
00537     }
00538     
00539     gtk_widget_destroy(widget);
00540     g_free(message);
00541 }
00542 
00543 
00544 static void download_set_ok(DownloadDialog *dialog, const gchar *message)
00545 {
00546     LOGPRINTF("entry");
00547     
00548     gtk_label_set_label(GTK_LABEL(dialog->uri_label), message);
00549     gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(dialog->progress_bar), 1.0);
00550     gtk_button_set_label(GTK_BUTTON(dialog->button), GTK_STOCK_OK);
00551     
00552     g_signal_handlers_disconnect_by_func(dialog->button, download_button_cancel_cb, dialog);
00553     g_signal_handlers_disconnect_by_func(dialog->button, download_response_cb, dialog);
00554     g_signal_connect(G_OBJECT(dialog->button), "clicked", G_CALLBACK(download_destroy_cb), dialog);
00555 }
00556 
00557 
00558 static void download_response_cb(GtkWidget *window, gint response, DownloadDialog *dialog)
00559 {
00560     UNUSED(window);    
00561     LOGPRINTF("entry: %d", response);
00562 
00563     switch (response)
00564     {
00565     case GTK_RESPONSE_CANCEL:
00566     case GTK_RESPONSE_DELETE_EVENT:
00567         webkit_download_cancel(dialog->download);
00568         break;
00569 
00570     case GTK_RESPONSE_CLOSE:
00571         gtk_widget_destroy(dialog->window);
00572         dialog->window = NULL;
00573         break;
00574 
00575     default:
00576         // already cleaned up
00577         break;
00578     }
00579 }
00580 
00581 
00582 static void download_destroy_cb(GtkWidget *widget, DownloadDialog *dialog)
00583 {
00584     UNUSED(widget);
00585     LOGPRINTF("entry");
00586     
00587     if (dialog->window)
00588     {
00589         gtk_widget_destroy(dialog->window);
00590         dialog->window = NULL;
00591     }
00592 
00593     g_free(dialog);
00594     dialog = NULL;
00595 }
00596 
00597 
00598 static gboolean is_local_file(const gchar *uri)
00599 {
00600     return uri && g_str_has_prefix(uri, "file://");
00601 }
00602 
00603 
00604 static gchar *make_unique_filename(const gchar *dirname, const gchar *fn)
00605 {
00606     LOGPRINTF("entry [%s]", fn);
00607 
00608     gchar *filename = g_strdup_printf("%s/%s", dirname, fn);
00609     gchar *name = g_strdup(fn);
00610     gchar *dot = g_strrstr(name, ".");
00611     gchar *ext = NULL;
00612     if (dot)
00613     {
00614         ext = g_strdup(dot+1);
00615         *dot = '\0'; // terminate name
00616     }
00617         
00618     int i = 0;
00619     while (g_file_test(filename, G_FILE_TEST_IS_REGULAR | G_FILE_TEST_EXISTS) && i < 99)
00620     {
00621         g_free(filename);
00622         if (ext)
00623         {
00624             filename = g_strdup_printf("%s/%s_%02d.%s", dirname, name, ++i, ext);
00625         }
00626         else
00627         {
00628             filename = g_strdup_printf("%s/%s_%02d", dirname, name, ++i);
00629         }
00630     }
00631     
00632     g_free(ext);
00633     g_free(name);
00634     
00635     LOGPRINTF("leave [%s]", filename);
00636     
00637     return filename;
00638 }
Generated by  doxygen 1.6.2-20100208