Создавайте приложения с помощью Sencha Ext JS

Цель этого документа — помочь вам начать создание приложений Chrome с помощью платформы Sencha Ext JS . Чтобы достичь этой цели, мы углубимся в приложение медиаплеера, созданное Sencha. Исходный код и документация по API доступны на GitHub.

Это приложение обнаруживает доступные медиа-серверы пользователя, включая мультимедийные устройства, подключенные к компьютеру, и программное обеспечение, которое управляет мультимедиа по сети. Пользователи могут просматривать медиафайлы, воспроизводить их по сети или сохранять в автономном режиме.

Вот ключевые вещи, которые вам необходимо сделать для создания приложения медиаплеера с использованием Sencha Ext JS:

  • Создайте манифест, manifest.json .
  • Создайте страницу события , background.js .
  • Логика приложения- песочницы .
  • Обмен данными между приложением Chrome и файлами в песочнице.
  • Откройте для себя медиа-серверы.
  • Исследуйте и воспроизводите медиа.
  • Сохраняйте медиа в автономном режиме.

Создать манифест

Всем приложениям Chrome требуется файл манифеста , содержащий информацию, необходимую Chrome для запуска приложений. Как указано в манифесте, приложение медиаплеера имеет статус «offline_enabled»; Медиа-ресурсы можно сохранять локально, получать к ним доступ и воспроизводить независимо от подключения.

Поле «песочница» используется для изолирования основной логики приложения в уникальном источнике. Весь изолированный контент не подпадает под действие Политики безопасности контента приложений Chrome, но не имеет прямого доступа к API приложений Chrome. Манифест также включает разрешение «сокета»; приложение медиаплеера использует API сокетов для подключения к медиасерверу по сети.

{
    "name": "Video Player",
    "description": "Features network media discovery and playlist management",
    "version": "1.0.0",
    "manifest_version": 2,
    "offline_enabled": true,
    "app": {
        "background": {
            "scripts": [
                "background.js"
            ]
        }
    },
    ...

    "sandbox": {
        "pages": ["sandbox.html"]
    },
    "permissions": [
        "experimental",
        "http://*/*",
        "unlimitedStorage",
        {
            "socket": [
                "tcp-connect",
                "udp-send-to",
                "udp-bind"
            ]
        }
    ]
}

Создать страницу мероприятия

Всем приложениям Chrome для запуска приложения требуется background.js Главная страница медиаплеера index.html открывается в окне указанных размеров:

chrome.app.runtime.onLaunched.addListener(function(launchData) {
    var opt = {
        width: 1000,
        height: 700
    };

    chrome.app.window.create('index.html', opt, function (win) {
        win.launchData = launchData;
    });

});

Логика приложения-песочницы

Приложения Chrome работают в контролируемой среде, в которой применяется строгая Политика безопасности контента (CSP) . Приложению медиаплеера требуются более высокие привилегии для рендеринга компонентов Ext JS. Чтобы соответствовать CSP и выполнять логику приложения, на главной странице приложения index.html создается iframe, который действует как среда песочницы:

<iframe id="sandbox-frame" sandbox="allow-scripts" src="sandbox.html"></iframe>

iframe указывает на файл sandbox.html , который включает файлы, необходимые для приложения Ext JS:

<html>
<head>
    <link rel="stylesheet" type="text/css" href="resources/css/app.css" />'
    <script src="sdk/ext-all-dev.js"></script>'
    <script src="lib/ext/data/PostMessage.js"></script>'
    <script src="lib/ChromeProxy.js"></script>'
    <script src="app.js"></script>
</head>
<body></body>
</html>

Скрипт app.js выполняет весь код Ext JS и отображает представления медиаплеера. Поскольку этот скрипт находится в песочнице, он не может напрямую получить доступ к API-интерфейсам приложений Chrome. Связь между app.js и файлами, не изолированными в песочнице, осуществляется с помощью HTML5 Post Message API .

Обмен файлами

Чтобы приложение медиаплеера могло получить доступ к API приложений Chrome, например, запросить в сети медиасерверы, app.js отправляет сообщения в index.js . В отличие от изолированного app.js , index.js может напрямую обращаться к API-интерфейсам приложений Chrome.

index.js создает iframe:

var iframe = document.getElementById('sandbox-frame');

iframeWindow = iframe.contentWindow;

И прослушивает сообщения из файлов песочницы:

