Как заставить файловый API Phonegap (Cordova) работать как файловый API в обычном браузере

Есть ли способ заставить файловый API Phonegap работать так же, как тот же API работает в браузере? Мне нужно то же поведение, что и в браузере: нажмите кнопку «Открыть файл» -> диалоговое окно «Выбрать файл» ->... -> Открытие файла с помощью объекта FileReader.

Спасибо!!!

ОБНОВЛЕНИЕ: я нашел решение. Он открывает диалоговое окно «Файл» и возвращает имя файлового объекта JS. Но когда я передаю этот объект файла JS в FileReader из PhoneGap, он не открывается (прослушиватель загрузки никогда не срабатывает). Что я делаю неправильно?

Вот моя Ява:

package org.apache.cordova.example;

import android.os.Bundle;
import org.apache.cordova.api.CordovaInterface;
import android.content.Intent;
import android.net.Uri;
import android.webkit.ValueCallback;
import org.apache.cordova.CordovaChromeClient;
import org.apache.cordova.CordovaWebView;
import org.apache.cordova.DroidGap;


public class cordovaExample extends DroidGap
{
private ValueCallback<Uri> mUploadMessage;
private final static int FILECHOOSER_RESULTCODE = 1;

@Override
public void onCreate(Bundle savedInstanceState)
{
    super.onCreate(savedInstanceState);
    super.loadUrl("file:///android_asset/www/index.html");

    this.appView.setWebChromeClient(new FileAttachmentChromeClient(this,     this.appView));
}



@Override
protected void onActivityResult(int requestCode, int resultCode, Intent intent) {
    if (requestCode == FILECHOOSER_RESULTCODE) {
        if (null == mUploadMessage) return;
        Uri result = intent == null || resultCode != RESULT_OK ? null : intent.getData();
        mUploadMessage.onReceiveValue(result);
        mUploadMessage = null;
    }
}

// openFileChooser is an overridable method in WebChromeClient which isn't
// included in the SDK's Android stub code
public class FileAttachmentChromeClient extends CordovaChromeClient {

    public FileAttachmentChromeClient(CordovaInterface ctx, CordovaWebView app) {
        super(ctx, app);

    }
    public void openFileChooser(ValueCallback<Uri> uploadMsg) {
        mUploadMessage = uploadMsg;
        Intent i = new Intent(Intent.ACTION_GET_CONTENT);
        i.addCategory(Intent.CATEGORY_OPENABLE);
        i.setType("*/*");
        cordovaExample.this.startActivityForResult(Intent.createChooser(i, "Choose type of attachment"), FILECHOOSER_RESULTCODE);
    }

}
}

И что я делаю с JavaScript:

 var SelectedFile;
 var FReader; 

 document.getElementById('fileBox').addEventListener('change', fileChosen);     


 function fileChosen(evnt) {
SelectedFile = evnt.target.files[0];      
     FReader = new FileReader(); 
     FReader.readAsDataURL(SelectedFile);
     FReader.onload = function(loadevnt){   ....   }  //never happens
}

person Ivan Vetrov    schedule 06.12.2012    source источник
comment
Во-первых, установите прослушиватель загрузки перед вызовом readAsDataUrl. Во-вторых, вы должны зарегистрировать прослушиватель onloadend, чтобы вы знали, когда файл будет прочитан. В-третьих, что такое NewPart? Я думаю, вы хотите SelectedFile и что такое SelectedFile? Это путь к файлу?   -  person Simon MacDonald    schedule 09.12.2012
comment
Извините, это был SelectedFile (не NewPart). SelectedFile - это JS [объектный файл] из ‹input type=file /›. я должен передать в FileReader только путь к файлу? Как его получить? Благодарю вас!   -  person Ivan Vetrov    schedule 11.12.2012
comment
Каково значение SelectedFile?   -  person Simon MacDonald    schedule 11.12.2012
comment
Если я предупрежу его или войду в консоль, он скажет: [Object File]. У него есть имя свойства с именем файла и так далее...   -  person Ivan Vetrov    schedule 12.12.2012
comment
Итак, readAsDataURL ожидает объект FileEntry. Ваш SelectedFile должен иметь свойство fullPath. Без него он не будет знать, откуда читать файл.   -  person Simon MacDonald    schedule 12.12.2012
comment
Хорошо, нет свойства fullPath. Как его получить? Благодарю вас!   -  person Ivan Vetrov    schedule 13.12.2012