window.addEventListener('message', function(e) {
    var data= e.data,
        key = data.key;

    console.log('[index.js] Post Message received with key ' + key);

    switch (key) {
        case 'extension-baseurl':
            extensionBaseUrl(data);
            break;

        case 'upnp-discover':
            upnpDiscover(data);
            break;

        case 'upnp-browse':
            upnpBrowse(data);
            break;

        case 'play-media':
            playMedia(data);
            break;

        case 'download-media':
            downloadMedia(data);
            break;

        case 'cancel-download':
            cancelDownload(data);
            break;

        default:
            console.log('[index.js] unidentified key for Post Message: "' + key + '"');
    }
}, false);

В следующем примере app.js отправляет сообщение index.js с запросом ключа «extension-baseurl»:

Ext.data.PostMessage.request({
    key: 'extension-baseurl',
    success: function(data) {
        //...
    }
});

index.js получает запрос, присваивает результат и отвечает, отправляя обратно базовый URL-адрес:

function extensionBaseUrl(data) {
    data.result = chrome.extension.getURL('/');
    iframeWindow.postMessage(data, '*');
}

Откройте для себя медиа-серверы

Для обнаружения медиа-серверов требуется очень многое. На высоком уровне рабочий процесс обнаружения инициируется действием пользователя по поиску доступных медиасерверов. Контроллер MediaServer отправляет сообщение в index.js ; index.js прослушивает это сообщение и при его получении вызывает Upnp.js.

Upnp library использует API-интерфейс сокета приложения Chrome для подключения приложения медиаплеера к любым обнаруженным медиа-серверам и получения медиа-данных с медиа-сервера. Upnp.js также использует Soapclient.js для анализа данных медиасервера. В оставшейся части этого раздела этот рабочий процесс описан более подробно.

Опубликовать сообщение

Когда пользователь нажимает кнопку «Медиа-серверы» в центре приложения медиаплеера, MediaServers.js вызывает discoverServers() . Эта функция сначала проверяет наличие невыполненных запросов на обнаружение и, если это правда, прерывает их, чтобы можно было инициировать новый запрос. Затем контроллер отправляет сообщение в index.js с ключом upnp-discovery и двумя прослушивателями обратного вызова:

me.activeDiscoverRequest = Ext.data.PostMessage.request({
    key: 'upnp-discover',
    success: function(data) {
        var items = [];
        delete me.activeDiscoverRequest;

        if (serversGraph.isDestroyed) {
            return;
        }

        mainBtn.isLoading = false;
        mainBtn.removeCls('pop-in');
        mainBtn.setIconCls('ico-server');
        mainBtn.setText('Media Servers');

        //add servers
        Ext.each(data, function(server) {
            var icon,
                urlBase = server.urlBase;

            if (urlBase) {
                if (urlBase.substr(urlBase.length-1, 1) === '/'){
                        urlBase = urlBase.substr(0, urlBase.length-1);
                }
            }

            if (server.icons && server.icons.length) {
                if (server.icons[1]) {
                    icon = server.icons[1].url;
                }
                else {
                    icon = server.icons[0].url;
                }

                icon = urlBase + icon;
            }

            items.push({
                itemId: server.id,
                text: server.friendlyName,
                icon: icon,
                data: server
            });
        });

        ...
    },
    failure: function() {
        delete me.activeDiscoverRequest;

        if (serversGraph.isDestroyed) {
            return;
        }

        mainBtn.isLoading = false;
        mainBtn.removeCls('pop-in');
        mainBtn.setIconCls('ico-error');
        mainBtn.setText('Error...click to retry');
    }
});

Вызовите upnpDiscover().

index.js прослушивает сообщение upnp-discover от app.js и отвечает вызовом upnpDiscover() . При обнаружении медиасервера index.js извлекает домен медиасервера из параметров, сохраняет сервер локально, форматирует данные медиасервера и отправляет данные в контроллер MediaServer .

Анализ данных медиасервера

Когда Upnp.js обнаруживает новый медиасервер, он затем получает описание устройства и отправляет Soaprequest для просмотра и анализа данных медиасервера; soapclient.js анализирует элементы мультимедиа по имени тега в документе.

Подключиться к медиасерверу

Upnp.js подключается к обнаруженным медиа-серверам и получает медиа-данные с помощью API-интерфейса сокета приложения Chrome:

socket.create("udp", {}, function(info) {
    var socketId = info.socketId;

    //bind locally
    socket.bind(socketId, "0.0.0.0", 0, function(info) {

        //pack upnp message
        var message = String.toBuffer(UPNP_MESSAGE);

        //broadcast to upnp
        socket.sendTo(socketId, message, UPNP_ADDRESS, UPNP_PORT, function(info) {

            // Wait 1 second
            setTimeout(function() {

                //receive
                socket.recvFrom(socketId, function(info) {

                    //unpack message
                    var data        = String.fromBuffer(info.data),
                        servers     = [],
                        locationReg = /^location:/i;

                    //extract location info
                    if (data) {
                        data = data.split("\r\n");

                        data.forEach(function(value) {
                            if (locationReg.test(value)){
                                servers.push(value.replace(locationReg, "").trim());
                            }
                        });
                    }

                    //success
                    callback(servers);
                });

            }, 1000);
        });
    });
});

Исследуйте и воспроизводите медиафайлы

Контроллер MediaExplorer перечисляет все медиафайлы в папке медиасервера и отвечает за обновление навигационной цепочки в окне приложения медиаплеера. Когда пользователь выбирает медиафайл, контроллер отправляет сообщение в index.js с ключом play-media:

onFileDblClick: function(explorer, record) {
    var serverPanel, node,
        type    = record.get('type'),
        url     = record.get('url'),
        name    = record.get('name'),
        serverId= record.get('serverId');

    if (type === 'audio' || type === 'video') {
        Ext.data.PostMessage.request({
            key     : 'play-media',
            params  : {
                url: url,
                name: name,
                type: type
            }
        });
    }
},

index.js прослушивает это сообщение и отвечает, вызывая playMedia() :

function playMedia(data) {
    var type        = data.params.type,
        url         = data.params.url,
        playerCt    = document.getElementById('player-ct'),
        audioBody   = document.getElementById('audio-body'),
        videoBody   = document.getElementById('video-body'),
        mediaEl     = playerCt.getElementsByTagName(type)[0],
        mediaBody   = type === 'video' ? videoBody : audioBody,
        isLocal     = false;

    //save data
    filePlaying = {
        url : url,
        type: type,
        name: data.params.name
    };

    //hide body els
    audioBody.style.display = 'none';
    videoBody.style.display = 'none';

    var animEnd = function(e) {

        //show body el
        mediaBody.style.display = '';

        //play media
        mediaEl.play();

        //clear listeners
        playerCt.removeEventListener( 'transitionend', animEnd, false );
        animEnd = null;
    };

    //load media
    mediaEl.src = url;
    mediaEl.load();

    //animate in player
    playerCt.addEventListener( 'transitionend', animEnd, false );
    playerCt.style.transform = "translateY(0)";

    //reply postmessage
    data.result = true;
    sendMessage(data);
}

Сохраняйте медиафайлы в автономном режиме

Большую часть тяжелой работы по сохранению мультимедиа в автономном режиме выполняет библиотека filer.js . Подробнее об этой библиотеке можно прочитать в разделе Знакомство с filer.js .

Процесс запускается, когда пользователь выбирает один или несколько файлов и инициирует действие «Отключиться от сети». Контроллер MediaExplorer отправляет сообщение в index.js с ключом «download-media»; index.js прослушивает это сообщение и вызывает функцию downloadMedia() , чтобы инициировать процесс загрузки:

function downloadMedia(data) {
        DownloadProcess.run(data.params.files, function() {
            data.result = true;
            sendMessage(data);
        });
    }

Метод утилиты DownloadProcess создает запрос xhr для получения данных с медиа-сервера и ожидает статуса завершения. Это инициирует обратный вызов при загрузке, который проверяет полученный контент и сохраняет данные локально с помощью функции filer.js :

filer.write(
    saveUrl,
    {
        data: Util.arrayBufferToBlob(fileArrayBuf),
        type: contentType
    },
    function(fileEntry, fileWriter) {

        console.log('file saved!');

        //increment downloaded
        me.completedFiles++;

        //if reached the end, finalize the process
        if (me.completedFiles === me.totalFiles) {

            sendMessage({
                key             : 'download-progresss',
                totalFiles      : me.totalFiles,
                completedFiles  : me.completedFiles
            });

            me.completedFiles = me.totalFiles = me.percentage = me.downloadedFiles = 0;
            delete me.percentages;

            //reload local
            loadLocalFiles(callback);
        }
    },
    function(e) {
        console.log(e);
    }
);

По завершении процесса загрузки MediaExplorer обновляет список медиафайлов и панель дерева медиаплеера.