Ответы (1)


Проблема в том, что большинство мобильных браузеров не поддерживают диалоговое окно с файлами. IIRC iOS 6 — первый мобильный браузер, поддерживающий эту функцию. Я написал некоторый код в своем проекте Corinthian, не находящемся в активной разработке, который исправляет эту функциональность.

Сначала вам нужно написать плагин для Android, чтобы запустить диалоговое окно с намерением предоставить файл. Я использую файловый менеджер OI.

package org.apache.cordova;

import org.apache.cordova.api.CallbackContext;
import org.apache.cordova.api.CordovaPlugin;
import org.apache.cordova.api.LOG;
import org.apache.cordova.api.PluginResult;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.util.HashMap;

/**
 * This class exposes methods in DroidGap that can be called from JavaScript.
 */
public class App extends CordovaPlugin {

    /**
     * Executes the request and returns PluginResult.
     *
     * @param action            The action to execute.
     * @param args              JSONArry of arguments for the plugin.
     * @param callbackContext   The callback context from which we were invoked.
     * @return                  A PluginResult object with a status and message.
     */
    public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException {
        PluginResult.Status status = PluginResult.Status.OK;
        String result = "";

        try {
            if (action.equals("clearCache")) {
                this.clearCache();
            }
            else if (action.equals("show")) {
                // This gets called from JavaScript onCordovaReady to show the webview.
                // I recommend we change the name of the Message as spinner/stop is not
                // indicative of what this actually does (shows the webview).
                cordova.getActivity().runOnUiThread(new Runnable() {
                    public void run() {
                        webView.postMessage("spinner", "stop");
                    }
                });
            }
            else if (action.equals("loadUrl")) {
                this.loadUrl(args.getString(0), args.optJSONObject(1));
            }
            else if (action.equals("cancelLoadUrl")) {
                this.cancelLoadUrl();
            }
            else if (action.equals("clearHistory")) {
                this.clearHistory();
            }
            else if (action.equals("backHistory")) {
                this.backHistory();
            }
            else if (action.equals("overrideButton")) {
                this.overrideButton(args.getString(0), args.getBoolean(1));
            }
            else if (action.equals("overrideBackbutton")) {
                this.overrideBackbutton(args.getBoolean(0));
            }
            else if (action.equals("exitApp")) {
                this.exitApp();
            }
            callbackContext.sendPluginResult(new PluginResult(status, result));
            return true;
        } catch (JSONException e) {
            callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.JSON_EXCEPTION));
            return false;
        }
    }

    //--------------------------------------------------------------------------
    // LOCAL METHODS
    //--------------------------------------------------------------------------

    /**
     * Clear the resource cache.
     */
    public void clearCache() {
        this.webView.clearCache(true);
    }

    /**
     * Load the url into the webview.
     *
     * @param url
     * @param props         Properties that can be passed in to the DroidGap activity (i.e. loadingDialog, wait, ...)
     * @throws JSONException
     */
    public void loadUrl(String url, JSONObject props) throws JSONException {
        LOG.d("App", "App.loadUrl("+url+","+props+")");
        int wait = 0;
        boolean openExternal = false;
        boolean clearHistory = false;

        // If there are properties, then set them on the Activity
        HashMap<String, Object> params = new HashMap<String, Object>();
        if (props != null) {
            JSONArray keys = props.names();
            for (int i = 0; i < keys.length(); i++) {
                String key = keys.getString(i);
                if (key.equals("wait")) {
                    wait = props.getInt(key);
                }
                else if (key.equalsIgnoreCase("openexternal")) {
                    openExternal = props.getBoolean(key);
                }
                else if (key.equalsIgnoreCase("clearhistory")) {
                    clearHistory = props.getBoolean(key);
                }
                else {
                    Object value = props.get(key);
                    if (value == null) {

                    }
                    else if (value.getClass().equals(String.class)) {
                        params.put(key, (String)value);
                    }
                    else if (value.getClass().equals(Boolean.class)) {
                        params.put(key, (Boolean)value);
                    }
                    else if (value.getClass().equals(Integer.class)) {
                        params.put(key, (Integer)value);
                    }
                }
            }
        }

        // If wait property, then delay loading

        if (wait > 0) {
            try {
                synchronized(this) {
                    this.wait(wait);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        this.webView.showWebPage(url, openExternal, clearHistory, params);
    }

    /**
     * Cancel loadUrl before it has been loaded (Only works on a CordovaInterface class)
     */
    @Deprecated
    public void cancelLoadUrl() {
        this.cordova.cancelLoadUrl();
    }

    /**
     * Clear page history for the app.
     */
    public void clearHistory() {
        this.webView.clearHistory();
    }

    /**
     * Go to previous page displayed.
     * This is the same as pressing the backbutton on Android device.
     */
    public void backHistory() {
        cordova.getActivity().runOnUiThread(new Runnable() {
            public void run() {
                webView.backHistory();
            }
        });
    }

    /**
     * Override the default behavior of the Android back button.
     * If overridden, when the back button is pressed, the "backKeyDown" JavaScript event will be fired.
     *
     * @param override      T=override, F=cancel override
     */
    public void overrideBackbutton(boolean override) {
        LOG.i("App", "WARNING: Back Button Default Behaviour will be overridden.  The backbutton event will be fired!");
        webView.bindButton(override);
    }

    /**
     * Override the default behavior of the Android volume buttons.
     * If overridden, when the volume button is pressed, the "volume[up|down]button" JavaScript event will be fired.
     *
     * @param button        volumeup, volumedown
     * @param override      T=override, F=cancel override
     */
    public void overrideButton(String button, boolean override) {
        LOG.i("DroidGap", "WARNING: Volume Button Default Behaviour will be overridden.  The volume event will be fired!");
        webView.bindButton(button, override);
    }

    /**
     * Return whether the Android back button is overridden by the user.
     *
     * @return boolean
     */
    public boolean isBackbuttonOverridden() {
        return webView.isBackButtonBound();
    }

    /**
     * Exit the Android application.
     */
    public void exitApp() {
        this.webView.postMessage("exit", null);
    }

}

package com.simonmacdonald.corinthian;

import org.apache.cordova.api.Plugin;
import org.apache.cordova.api.PluginResult;
import org.json.JSONArray;
import org.json.JSONObject;

import android.app.Activity;
import android.app.AlertDialog;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.net.Uri;
import android.util.Log;

public class FileDialog extends Plugin {
    private static final int PICK_FILE_RESULT_CODE = 8974;
    private static final int PICK_DIRECTORY_RESULT_CODE = 8975;
    private static final String LOG_TAG = "FileDialog";
    public String callbackId;

    /**
     * Executes the request and returns PluginResult.
     *
     * @param action        The action to execute.
     * @param args          JSONArry of arguments for the plugin.
     * @param callbackId    The callback id used when calling back into JavaScript.
     * @return              A PluginResult object with a status and message.
     */
    @Override
    public PluginResult execute(String action, JSONArray args, String callbackId) {
        this.callbackId = callbackId;

        JSONObject options = args.optJSONObject(0);

        if (action.equals("pickFile")) {
            showDialog(options, PICK_FILE_RESULT_CODE);
        } else if (action.equals("pickFolder")) {
            showDialog(options, PICK_DIRECTORY_RESULT_CODE);
        }
        else {
            return new PluginResult(PluginResult.Status.INVALID_ACTION);
        }
        PluginResult r = new PluginResult(PluginResult.Status.NO_RESULT);
        r.setKeepCallback(true);
        return r;
    }

    private void showDialog(JSONObject options, int type) {
        Intent intent;
        if (type == PICK_FILE_RESULT_CODE) {
            intent = new Intent("org.openintents.action.PICK_FILE");
        } else {
            intent = new Intent("org.openintents.action.PICK_DIRECTORY");
        }
        if (options != null) {
            String title = options.optString("title");
            if (title != null) {
                intent.putExtra("org.openintents.extra.TITLE", title);
            }
            String button = options.optString("button");
            if (button != null) {
                intent.putExtra("org.openintents.extra.BUTTON_TEXT", button);
            }
        }
        //intent.setData(Uri.fromFile(new File("/")));
        try {
            this.cordova.startActivityForResult((Plugin)this,intent,PICK_FILE_RESULT_CODE);
        } catch (ActivityNotFoundException e) {
            showDownloadDialog();
        }
    }

    private void showDownloadDialog() {
        final Context context = this.cordova.getContext();
        Runnable runnable = new Runnable() {
            public void run() {

                AlertDialog.Builder dialog = new AlertDialog.Builder(context);
                dialog.setTitle("Install File Manager?");
                dialog.setMessage("This requires the free OI File Manager app. Would you like to install it now?");
                dialog.setPositiveButton("Yes", new DialogInterface.OnClickListener() {
                    public void onClick(DialogInterface dlg, int i) {
                        dlg.dismiss();
                        Intent intent = new Intent(Intent.ACTION_VIEW,
                            Uri.parse("market://search?q=pname:org.openintents.filemanager")
                        );
                        try {
                            context.startActivity(intent);
                        } catch (ActivityNotFoundException e) {
//                          We don't have the market app installed, so download it directly.
                            Intent in = new Intent(Intent.ACTION_VIEW);
                            in.setData(Uri.parse("http://openintents.googlecode.com/files/FileManager-1.2.apk"));
                            context.startActivity(in);

                        }

                    }
                });
                dialog.setNegativeButton("No", new DialogInterface.OnClickListener() {
                    public void onClick(DialogInterface dlg, int i) {
                        dlg.dismiss();
                    }
                });
                dialog.create();
                dialog.show();
            }
        };
        this.cordova.getActivity().runOnUiThread(runnable);
    }

    @Override
    public void onActivityResult(int reqCode, int resultCode, Intent data) {
      //super.onActivityResult(reqCode, resultCode, data);
      //Log.d(LOG_TAG, "Data is " + data.getData().toString());
      Log.d(LOG_TAG, "we are in on activity result");
      switch (reqCode) {
      case PICK_FILE_RESULT_CODE:
      case PICK_DIRECTORY_RESULT_CODE: {
          if (resultCode==Activity.RESULT_OK && data!=null && data.getData()!=null) {
              String filePath = "file://" + data.getData().getPath();
              Log.d(LOG_TAG, "The data is = " + filePath);
              Log.d(LOG_TAG, "Calling succes with callback id = " + this.callbackId);
              this.success(new PluginResult(PluginResult.Status.OK, filePath), this.callbackId);
          }
          break;
      }
  }
    }
}

Затем вам нужно написать свой интерфейс JavaScript:

FileDialog: {
    pickFile: function(successCallback, errorCallback, options) {
        var win = typeof successCallback !== 'function' ? null : function(f) {
            window.resolveLocalFileSystemURI(f, function(fileEntry) {
                successCallback(fileEntry);
            }, fail);
        };
        cordova.exec(win, errorCallback, "FileDialog", "pickFile", [options]);
    },
    pickFolder: function(successCallback, errorCallback, options) {
        var win = typeof successCallback !== 'function' ? null : function(d) {
            window.resolveLocalFileSystemURI(d, function(dirEntry) {
                successCallback(dirEntry);
            }, fail);
        };
        cordova.exec(win, errorCallback, "FileDialog", "pickFolder", [options]);
    },
    patch: function() {
        var inputs = document.getElementsByTagName("input");
        for (var i=0; i < inputs.length; i++) {
           if (inputs[i].getAttribute('type') == 'file'){
               var me = inputs[i];
               inputs[i].addEventListener("click", function() {
                   corinthian.FileDialog.pickFile(function(fileEntry) {
                       me.value = fileEntry.fullPath;
                   });
               });
           }
        }
    }
}

и, наконец, патч для обезьян, вызвав FileDialog.patch(); как только вы получили событие deviceready в PhoneGap.

Надеюсь это поможет...

person Simon MacDonald    schedule 07.12.2012
comment
Спасибо, Семён за ответ! Но так много кода... Я попытался реализовать какое-то решение, которое нашел в Интернете: оно открывает диалоговое окно "Файл", но FileReader не работает. Я обновил свой вопрос некоторым кодом, который у меня есть сейчас. Может быть, вы можете помочь мне улучшить его? Благодарю вас! - person Ivan Vetrov; 08.12.2012