,

Цель этого документа — помочь вам начать создание приложений Chrome с помощью платформы Sencha Ext JS . Чтобы достичь этой цели, мы углубимся в приложение медиаплеера, созданное Sencha. Исходный код и документация по API доступны на GitHub.

Это приложение обнаруживает доступные медиа-серверы пользователя, включая мультимедийные устройства, подключенные к компьютеру, и программное обеспечение, которое управляет мультимедиа по сети. Пользователи могут просматривать медиафайлы, воспроизводить их по сети или сохранять в автономном режиме.

Вот ключевые вещи, которые вам необходимо сделать для создания приложения медиаплеера с использованием Sencha Ext JS:

  • Создайте манифест, manifest.json .
  • Создайте страницу события , background.js .
  • Логика приложения- песочницы .
  • Обмен данными между приложением Chrome и файлами в песочнице.
  • Откройте для себя медиа-серверы.
  • Исследуйте и воспроизводите медиа.
  • Сохраняйте медиа в автономном режиме.

Создать манифест

Всем приложениям Chrome требуется файл манифеста , содержащий информацию, необходимую Chrome для запуска приложений. Как указано в манифесте, приложение медиаплеера имеет статус «offline_enabled»; Медиа-ресурсы можно сохранять локально, получать к ним доступ и воспроизводить независимо от подключения.

Поле «песочница» используется для изолирования основной логики приложения в уникальном источнике. Весь изолированный контент не подпадает под действие Политики безопасности контента приложений Chrome, но не имеет прямого доступа к API приложений Chrome. Манифест также включает разрешение «сокета»; приложение медиаплеера использует API сокетов для подключения к медиасерверу по сети.

{
    "name": "Video Player",
    "description": "Features network media discovery and playlist management",
    "version": "1.0.0",
    "manifest_version": 2,
    "offline_enabled": true,
    "app": {
        "background": {
            "scripts": [
                "background.js"
            ]
        }
    },
    ...

    "sandbox": {
        "pages": ["sandbox.html"]
    },
    "permissions": [
        "experimental",
        "http://*/*",
        "unlimitedStorage",
        {
            "socket": [
                "tcp-connect",
                "udp-send-to",
                "udp-bind"
            ]
        }
    ]
}

Создать страницу мероприятия

Всем приложениям Chrome для запуска приложения требуется background.js Главная страница медиаплеера index.html открывается в окне указанных размеров:

chrome.app.runtime.onLaunched.addListener(function(launchData) {
    var opt = {
        width: 1000,
        height: 700
    };

    chrome.app.window.create('index.html', opt, function (win) {
        win.launchData = launchData;
    });

});

Логика приложения-песочницы

Приложения Chrome работают в контролируемой среде, в которой применяется строгая Политика безопасности контента (CSP) . Приложению медиаплеера требуются более высокие привилегии для рендеринга компонентов Ext JS. Чтобы соответствовать CSP и выполнять логику приложения, на главной странице приложения index.html создается iframe, который действует как среда песочницы:

<iframe id="sandbox-frame" sandbox="allow-scripts" src="sandbox.html"></iframe>

iframe указывает на файл sandbox.html , который включает файлы, необходимые для приложения Ext JS:

<html>
<head>
    <link rel="stylesheet" type="text/css" href="resources/css/app.css" />'
    <script src="sdk/ext-all-dev.js"></script>'
    <script src="lib/ext/data/PostMessage.js"></script>'
    <script src="lib/ChromeProxy.js"></script>'
    <script src="app.js"></script>
</head>
<body></body>
</html>

Скрипт app.js выполняет весь код Ext JS и отображает представления медиаплеера. Поскольку этот скрипт находится в песочнице, он не может напрямую получить доступ к API-интерфейсам приложений Chrome. Связь между app.js и файлами, не изолированными в песочнице, осуществляется с помощью HTML5 Post Message API .

Обмен файлами

Чтобы приложение медиаплеера могло получить доступ к API приложений Chrome, например, запросить в сети медиасерверы, app.js отправляет сообщения в index.js . В отличие от изолированного app.js , index.js может напрямую обращаться к API-интерфейсам приложений Chrome.

index.js создает iframe:

var iframe = document.getElementById('sandbox-frame');

iframeWindow = iframe.contentWindow;

И прослушивает сообщения из файлов песочницы:

window.addEventListener('message', function(e) {
    var data= e.data,
        key = data.key;

    console.log('[index.js] Post Message received with key ' + key);

    switch (key) {
        case 'extension-baseurl':
            extensionBaseUrl(data);
            break;

        case 'upnp-discover':
            upnpDiscover(data);
            break;

        case 'upnp-browse':
            upnpBrowse(data);
            break;

        case 'play-media':
            playMedia(data);
            break;

        case 'download-media':
            downloadMedia(data);
            break;

        case 'cancel-download':
            cancelDownload(data);
            break;

        default:
            console.log('[index.js] unidentified key for Post Message: "' + key + '"');
    }
}, false);

В следующем примере app.js отправляет сообщение index.js с запросом ключа «extension-baseurl»:

Ext.data.PostMessage.request({
    key: 'extension-baseurl',
    success: function(data) {
        //...
    }
});

index.js получает запрос, присваивает результат и отвечает, отправляя обратно базовый URL-адрес:

function extensionBaseUrl(data) {
    data.result = chrome.extension.getURL('/');
    iframeWindow.postMessage(data, '*');
}

Откройте для себя медиа-серверы

Для обнаружения медиа-серверов требуется очень многое. На высоком уровне рабочий процесс обнаружения инициируется действием пользователя по поиску доступных медиасерверов. Контроллер MediaServer отправляет сообщение в index.js ; index.js прослушивает это сообщение и при его получении вызывает Upnp.js.

Upnp library использует API-интерфейс сокета приложения Chrome для подключения приложения медиаплеера к любым обнаруженным медиа-серверам и получения медиа-данных с медиа-сервера. Upnp.js также использует Soapclient.js для анализа данных медиасервера. В оставшейся части этого раздела этот рабочий процесс описан более подробно.

Опубликовать сообщение

Когда пользователь нажимает кнопку «Медиа-серверы» в центре приложения медиаплеера, MediaServers.js вызывает discoverServers() . Эта функция сначала проверяет наличие невыполненных запросов на обнаружение и, если это правда, прерывает их, чтобы можно было инициировать новый запрос. Затем контроллер отправляет сообщение в index.js с ключом upnp-discovery и двумя прослушивателями обратного вызова:

me.activeDiscoverRequest = Ext.data.PostMessage.request({
    key: 'upnp-discover',
    success: function(data) {
        var items = [];
        delete me.activeDiscoverRequest;

        if (serversGraph.isDestroyed) {
            return;
        }

        mainBtn.isLoading = false;
        mainBtn.removeCls('pop-in');
        mainBtn.setIconCls('ico-server');
        mainBtn.setText('Media Servers');

        //add servers
        Ext.each(data, function(server) {
            var icon,
                urlBase = server.urlBase;

            if (urlBase) {
                if (urlBase.substr(urlBase.length-1, 1) === '/'){
                        urlBase = urlBase.substr(0, urlBase.length-1);
                }
            }

            if (server.icons && server.icons.length) {
                if (server.icons[1]) {
                    icon = server.icons[1].url;
                }
                else {
                    icon = server.icons[0].url;
                }

                icon = urlBase + icon;
            }

            items.push({
                itemId: server.id,
                text: server.friendlyName,
                icon: icon,
                data: server
            });
        });

        ...
    },
    failure: function() {
        delete me.activeDiscoverRequest;

        if (serversGraph.isDestroyed) {
            return;
        }

        mainBtn.isLoading = false;
        mainBtn.removeCls('pop-in');
        mainBtn.setIconCls('ico-error');
        mainBtn.setText('Error...click to retry');
    }
});

Вызовите upnpDiscover().

index.js прослушивает сообщение upnp-discover от app.js и отвечает вызовом upnpDiscover() . При обнаружении медиасервера index.js извлекает домен медиасервера из параметров, сохраняет сервер локально, форматирует данные медиасервера и отправляет данные в контроллер MediaServer .

Анализ данных медиасервера

Когда Upnp.js обнаруживает новый медиасервер, он затем получает описание устройства и отправляет Soaprequest для просмотра и анализа данных медиасервера; soapclient.js анализирует элементы мультимедиа по имени тега в документе.

Подключиться к медиасерверу

Upnp.js подключается к обнаруженным медиа-серверам и получает медиа-данные с помощью API-интерфейса сокета приложения Chrome:

socket.create("udp", {}, function(info) {
    var socketId = info.socketId;

    //bind locally
    socket.bind(socketId, "0.0.0.0", 0, function(info) {

        //pack upnp message
        var message = String.toBuffer(UPNP_MESSAGE);

        //broadcast to upnp
        socket.sendTo(socketId, message, UPNP_ADDRESS, UPNP_PORT, function(info) {

            // Wait 1 second
            setTimeout(function() {

                //receive
                socket.recvFrom(socketId, function(info) {

                    //unpack message
                    var data        = String.fromBuffer(info.data),
                        servers     = [],
                        locationReg = /^location:/i;

                    //extract location info
                    if (data) {
                        data = data.split("\r\n");

                        data.forEach(function(value) {
                            if (locationReg.test(value)){
                                servers.push(value.replace(locationReg, "").trim());
                            }
                        });
                    }

                    //success
                    callback(servers);
                });

            }, 1000);
        });
    });
});

Исследуйте и воспроизводите медиафайлы

Контроллер MediaExplorer перечисляет все медиафайлы в папке медиасервера и отвечает за обновление навигационной цепочки в окне приложения медиаплеера. Когда пользователь выбирает медиафайл, контроллер отправляет сообщение в index.js с ключом play-media:

onFileDblClick: function(explorer, record) {
    var serverPanel, node,
        type    = record.get('type'),
        url     = record.get('url'),
        name    = record.get('name'),
        serverId= record.get('serverId');

    if (type === 'audio' || type === 'video') {
        Ext.data.PostMessage.request({
            key     : 'play-media',
            params  : {
                url: url,
                name: name,
                type: type
            }
        });
    }
},

index.js прослушивает это сообщение и отвечает, вызывая playMedia() :

function playMedia(data) {
    var type        = data.params.type,
        url         = data.params.url,
        playerCt    = document.getElementById('player-ct'),
        audioBody   = document.getElementById('audio-body'),
        videoBody   = document.getElementById('video-body'),
        mediaEl     = playerCt.getElementsByTagName(type)[0],
        mediaBody   = type === 'video' ? videoBody : audioBody,
        isLocal     = false;

    //save data
    filePlaying = {
        url : url,
        type: type,
        name: data.params.name
    };

    //hide body els
    audioBody.style.display = 'none';
    videoBody.style.display = 'none';

    var animEnd = function(e) {

        //show body el
        mediaBody.style.display = '';

        //play media
        mediaEl.play();

        //clear listeners
        playerCt.removeEventListener( 'transitionend', animEnd, false );
        animEnd = null;
    };

    //load media
    mediaEl.src = url;
    mediaEl.load();

    //animate in player
    playerCt.addEventListener( 'transitionend', animEnd, false );
    playerCt.style.transform = "translateY(0)";

    //reply postmessage
    data.result = true;
    sendMessage(data);
}

Сохраняйте медиафайлы в автономном режиме

Большую часть тяжелой работы по сохранению мультимедиа в автономном режиме выполняет библиотека filer.js . Подробнее об этой библиотеке можно прочитать в разделе Знакомство с filer.js .

Процесс запускается, когда пользователь выбирает один или несколько файлов и инициирует действие «Отключиться от сети». Контроллер MediaExplorer отправляет сообщение в index.js с ключом «download-media»; index.js прослушивает это сообщение и вызывает функцию downloadMedia() , чтобы инициировать процесс загрузки:

function downloadMedia(data) {
        DownloadProcess.run(data.params.files, function() {
            data.result = true;
            sendMessage(data);
        });
    }

Метод утилиты DownloadProcess создает запрос xhr для получения данных с медиа-сервера и ожидает статуса завершения. Это инициирует обратный вызов при загрузке, который проверяет полученный контент и сохраняет данные локально с помощью функции filer.js :

filer.write(
    saveUrl,
    {
        data: Util.arrayBufferToBlob(fileArrayBuf),
        type: contentType
    },
    function(fileEntry, fileWriter) {

        console.log('file saved!');

        //increment downloaded
        me.completedFiles++;

        //if reached the end, finalize the process
        if (me.completedFiles === me.totalFiles) {

            sendMessage({
                key             : 'download-progresss',
                totalFiles      : me.totalFiles,
                completedFiles  : me.completedFiles
            });

            me.completedFiles = me.totalFiles = me.percentage = me.downloadedFiles = 0;
            delete me.percentages;

            //reload local
            loadLocalFiles(callback);
        }
    },
    function(e) {
        console.log(e);
    }
);

По завершении процесса загрузки MediaExplorer обновляет список медиафайлов и панель дерева медиаплеера.