现在的位置: 首页 > 移动开发> 正文
移动 APP 之跨平台解决方案
2011年10月29日 移动开发 评论数 2 ⁄ 被围观 6,345+

最近看了一本书《Building Android Apps with HTML CSS and JavaScript》,顾名思义就是用开发web的方式来开发Android的APP,其中倒不是web的开发技术最吸我,而是这样的一种解决方案。像我们现在的手持设备种类这么多,主流的不外乎Android,Iphone,Ipad等等,如果要对每一种平台都开发一个相应的APP版本,代价太大。而基于浏览器的web app就容易解决这个问题,只需开发一次,便可以通用部署。

jqtouch

上面即是使用web技术来开发本地iphone app的一幅运行图,关于web的开发技术,这里就不多说了,这里重点提及Phonegap,利用它我们就可以把web app转换成各种移动平台的APP。

PhoneGap is an HTML5 app platform that allows you to author native applications with web technologies and get access to APIs and app stores. PhoneGap leverages web technologies developers already know best... HTML and JavaScript

这里简单介绍一下PhoneGap,它对各个移动平台的API进行了一次封装,屏蔽了每种移动平台的具体细节,而设计了一个公共的API层,对开发者来说,只要调用统一的API来进行开发就可以了。

把PhoneGap的源代码下载来通读了一下,了解了一下背后的实现原理,下面以Camera的功能为例来进行说明。

API 开发文档请参照 http://docs.phonegap.com/en/1.1.0/index.html,其中有关Camera的开发API请参照http://docs.phonegap.com/en/1.1.0/phonegap_camera_camera.md.html#Camera,我们的代码就可以写成

1
2
3
4
5
6
7
8
navigator.camera.getPicture(onSuccess, onFail, { quality: 50 });
function onSuccess(imageData) {    
  var image = document.getElementById('myImage');    
  image.src = "data:image/jpeg;base64," + imageData;
}
function onFail(message) {    
  alert('Failed because: ' + message);
}
navigator.camera.getPicture(onSuccess, onFail, { quality: 50 });
function onSuccess(imageData) {    
  var image = document.getElementById('myImage');    
  image.src = "data:image/jpeg;base64," + imageData;
}
function onFail(message) {    
  alert('Failed because: ' + message);
}

是不是很简单,具体后台平台的差异我们根本不需要来进行区别,全部由PhoneGap来给处理了,那么它是怎么处理的呢,继续往下看。

先来看看JS端的API实现,它主要是定义了web层开发的接口,也即我们上面例子中的camera.getPicture 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
/**
 * Gets a picture from source defined by "options.sourceType", and returns the
 * image as defined by the "options.destinationType" option.
 
 * The defaults are sourceType=CAMERA and destinationType=DATA_URL.
 *
 * @param {Function} successCallback
 * @param {Function} errorCallback
 * @param {Object} options
 */
Camera.prototype.getPicture = function(successCallback, errorCallback, options) {
 
    // successCallback required
    if (typeof successCallback !== "function") {
        console.log("Camera Error: successCallback is not a function");
        return;
    }
 
    // errorCallback optional
    if (errorCallback && (typeof errorCallback !== "function")) {
        console.log("Camera Error: errorCallback is not a function");
        return;
    }
 
    if (options === null || typeof options === "undefined") {
        options = {};
    }
    if (options.quality === null || typeof options.quality === "undefined") {
        options.quality = 80;
    }
    if (options.maxResolution === null || typeof options.maxResolution === "undefined") {
        options.maxResolution = 0;
    }
    if (options.destinationType === null || typeof options.destinationType === "undefined") {
        options.destinationType = Camera.DestinationType.DATA_URL;
    }
    if (options.sourceType === null || typeof options.sourceType === "undefined") {
        options.sourceType = Camera.PictureSourceType.CAMERA;
    }
    if (options.encodingType === null || typeof options.encodingType === "undefined") {
        options.encodingType = Camera.EncodingType.JPEG;
    }
    if (options.mediaType === null || typeof options.mediaType === "undefined") {
        options.mediaType = Camera.MediaType.PICTURE;
    }
    if (options.targetWidth === null || typeof options.targetWidth === "undefined") {
        options.targetWidth = -1;
    }
    else if (typeof options.targetWidth == "string") {
        var width = new Number(options.targetWidth);
        if (isNaN(width) === false) {
            options.targetWidth = width.valueOf();
        }
    }
    if (options.targetHeight === null || typeof options.targetHeight === "undefined") {
        options.targetHeight = -1;
    }
    else if (typeof options.targetHeight == "string") {
        var height = new Number(options.targetHeight);
        if (isNaN(height) === false) {
            options.targetHeight = height.valueOf();
        }
    }
 
    PhoneGap.exec(successCallback, errorCallback, "Camera", "takePicture", [options]);
};
/**
 * Gets a picture from source defined by "options.sourceType", and returns the
 * image as defined by the "options.destinationType" option.

 * The defaults are sourceType=CAMERA and destinationType=DATA_URL.
 *
 * @param {Function} successCallback
 * @param {Function} errorCallback
 * @param {Object} options
 */
Camera.prototype.getPicture = function(successCallback, errorCallback, options) {

    // successCallback required
    if (typeof successCallback !== "function") {
        console.log("Camera Error: successCallback is not a function");
        return;
    }

    // errorCallback optional
    if (errorCallback && (typeof errorCallback !== "function")) {
        console.log("Camera Error: errorCallback is not a function");
        return;
    }

    if (options === null || typeof options === "undefined") {
        options = {};
    }
    if (options.quality === null || typeof options.quality === "undefined") {
        options.quality = 80;
    }
    if (options.maxResolution === null || typeof options.maxResolution === "undefined") {
    	options.maxResolution = 0;
    }
    if (options.destinationType === null || typeof options.destinationType === "undefined") {
        options.destinationType = Camera.DestinationType.DATA_URL;
    }
    if (options.sourceType === null || typeof options.sourceType === "undefined") {
        options.sourceType = Camera.PictureSourceType.CAMERA;
    }
    if (options.encodingType === null || typeof options.encodingType === "undefined") {
        options.encodingType = Camera.EncodingType.JPEG;
    }
    if (options.mediaType === null || typeof options.mediaType === "undefined") {
        options.mediaType = Camera.MediaType.PICTURE;
    }
    if (options.targetWidth === null || typeof options.targetWidth === "undefined") {
        options.targetWidth = -1;
    }
    else if (typeof options.targetWidth == "string") {
        var width = new Number(options.targetWidth);
        if (isNaN(width) === false) {
            options.targetWidth = width.valueOf();
        }
    }
    if (options.targetHeight === null || typeof options.targetHeight === "undefined") {
        options.targetHeight = -1;
    }
    else if (typeof options.targetHeight == "string") {
        var height = new Number(options.targetHeight);
        if (isNaN(height) === false) {
            options.targetHeight = height.valueOf();
        }
    }

    PhoneGap.exec(successCallback, errorCallback, "Camera", "takePicture", [options]);
};

上段代码中的最后一句就是呼叫本地的Camera模块的takePicture函数,具体的PhoneGap.exec实现代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
/**
 * Execute a PhoneGap command.  It is up to the native side whether this action is synch or async.
 * The native side can return:
 *      Synchronous: PluginResult object as a JSON string
 *      Asynchrounous: Empty string ""
 * If async, the native side will PhoneGap.callbackSuccess or PhoneGap.callbackError,
 * depending upon the result of the action.
 *
 * @param {Function} success    The success callback
 * @param {Function} fail       The fail callback
 * @param {String} service      The name of the service to use
 * @param {String} action       Action to be run in PhoneGap
 * @param {Array.<String>} [args]     Zero or more arguments to pass to the method
 */
PhoneGap.exec = function(success, fail, service, action, args) {
    try {
        var callbackId = service + PhoneGap.callbackId++;
        if (success || fail) {
            PhoneGap.callbacks[callbackId] = {success:success, fail:fail};
        }
 
        var r = prompt(PhoneGap.stringify(args), "gap:"+PhoneGap.stringify([service, action, callbackId, true]));
 
        // If a result was returned
        if (r.length > 0) {
            eval("var v="+r+";");
 
            // If status is OK, then return value back to caller
            if (v.status === PhoneGap.callbackStatus.OK) {
 
                // If there is a success callback, then call it now with
                // returned value
                if (success) {
                    try {
                        success(v.message);
                    } catch (e) {
                        console.log("Error in success callback: " + callbackId  + " = " + e);
                    }
 
                    // Clear callback if not expecting any more results
                    if (!v.keepCallback) {
                        delete PhoneGap.callbacks[callbackId];
                    }
                }
                return v.message;
            }
 
            // If no result
            else if (v.status === PhoneGap.callbackStatus.NO_RESULT) {
 
                // Clear callback if not expecting any more results
                if (!v.keepCallback) {
                    delete PhoneGap.callbacks[callbackId];
                }
            }
 
            // If error, then display error
            else {
                console.log("Error: Status="+v.status+" Message="+v.message);
 
                // If there is a fail callback, then call it now with returned value
                if (fail) {
                    try {
                        fail(v.message);
                    }
                    catch (e1) {
                        console.log("Error in error callback: "+callbackId+" = "+e1);
                    }
 
                    // Clear callback if not expecting any more results
                    if (!v.keepCallback) {
                        delete PhoneGap.callbacks[callbackId];
                    }
                }
                return null;
            }
        }
    } catch (e2) {
        console.log("Error: "+e2);
    }
};
/**
 * Execute a PhoneGap command.  It is up to the native side whether this action is synch or async.
 * The native side can return:
 *      Synchronous: PluginResult object as a JSON string
 *      Asynchrounous: Empty string ""
 * If async, the native side will PhoneGap.callbackSuccess or PhoneGap.callbackError,
 * depending upon the result of the action.
 *
 * @param {Function} success    The success callback
 * @param {Function} fail       The fail callback
 * @param {String} service      The name of the service to use
 * @param {String} action       Action to be run in PhoneGap
 * @param {Array.<String>} [args]     Zero or more arguments to pass to the method
 */
PhoneGap.exec = function(success, fail, service, action, args) {
    try {
        var callbackId = service + PhoneGap.callbackId++;
        if (success || fail) {
            PhoneGap.callbacks[callbackId] = {success:success, fail:fail};
        }

        var r = prompt(PhoneGap.stringify(args), "gap:"+PhoneGap.stringify([service, action, callbackId, true]));

        // If a result was returned
        if (r.length > 0) {
            eval("var v="+r+";");

            // If status is OK, then return value back to caller
            if (v.status === PhoneGap.callbackStatus.OK) {

                // If there is a success callback, then call it now with
                // returned value
                if (success) {
                    try {
                        success(v.message);
                    } catch (e) {
                        console.log("Error in success callback: " + callbackId  + " = " + e);
                    }

                    // Clear callback if not expecting any more results
                    if (!v.keepCallback) {
                        delete PhoneGap.callbacks[callbackId];
                    }
                }
                return v.message;
            }

            // If no result
            else if (v.status === PhoneGap.callbackStatus.NO_RESULT) {

                // Clear callback if not expecting any more results
                if (!v.keepCallback) {
                    delete PhoneGap.callbacks[callbackId];
                }
            }

            // If error, then display error
            else {
                console.log("Error: Status="+v.status+" Message="+v.message);

                // If there is a fail callback, then call it now with returned value
                if (fail) {
                    try {
                        fail(v.message);
                    }
                    catch (e1) {
                        console.log("Error in error callback: "+callbackId+" = "+e1);
                    }

                    // Clear callback if not expecting any more results
                    if (!v.keepCallback) {
                        delete PhoneGap.callbacks[callbackId];
                    }
                }
                return null;
            }
        }
    } catch (e2) {
        console.log("Error: "+e2);
    }
};

至此为止,web端js的代码工作就完毕了,程序流程转移到了本地代码中,我们以Android的本地代码为例,看看是怎么具体实现的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
package com.phonegap;
 
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Intent;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.Bitmap.CompressFormat;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.provider.MediaStore.Images.Media;
import com.phonegap.api.PhonegapActivity;
import com.phonegap.api.Plugin;
import com.phonegap.api.PluginResult;
import com.phonegap.api.PluginResult.Status;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import org.apache.commons.codec.binary.Base64;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
 
public class CameraLauncher extends Plugin
{
  private static final int DATA_URL = 0;
  private static final int FILE_URI = 1;
  private static final int PHOTOLIBRARY = 0;
  private static final int CAMERA = 1;
  private static final int SAVEDPHOTOALBUM = 2;
  private static final int PICTURE = 0;
  private static final int VIDEO = 1;
  private static final int ALLMEDIA = 2;
  private static final int JPEG = 0;
  private static final int PNG = 1;
  private static final String GET_PICTURE = "Get Picture";
  private static final String GET_VIDEO = "Get Video";
  private static final String GET_All = "Get All";
  private static final String LOG_TAG = "CameraLauncher";
  private int mQuality;
  private int targetWidth;
  private int targetHeight;
  private Uri imageUri;
  private int encodingType;
  private int mediaType;
  public String callbackId;
  private int numPics;
 
  public PluginResult execute(String action, JSONArray args, String callbackId)
  {
    PluginResult.Status status = PluginResult.Status.OK;
    String result = "";
    this.callbackId = callbackId;
    try
    {
      if (action.equals("takePicture")) {
        int srcType = 1;
        int destType = 0;
        this.targetHeight = 0;
        this.targetWidth = 0;
        this.encodingType = 0;
        this.mediaType = 0;
        this.mQuality = 80;
 
        JSONObject options = args.optJSONObject(0);
        if (options != null) {
          srcType = options.getInt("sourceType");
          destType = options.getInt("destinationType");
          this.targetHeight = options.getInt("targetHeight");
          this.targetWidth = options.getInt("targetWidth");
          this.encodingType = options.getInt("encodingType");
          this.mediaType = options.getInt("mediaType");
          this.mQuality = options.getInt("quality");
        }
 
        if (srcType == 1) {
          takePicture(destType, this.encodingType);
        }
        else if ((srcType == 0) || (srcType == 2)) {
          getImage(srcType, destType);
        }
        PluginResult r = new PluginResult(PluginResult.Status.NO_RESULT);
        r.setKeepCallback(true);
        return r;
      }
      return new PluginResult(status, result);
    } catch (JSONException e) {
      e.printStackTrace();
    }return new PluginResult(PluginResult.Status.JSON_EXCEPTION);
  }
 
  public void takePicture(int returnType, int encodingType)
  {
    this.numPics = queryImgDB().getCount();
 
    Intent intent = new Intent("android.media.action.IMAGE_CAPTURE");
 
    File photo = createCaptureFile(encodingType);
    intent.putExtra("output", Uri.fromFile(photo));
    this.imageUri = Uri.fromFile(photo);
 
    this.ctx.startActivityForResult(this, intent, 32 + returnType + 1);
  }
 
  private File createCaptureFile(int encodingType)
  {
    File photo = null;
    if (encodingType == 0)
      photo = new File(DirectoryManager.getTempDirectoryPath(this.ctx), "Pic.jpg");
    else {
      photo = new File(DirectoryManager.getTempDirectoryPath(this.ctx), "Pic.png");
    }
    return photo;
  }
 
  public void getImage(int srcType, int returnType)
  {
    Intent intent = new Intent();
    String title = "Get Picture";
    if (this.mediaType == 0) {
      intent.setType("image/*");
    }
    else if (this.mediaType == 1) {
      intent.setType("video/*");
      title = "Get Video";
    }
    else if (this.mediaType == 2)
    {
      intent.setType("*/*");
      title = "Get All";
    }
 
    intent.setAction("android.intent.action.GET_CONTENT");
    intent.addCategory("android.intent.category.OPENABLE");
    this.ctx.startActivityForResult(this, Intent.createChooser(intent, new String(title)), (srcType + 1) * 16 + returnType + 1);
  }
 
  public Bitmap scaleBitmap(Bitmap bitmap)
  {
    int newWidth = this.targetWidth;
    int newHeight = this.targetHeight;
    int origWidth = bitmap.getWidth();
    int origHeight = bitmap.getHeight();
 
    if ((newWidth <= 0) && (newHeight <= 0)) {
      return bitmap;
    }
 
    if ((newWidth > 0) && (newHeight <= 0)) {
      newHeight = newWidth * origHeight / origWidth;
    }
    else if ((newWidth <= 0) && (newHeight > 0)) {
      newWidth = newHeight * origWidth / origHeight;
    }
    else
    {
      double newRatio = newWidth / newHeight;
      double origRatio = origWidth / origHeight;
 
      if (origRatio > newRatio)
        newHeight = newWidth * origHeight / origWidth;
      else if (origRatio < newRatio) {
        newWidth = newHeight * origWidth / origHeight;
      }
    }
 
    return Bitmap.createScaledBitmap(bitmap, newWidth, newHeight, true);
  }
 
  public void onActivityResult(int requestCode, int resultCode, Intent intent)
  {
    int srcType = requestCode / 16 - 1;
    int destType = requestCode % 16 - 1;
 
    if (srcType == 1)
    {
      if (resultCode == -1) {
        try
        {
          ExifHelper exif = new ExifHelper();
          if (this.encodingType == 0) {
            exif.createInFile(DirectoryManager.getTempDirectoryPath(this.ctx) + "/Pic.jpg");
            exif.readExifData();
          }
 
          try
          {
            bitmap = MediaStore.Images.Media.getBitmap(this.ctx.getContentResolver(), this.imageUri);
          } catch (FileNotFoundException e) {
            Uri uri = intent.getData();
            ContentResolver resolver = this.ctx.getContentResolver();
            bitmap = BitmapFactory.decodeStream(resolver.openInputStream(uri));
          }
 
          Bitmap bitmap = scaleBitmap(bitmap);
 
          if (destType == 0) {
            processPicture(bitmap);
            checkForDuplicateImage(0);
          }
          else if (destType == 1)
          {
            ContentValues values = new ContentValues();
            values.put("mime_type", "image/jpeg");
            Uri uri = null;
            try {
              uri = this.ctx.getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
            } catch (UnsupportedOperationException e) {
              System.out.println("Can't write to external media storage.");
              try {
                uri = this.ctx.getContentResolver().insert(MediaStore.Images.Media.INTERNAL_CONTENT_URI, values);
              } catch (UnsupportedOperationException ex) {
                System.out.println("Can't write to internal media storage.");
                failPicture("Error capturing image - no media storage found.");
                return;
              }
 
            }
 
            OutputStream os = this.ctx.getContentResolver().openOutputStream(uri);
            bitmap.compress(Bitmap.CompressFormat.JPEG, this.mQuality, os);
            os.close();
 
            if (this.encodingType == 0) {
              exif.createOutFile(FileUtils.getRealPathFromURI(uri, this.ctx));
              exif.writeExifData();
            }
 
            success(new PluginResult(PluginResult.Status.OK, uri.toString()), this.callbackId);
          }
          bitmap.recycle();
          bitmap = null;
          System.gc();
 
          checkForDuplicateImage(1);
        } catch (IOException e) {
          e.printStackTrace();
          failPicture("Error capturing image.");
        }
 
      }
      else if (resultCode == 0) {
        failPicture("Camera cancelled.");
      }
      else
      {
        failPicture("Did not complete!");
      }
 
    }
    else if ((srcType == 0) || (srcType == 2))
      if (resultCode == -1) {
        Uri uri = intent.getData();
        ContentResolver resolver = this.ctx.getContentResolver();
 
        if (this.mediaType != 0) {
          success(new PluginResult(PluginResult.Status.OK, uri.toString()), this.callbackId);
        }
        else if (destType == 0) {
          try {
            Bitmap bitmap = BitmapFactory.decodeStream(resolver.openInputStream(uri));
            bitmap = scaleBitmap(bitmap);
            processPicture(bitmap);
            bitmap.recycle();
            bitmap = null;
            System.gc();
          } catch (FileNotFoundException e) {
            e.printStackTrace();
            failPicture("Error retrieving image.");
          }
 
        }
        else if (destType == 1)
        {
          if ((this.targetHeight > 0) && (this.targetWidth > 0)) {
            try {
              Bitmap bitmap = BitmapFactory.decodeStream(resolver.openInputStream(uri));
              bitmap = scaleBitmap(bitmap);
 
              String fileName = DirectoryManager.getTempDirectoryPath(this.ctx) + "/resize.jpg";
              OutputStream os = new FileOutputStream(fileName);
              bitmap.compress(Bitmap.CompressFormat.JPEG, this.mQuality, os);
              os.close();
 
              bitmap.recycle();
              bitmap = null;
 
              success(new PluginResult(PluginResult.Status.OK, "file://" + fileName), this.callbackId);
              System.gc();
            } catch (Exception e) {
              e.printStackTrace();
              failPicture("Error retrieving image.");
            }
          }
          else {
            success(new PluginResult(PluginResult.Status.OK, uri.toString()), this.callbackId);
          }
        }
 
      }
      else if (resultCode == 0) {
        failPicture("Selection cancelled.");
      }
      else {
        failPicture("Selection did not complete!");
      }
  }
 
  private Cursor queryImgDB()
  {
    return this.ctx.getContentResolver().query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, new String[] { "_id" }, null, null, null);
  }
 
  private void checkForDuplicateImage(int type)
  {
    int diff = 1;
    Cursor cursor = queryImgDB();
    int currentNumOfImages = cursor.getCount();
 
    if (type == 1) {
      diff = 2;
    }
 
    if (currentNumOfImages - this.numPics == diff) {
      cursor.moveToLast();
      int id = Integer.valueOf(cursor.getString(cursor.getColumnIndex("_id"))).intValue() - 1;
      Uri uri = Uri.parse(MediaStore.Images.Media.EXTERNAL_CONTENT_URI + "/" + id);
      this.ctx.getContentResolver().delete(uri, null, null);
    }
  }
 
  public void processPicture(Bitmap bitmap)
  {
    ByteArrayOutputStream jpeg_data = new ByteArrayOutputStream();
    try {
      if (bitmap.compress(Bitmap.CompressFormat.JPEG, this.mQuality, jpeg_data)) {
        byte[] code = jpeg_data.toByteArray();
        byte[] output = Base64.encodeBase64(code);
        String js_out = new String(output);
        success(new PluginResult(PluginResult.Status.OK, js_out), this.callbackId);
        js_out = null;
        output = null;
        code = null;
      }
    }
    catch (Exception e) {
      failPicture("Error compressing image.");
    }
    jpeg_data = null;
  }
 
  public void failPicture(String err)
  {
    error(new PluginResult(PluginResult.Status.ERROR, err), this.callbackId);
  }
}
package com.phonegap;

import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Intent;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.Bitmap.CompressFormat;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.provider.MediaStore.Images.Media;
import com.phonegap.api.PhonegapActivity;
import com.phonegap.api.Plugin;
import com.phonegap.api.PluginResult;
import com.phonegap.api.PluginResult.Status;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import org.apache.commons.codec.binary.Base64;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

public class CameraLauncher extends Plugin
{
  private static final int DATA_URL = 0;
  private static final int FILE_URI = 1;
  private static final int PHOTOLIBRARY = 0;
  private static final int CAMERA = 1;
  private static final int SAVEDPHOTOALBUM = 2;
  private static final int PICTURE = 0;
  private static final int VIDEO = 1;
  private static final int ALLMEDIA = 2;
  private static final int JPEG = 0;
  private static final int PNG = 1;
  private static final String GET_PICTURE = "Get Picture";
  private static final String GET_VIDEO = "Get Video";
  private static final String GET_All = "Get All";
  private static final String LOG_TAG = "CameraLauncher";
  private int mQuality;
  private int targetWidth;
  private int targetHeight;
  private Uri imageUri;
  private int encodingType;
  private int mediaType;
  public String callbackId;
  private int numPics;

  public PluginResult execute(String action, JSONArray args, String callbackId)
  {
    PluginResult.Status status = PluginResult.Status.OK;
    String result = "";
    this.callbackId = callbackId;
    try
    {
      if (action.equals("takePicture")) {
        int srcType = 1;
        int destType = 0;
        this.targetHeight = 0;
        this.targetWidth = 0;
        this.encodingType = 0;
        this.mediaType = 0;
        this.mQuality = 80;

        JSONObject options = args.optJSONObject(0);
        if (options != null) {
          srcType = options.getInt("sourceType");
          destType = options.getInt("destinationType");
          this.targetHeight = options.getInt("targetHeight");
          this.targetWidth = options.getInt("targetWidth");
          this.encodingType = options.getInt("encodingType");
          this.mediaType = options.getInt("mediaType");
          this.mQuality = options.getInt("quality");
        }

        if (srcType == 1) {
          takePicture(destType, this.encodingType);
        }
        else if ((srcType == 0) || (srcType == 2)) {
          getImage(srcType, destType);
        }
        PluginResult r = new PluginResult(PluginResult.Status.NO_RESULT);
        r.setKeepCallback(true);
        return r;
      }
      return new PluginResult(status, result);
    } catch (JSONException e) {
      e.printStackTrace();
    }return new PluginResult(PluginResult.Status.JSON_EXCEPTION);
  }

  public void takePicture(int returnType, int encodingType)
  {
    this.numPics = queryImgDB().getCount();

    Intent intent = new Intent("android.media.action.IMAGE_CAPTURE");

    File photo = createCaptureFile(encodingType);
    intent.putExtra("output", Uri.fromFile(photo));
    this.imageUri = Uri.fromFile(photo);

    this.ctx.startActivityForResult(this, intent, 32 + returnType + 1);
  }

  private File createCaptureFile(int encodingType)
  {
    File photo = null;
    if (encodingType == 0)
      photo = new File(DirectoryManager.getTempDirectoryPath(this.ctx), "Pic.jpg");
    else {
      photo = new File(DirectoryManager.getTempDirectoryPath(this.ctx), "Pic.png");
    }
    return photo;
  }

  public void getImage(int srcType, int returnType)
  {
    Intent intent = new Intent();
    String title = "Get Picture";
    if (this.mediaType == 0) {
      intent.setType("image/*");
    }
    else if (this.mediaType == 1) {
      intent.setType("video/*");
      title = "Get Video";
    }
    else if (this.mediaType == 2)
    {
      intent.setType("*/*");
      title = "Get All";
    }

    intent.setAction("android.intent.action.GET_CONTENT");
    intent.addCategory("android.intent.category.OPENABLE");
    this.ctx.startActivityForResult(this, Intent.createChooser(intent, new String(title)), (srcType + 1) * 16 + returnType + 1);
  }

  public Bitmap scaleBitmap(Bitmap bitmap)
  {
    int newWidth = this.targetWidth;
    int newHeight = this.targetHeight;
    int origWidth = bitmap.getWidth();
    int origHeight = bitmap.getHeight();

    if ((newWidth <= 0) && (newHeight <= 0)) {
      return bitmap;
    }

    if ((newWidth > 0) && (newHeight <= 0)) {
      newHeight = newWidth * origHeight / origWidth;
    }
    else if ((newWidth <= 0) && (newHeight > 0)) {
      newWidth = newHeight * origWidth / origHeight;
    }
    else
    {
      double newRatio = newWidth / newHeight;
      double origRatio = origWidth / origHeight;

      if (origRatio > newRatio)
        newHeight = newWidth * origHeight / origWidth;
      else if (origRatio < newRatio) {
        newWidth = newHeight * origWidth / origHeight;
      }
    }

    return Bitmap.createScaledBitmap(bitmap, newWidth, newHeight, true);
  }

  public void onActivityResult(int requestCode, int resultCode, Intent intent)
  {
    int srcType = requestCode / 16 - 1;
    int destType = requestCode % 16 - 1;

    if (srcType == 1)
    {
      if (resultCode == -1) {
        try
        {
          ExifHelper exif = new ExifHelper();
          if (this.encodingType == 0) {
            exif.createInFile(DirectoryManager.getTempDirectoryPath(this.ctx) + "/Pic.jpg");
            exif.readExifData();
          }

          try
          {
            bitmap = MediaStore.Images.Media.getBitmap(this.ctx.getContentResolver(), this.imageUri);
          } catch (FileNotFoundException e) {
            Uri uri = intent.getData();
            ContentResolver resolver = this.ctx.getContentResolver();
            bitmap = BitmapFactory.decodeStream(resolver.openInputStream(uri));
          }

          Bitmap bitmap = scaleBitmap(bitmap);

          if (destType == 0) {
            processPicture(bitmap);
            checkForDuplicateImage(0);
          }
          else if (destType == 1)
          {
            ContentValues values = new ContentValues();
            values.put("mime_type", "image/jpeg");
            Uri uri = null;
            try {
              uri = this.ctx.getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
            } catch (UnsupportedOperationException e) {
              System.out.println("Can't write to external media storage.");
              try {
                uri = this.ctx.getContentResolver().insert(MediaStore.Images.Media.INTERNAL_CONTENT_URI, values);
              } catch (UnsupportedOperationException ex) {
                System.out.println("Can't write to internal media storage.");
                failPicture("Error capturing image - no media storage found.");
                return;
              }

            }

            OutputStream os = this.ctx.getContentResolver().openOutputStream(uri);
            bitmap.compress(Bitmap.CompressFormat.JPEG, this.mQuality, os);
            os.close();

            if (this.encodingType == 0) {
              exif.createOutFile(FileUtils.getRealPathFromURI(uri, this.ctx));
              exif.writeExifData();
            }

            success(new PluginResult(PluginResult.Status.OK, uri.toString()), this.callbackId);
          }
          bitmap.recycle();
          bitmap = null;
          System.gc();

          checkForDuplicateImage(1);
        } catch (IOException e) {
          e.printStackTrace();
          failPicture("Error capturing image.");
        }

      }
      else if (resultCode == 0) {
        failPicture("Camera cancelled.");
      }
      else
      {
        failPicture("Did not complete!");
      }

    }
    else if ((srcType == 0) || (srcType == 2))
      if (resultCode == -1) {
        Uri uri = intent.getData();
        ContentResolver resolver = this.ctx.getContentResolver();

        if (this.mediaType != 0) {
          success(new PluginResult(PluginResult.Status.OK, uri.toString()), this.callbackId);
        }
        else if (destType == 0) {
          try {
            Bitmap bitmap = BitmapFactory.decodeStream(resolver.openInputStream(uri));
            bitmap = scaleBitmap(bitmap);
            processPicture(bitmap);
            bitmap.recycle();
            bitmap = null;
            System.gc();
          } catch (FileNotFoundException e) {
            e.printStackTrace();
            failPicture("Error retrieving image.");
          }

        }
        else if (destType == 1)
        {
          if ((this.targetHeight > 0) && (this.targetWidth > 0)) {
            try {
              Bitmap bitmap = BitmapFactory.decodeStream(resolver.openInputStream(uri));
              bitmap = scaleBitmap(bitmap);

              String fileName = DirectoryManager.getTempDirectoryPath(this.ctx) + "/resize.jpg";
              OutputStream os = new FileOutputStream(fileName);
              bitmap.compress(Bitmap.CompressFormat.JPEG, this.mQuality, os);
              os.close();

              bitmap.recycle();
              bitmap = null;

              success(new PluginResult(PluginResult.Status.OK, "file://" + fileName), this.callbackId);
              System.gc();
            } catch (Exception e) {
              e.printStackTrace();
              failPicture("Error retrieving image.");
            }
          }
          else {
            success(new PluginResult(PluginResult.Status.OK, uri.toString()), this.callbackId);
          }
        }

      }
      else if (resultCode == 0) {
        failPicture("Selection cancelled.");
      }
      else {
        failPicture("Selection did not complete!");
      }
  }

  private Cursor queryImgDB()
  {
    return this.ctx.getContentResolver().query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, new String[] { "_id" }, null, null, null);
  }

  private void checkForDuplicateImage(int type)
  {
    int diff = 1;
    Cursor cursor = queryImgDB();
    int currentNumOfImages = cursor.getCount();

    if (type == 1) {
      diff = 2;
    }

    if (currentNumOfImages - this.numPics == diff) {
      cursor.moveToLast();
      int id = Integer.valueOf(cursor.getString(cursor.getColumnIndex("_id"))).intValue() - 1;
      Uri uri = Uri.parse(MediaStore.Images.Media.EXTERNAL_CONTENT_URI + "/" + id);
      this.ctx.getContentResolver().delete(uri, null, null);
    }
  }

  public void processPicture(Bitmap bitmap)
  {
    ByteArrayOutputStream jpeg_data = new ByteArrayOutputStream();
    try {
      if (bitmap.compress(Bitmap.CompressFormat.JPEG, this.mQuality, jpeg_data)) {
        byte[] code = jpeg_data.toByteArray();
        byte[] output = Base64.encodeBase64(code);
        String js_out = new String(output);
        success(new PluginResult(PluginResult.Status.OK, js_out), this.callbackId);
        js_out = null;
        output = null;
        code = null;
      }
    }
    catch (Exception e) {
      failPicture("Error compressing image.");
    }
    jpeg_data = null;
  }

  public void failPicture(String err)
  {
    error(new PluginResult(PluginResult.Status.ERROR, err), this.callbackId);
  }
}

可以看到Camera本身被设计一个插件的形式,实现了具体的takePicture函数,至于具体的js端代码和Android本地端代码的通信是怎么样建立起来的,也即js代码怎么和Android的Activity的代码进行数据的交互,那么下面的代码会给你答案了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
public class GapViewClient extends WebViewClient
  {
    DroidGap ctx;
 
    public GapViewClient(DroidGap ctx)
    {
      this.ctx = ctx;
    }
 
    public boolean shouldOverrideUrlLoading(WebView view, String url)
    {
      if (!this.ctx.pluginManager.onOverrideUrlLoading(url))
      {
        if (url.startsWith("tel:")) {
          try {
            Intent intent = new Intent("android.intent.action.DIAL");
            intent.setData(Uri.parse(url));
            DroidGap.this.startActivity(intent);
          } catch (ActivityNotFoundException e) {
            System.out.println("Error dialing " + url + ": " + e.toString());
          }
 
        }
        else if (url.startsWith("geo:")) {
          try {
            Intent intent = new Intent("android.intent.action.VIEW");
            intent.setData(Uri.parse(url));
            DroidGap.this.startActivity(intent);
          } catch (ActivityNotFoundException e) {
            System.out.println("Error showing map " + url + ": " + e.toString());
          }
 
        }
        else if (url.startsWith("mailto:")) {
          try {
            Intent intent = new Intent("android.intent.action.VIEW");
            intent.setData(Uri.parse(url));
            DroidGap.this.startActivity(intent);
          } catch (ActivityNotFoundException e) {
            System.out.println("Error sending email " + url + ": " + e.toString());
          }
 
        }
        else if (url.startsWith("sms:")) {
          try {
            Intent intent = new Intent("android.intent.action.VIEW");
 
            String address = null;
            int parmIndex = url.indexOf('?');
            if (parmIndex == -1) {
              address = url.substring(4);
            }
            else {
              address = url.substring(4, parmIndex);
 
              Uri uri = Uri.parse(url);
              String query = uri.getQuery();
              if ((query != null) &&
                (query.startsWith("body="))) {
                intent.putExtra("sms_body", query.substring(5));
              }
            }
 
            intent.setData(Uri.parse("sms:" + address));
            intent.putExtra("address", address);
            intent.setType("vnd.android-dir/mms-sms");
            DroidGap.this.startActivity(intent);
          } catch (ActivityNotFoundException e) {
            System.out.println("Error sending sms " + url + ":" + e.toString());
          }
 
        }
        else if ((this.ctx.loadInWebView) || (url.startsWith("file://")) || (url.indexOf(this.ctx.baseUrl) == 0) || (DroidGap.this.isUrlWhiteListed(url) != 0)) {
          try
          {
            HashMap params = new HashMap();
            this.ctx.showWebPage(url, true, false, params);
          } catch (ActivityNotFoundException e) {
            System.out.println("Error loading url into DroidGap - " + url + ":" + e.toString());
          }
 
        }
        else {
          try
          {
            Intent intent = new Intent("android.intent.action.VIEW");
            intent.setData(Uri.parse(url));
            DroidGap.this.startActivity(intent);
          } catch (ActivityNotFoundException e) {
            System.out.println("Error loading url " + url + ":" + e.toString());
          }
        }
      }
      return true;
    }
 
    public void onPageFinished(WebView view, String url)
    {
      super.onPageFinished(view, url);
 
      DroidGap.access$208(this.ctx);
 
      if (!url.equals("about:blank")) {
        DroidGap.this.appView.loadUrl("javascript:try{ PhoneGap.onNativeReady.fire();}catch(e){_nativeReady = true;}");
      }
 
      Thread t = new Thread(new Runnable() {
        public void run() {
          try {
            Thread.sleep(2000L);
            DroidGap.GapViewClient.this.ctx.runOnUiThread(new Runnable() {
              public void run() {
                DroidGap.this.appView.setVisibility(0);
                DroidGap.GapViewClient.this.ctx.spinnerStop();
              }
            });
          }
          catch (InterruptedException e)
          {
          }
        }
      });
      t.start();
 
      if (this.ctx.clearHistory) {
        this.ctx.clearHistory = false;
        this.ctx.appView.clearHistory();
      }
 
      if (url.equals("about:blank")) {
        if (this.ctx.callbackServer != null) {
          this.ctx.callbackServer.destroy();
        }
        this.ctx.finish();
      }
    }
 
    public void onReceivedError(WebView view, int errorCode, String description, String failingUrl)
    {
      System.out.println("onReceivedError: Error code=" + errorCode + " Description=" + description + " URL=" + failingUrl);
 
      DroidGap.access$208(this.ctx);
 
      this.ctx.spinnerStop();
 
      this.ctx.onReceivedError(errorCode, description, failingUrl);
    }
 
    public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error)
    {
      String packageName = this.ctx.getPackageName();
      PackageManager pm = this.ctx.getPackageManager();
      try
      {
        ApplicationInfo appInfo = pm.getApplicationInfo(packageName, 128);
        if ((appInfo.flags & 0x2) != 0)
        {
          handler.proceed();
          return;
        }
 
        super.onReceivedSslError(view, handler, error);
      }
      catch (PackageManager.NameNotFoundException e)
      {
        super.onReceivedSslError(view, handler, error);
      }
    }
  }
 
  public class GapClient extends WebChromeClient
  {
    private String TAG = "PhoneGapLog";
    private long MAX_QUOTA = 104857600L;
    private DroidGap ctx;
 
    public GapClient(Context ctx)
    {
      this.ctx = ((DroidGap)ctx);
    }
 
    public boolean onJsAlert(WebView view, String url, String message, JsResult result)
    {
      AlertDialog.Builder dlg = new AlertDialog.Builder(this.ctx);
      dlg.setMessage(message);
      dlg.setTitle("Alert");
      dlg.setCancelable(false);
      dlg.setPositiveButton(17039370, new DialogInterface.OnClickListener(result)
      {
        public void onClick(DialogInterface dialog, int which) {
          this.val$result.confirm();
        }
      });
      dlg.create();
      dlg.show();
      return true;
    }
 
    public boolean onJsConfirm(WebView view, String url, String message, JsResult result)
    {
      AlertDialog.Builder dlg = new AlertDialog.Builder(this.ctx);
      dlg.setMessage(message);
      dlg.setTitle("Confirm");
      dlg.setCancelable(false);
      dlg.setPositiveButton(17039370, new DialogInterface.OnClickListener(result)
      {
        public void onClick(DialogInterface dialog, int which) {
          this.val$result.confirm();
        }
      });
      dlg.setNegativeButton(17039360, new DialogInterface.OnClickListener(result)
      {
        public void onClick(DialogInterface dialog, int which) {
          this.val$result.cancel();
        }
      });
      dlg.create();
      dlg.show();
      return true;
    }
 
    public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result)
    {
      boolean reqOk = false;
      if ((url.indexOf(this.ctx.baseUrl) == 0) || (DroidGap.this.isUrlWhiteListed(url) != 0)) {
        reqOk = true;
      }
 
      if ((reqOk) && (defaultValue != null) && (defaultValue.length() > 3) && (defaultValue.substring(0, 4).equals("gap:")))
      {
        try {
          JSONArray array = new JSONArray(defaultValue.substring(4));
          String service = array.getString(0);
          String action = array.getString(1);
          String callbackId = array.getString(2);
          boolean async = array.getBoolean(3);
          String r = DroidGap.this.pluginManager.exec(service, action, callbackId, message, async);
          result.confirm(r);
        } catch (JSONException e) {
          e.printStackTrace();
        }
 
      }
      else if ((reqOk) && (defaultValue != null) && (defaultValue.equals("gap_poll:"))) {
        String r = DroidGap.this.callbackServer.getJavascript();
        result.confirm(r);
      }
      else if ((reqOk) && (defaultValue != null) && (defaultValue.equals("gap_callbackServer:"))) {
        String r = "";
        if (message.equals("usePolling")) {
          r = "" + DroidGap.this.callbackServer.usePolling();
        }
        else if (message.equals("restartServer")) {
          DroidGap.this.callbackServer.restartServer();
        }
        else if (message.equals("getPort")) {
          r = Integer.toString(DroidGap.this.callbackServer.getPort());
        }
        else if (message.equals("getToken")) {
          r = DroidGap.this.callbackServer.getToken();
        }
        result.confirm(r);
      }
      else if ((reqOk) && (defaultValue != null) && (defaultValue.equals("gap_init:"))) {
        DroidGap.this.appView.setVisibility(0);
        this.ctx.spinnerStop();
        result.confirm("OK");
      }
      else
      {
        JsPromptResult res = result;
        AlertDialog.Builder dlg = new AlertDialog.Builder(this.ctx);
        dlg.setMessage(message);
        EditText input = new EditText(this.ctx);
        if (defaultValue != null) {
          input.setText(defaultValue);
        }
        dlg.setView(input);
        dlg.setCancelable(false);
        dlg.setPositiveButton(17039370, new DialogInterface.OnClickListener(input, res)
        {
          public void onClick(DialogInterface dialog, int which) {
            String usertext = this.val$input.getText().toString();
            this.val$res.confirm(usertext);
          }
        });
        dlg.setNegativeButton(17039360, new DialogInterface.OnClickListener(res)
        {
          public void onClick(DialogInterface dialog, int which) {
            this.val$res.cancel();
          }
        });
        dlg.create();
        dlg.show();
      }
      return true;
    }
 
    public void onExceededDatabaseQuota(String url, String databaseIdentifier, long currentQuota, long estimatedSize, long totalUsedQuota, WebStorage.QuotaUpdater quotaUpdater)
    {
      Log.d(this.TAG, "event raised onExceededDatabaseQuota estimatedSize: " + Long.toString(estimatedSize) + " currentQuota: " + Long.toString(currentQuota) + " totalUsedQuota: " + Long.toString(totalUsedQuota));
 
      if (estimatedSize < this.MAX_QUOTA)
      {
        long newQuota = estimatedSize;
        Log.d(this.TAG, "calling quotaUpdater.updateQuota newQuota: " + Long.toString(newQuota));
        quotaUpdater.updateQuota(newQuota);
      }
      else
      {
        quotaUpdater.updateQuota(currentQuota);
      }
    }
 
    public void onConsoleMessage(String message, int lineNumber, String sourceID)
    {
      Log.d(this.TAG, sourceID + ": Line " + Integer.toString(lineNumber) + " : " + message);
    }
 
    public void onGeolocationPermissionsShowPrompt(String origin, GeolocationPermissions.Callback callback)
    {
      super.onGeolocationPermissionsShowPrompt(origin, callback);
      callback.invoke(origin, true, false);
    }
  }
public class GapViewClient extends WebViewClient
  {
    DroidGap ctx;

    public GapViewClient(DroidGap ctx)
    {
      this.ctx = ctx;
    }

    public boolean shouldOverrideUrlLoading(WebView view, String url)
    {
      if (!this.ctx.pluginManager.onOverrideUrlLoading(url))
      {
        if (url.startsWith("tel:")) {
          try {
            Intent intent = new Intent("android.intent.action.DIAL");
            intent.setData(Uri.parse(url));
            DroidGap.this.startActivity(intent);
          } catch (ActivityNotFoundException e) {
            System.out.println("Error dialing " + url + ": " + e.toString());
          }

        }
        else if (url.startsWith("geo:")) {
          try {
            Intent intent = new Intent("android.intent.action.VIEW");
            intent.setData(Uri.parse(url));
            DroidGap.this.startActivity(intent);
          } catch (ActivityNotFoundException e) {
            System.out.println("Error showing map " + url + ": " + e.toString());
          }

        }
        else if (url.startsWith("mailto:")) {
          try {
            Intent intent = new Intent("android.intent.action.VIEW");
            intent.setData(Uri.parse(url));
            DroidGap.this.startActivity(intent);
          } catch (ActivityNotFoundException e) {
            System.out.println("Error sending email " + url + ": " + e.toString());
          }

        }
        else if (url.startsWith("sms:")) {
          try {
            Intent intent = new Intent("android.intent.action.VIEW");

            String address = null;
            int parmIndex = url.indexOf('?');
            if (parmIndex == -1) {
              address = url.substring(4);
            }
            else {
              address = url.substring(4, parmIndex);

              Uri uri = Uri.parse(url);
              String query = uri.getQuery();
              if ((query != null) &&
                (query.startsWith("body="))) {
                intent.putExtra("sms_body", query.substring(5));
              }
            }

            intent.setData(Uri.parse("sms:" + address));
            intent.putExtra("address", address);
            intent.setType("vnd.android-dir/mms-sms");
            DroidGap.this.startActivity(intent);
          } catch (ActivityNotFoundException e) {
            System.out.println("Error sending sms " + url + ":" + e.toString());
          }

        }
        else if ((this.ctx.loadInWebView) || (url.startsWith("file://")) || (url.indexOf(this.ctx.baseUrl) == 0) || (DroidGap.this.isUrlWhiteListed(url) != 0)) {
          try
          {
            HashMap params = new HashMap();
            this.ctx.showWebPage(url, true, false, params);
          } catch (ActivityNotFoundException e) {
            System.out.println("Error loading url into DroidGap - " + url + ":" + e.toString());
          }

        }
        else {
          try
          {
            Intent intent = new Intent("android.intent.action.VIEW");
            intent.setData(Uri.parse(url));
            DroidGap.this.startActivity(intent);
          } catch (ActivityNotFoundException e) {
            System.out.println("Error loading url " + url + ":" + e.toString());
          }
        }
      }
      return true;
    }

    public void onPageFinished(WebView view, String url)
    {
      super.onPageFinished(view, url);

      DroidGap.access$208(this.ctx);

      if (!url.equals("about:blank")) {
        DroidGap.this.appView.loadUrl("javascript:try{ PhoneGap.onNativeReady.fire();}catch(e){_nativeReady = true;}");
      }

      Thread t = new Thread(new Runnable() {
        public void run() {
          try {
            Thread.sleep(2000L);
            DroidGap.GapViewClient.this.ctx.runOnUiThread(new Runnable() {
              public void run() {
                DroidGap.this.appView.setVisibility(0);
                DroidGap.GapViewClient.this.ctx.spinnerStop();
              }
            });
          }
          catch (InterruptedException e)
          {
          }
        }
      });
      t.start();

      if (this.ctx.clearHistory) {
        this.ctx.clearHistory = false;
        this.ctx.appView.clearHistory();
      }

      if (url.equals("about:blank")) {
        if (this.ctx.callbackServer != null) {
          this.ctx.callbackServer.destroy();
        }
        this.ctx.finish();
      }
    }

    public void onReceivedError(WebView view, int errorCode, String description, String failingUrl)
    {
      System.out.println("onReceivedError: Error code=" + errorCode + " Description=" + description + " URL=" + failingUrl);

      DroidGap.access$208(this.ctx);

      this.ctx.spinnerStop();

      this.ctx.onReceivedError(errorCode, description, failingUrl);
    }

    public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error)
    {
      String packageName = this.ctx.getPackageName();
      PackageManager pm = this.ctx.getPackageManager();
      try
      {
        ApplicationInfo appInfo = pm.getApplicationInfo(packageName, 128);
        if ((appInfo.flags & 0x2) != 0)
        {
          handler.proceed();
          return;
        }

        super.onReceivedSslError(view, handler, error);
      }
      catch (PackageManager.NameNotFoundException e)
      {
        super.onReceivedSslError(view, handler, error);
      }
    }
  }

  public class GapClient extends WebChromeClient
  {
    private String TAG = "PhoneGapLog";
    private long MAX_QUOTA = 104857600L;
    private DroidGap ctx;

    public GapClient(Context ctx)
    {
      this.ctx = ((DroidGap)ctx);
    }

    public boolean onJsAlert(WebView view, String url, String message, JsResult result)
    {
      AlertDialog.Builder dlg = new AlertDialog.Builder(this.ctx);
      dlg.setMessage(message);
      dlg.setTitle("Alert");
      dlg.setCancelable(false);
      dlg.setPositiveButton(17039370, new DialogInterface.OnClickListener(result)
      {
        public void onClick(DialogInterface dialog, int which) {
          this.val$result.confirm();
        }
      });
      dlg.create();
      dlg.show();
      return true;
    }

    public boolean onJsConfirm(WebView view, String url, String message, JsResult result)
    {
      AlertDialog.Builder dlg = new AlertDialog.Builder(this.ctx);
      dlg.setMessage(message);
      dlg.setTitle("Confirm");
      dlg.setCancelable(false);
      dlg.setPositiveButton(17039370, new DialogInterface.OnClickListener(result)
      {
        public void onClick(DialogInterface dialog, int which) {
          this.val$result.confirm();
        }
      });
      dlg.setNegativeButton(17039360, new DialogInterface.OnClickListener(result)
      {
        public void onClick(DialogInterface dialog, int which) {
          this.val$result.cancel();
        }
      });
      dlg.create();
      dlg.show();
      return true;
    }

    public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result)
    {
      boolean reqOk = false;
      if ((url.indexOf(this.ctx.baseUrl) == 0) || (DroidGap.this.isUrlWhiteListed(url) != 0)) {
        reqOk = true;
      }

      if ((reqOk) && (defaultValue != null) && (defaultValue.length() > 3) && (defaultValue.substring(0, 4).equals("gap:")))
      {
        try {
          JSONArray array = new JSONArray(defaultValue.substring(4));
          String service = array.getString(0);
          String action = array.getString(1);
          String callbackId = array.getString(2);
          boolean async = array.getBoolean(3);
          String r = DroidGap.this.pluginManager.exec(service, action, callbackId, message, async);
          result.confirm(r);
        } catch (JSONException e) {
          e.printStackTrace();
        }

      }
      else if ((reqOk) && (defaultValue != null) && (defaultValue.equals("gap_poll:"))) {
        String r = DroidGap.this.callbackServer.getJavascript();
        result.confirm(r);
      }
      else if ((reqOk) && (defaultValue != null) && (defaultValue.equals("gap_callbackServer:"))) {
        String r = "";
        if (message.equals("usePolling")) {
          r = "" + DroidGap.this.callbackServer.usePolling();
        }
        else if (message.equals("restartServer")) {
          DroidGap.this.callbackServer.restartServer();
        }
        else if (message.equals("getPort")) {
          r = Integer.toString(DroidGap.this.callbackServer.getPort());
        }
        else if (message.equals("getToken")) {
          r = DroidGap.this.callbackServer.getToken();
        }
        result.confirm(r);
      }
      else if ((reqOk) && (defaultValue != null) && (defaultValue.equals("gap_init:"))) {
        DroidGap.this.appView.setVisibility(0);
        this.ctx.spinnerStop();
        result.confirm("OK");
      }
      else
      {
        JsPromptResult res = result;
        AlertDialog.Builder dlg = new AlertDialog.Builder(this.ctx);
        dlg.setMessage(message);
        EditText input = new EditText(this.ctx);
        if (defaultValue != null) {
          input.setText(defaultValue);
        }
        dlg.setView(input);
        dlg.setCancelable(false);
        dlg.setPositiveButton(17039370, new DialogInterface.OnClickListener(input, res)
        {
          public void onClick(DialogInterface dialog, int which) {
            String usertext = this.val$input.getText().toString();
            this.val$res.confirm(usertext);
          }
        });
        dlg.setNegativeButton(17039360, new DialogInterface.OnClickListener(res)
        {
          public void onClick(DialogInterface dialog, int which) {
            this.val$res.cancel();
          }
        });
        dlg.create();
        dlg.show();
      }
      return true;
    }

    public void onExceededDatabaseQuota(String url, String databaseIdentifier, long currentQuota, long estimatedSize, long totalUsedQuota, WebStorage.QuotaUpdater quotaUpdater)
    {
      Log.d(this.TAG, "event raised onExceededDatabaseQuota estimatedSize: " + Long.toString(estimatedSize) + " currentQuota: " + Long.toString(currentQuota) + " totalUsedQuota: " + Long.toString(totalUsedQuota));

      if (estimatedSize < this.MAX_QUOTA)
      {
        long newQuota = estimatedSize;
        Log.d(this.TAG, "calling quotaUpdater.updateQuota newQuota: " + Long.toString(newQuota));
        quotaUpdater.updateQuota(newQuota);
      }
      else
      {
        quotaUpdater.updateQuota(currentQuota);
      }
    }

    public void onConsoleMessage(String message, int lineNumber, String sourceID)
    {
      Log.d(this.TAG, sourceID + ": Line " + Integer.toString(lineNumber) + " : " + message);
    }

    public void onGeolocationPermissionsShowPrompt(String origin, GeolocationPermissions.Callback callback)
    {
      super.onGeolocationPermissionsShowPrompt(origin, callback);
      callback.invoke(origin, true, false);
    }
  }

相信开发过webview的同学都知道其中的奥秘了,通过重载webview的WebChromeClient和WebViewClient接口,实现其中的js代码的处理部分,即可和phoneGap中js端的代码进行交互了。

相信现在大家都熟悉了一种利用web方式开发移动APP的解决方式了吧,期待更好的解决方案能够出来。

最后再向大家推荐一个js开发库,来方便的定制APP的样式,怎么样让你的APP看起来更本地化,它就是http://jqtouch.com/,运行结果见开始的图片,如果我把浏览器的部分隐藏掉,谁能轻易分辨出来是一个本地化的APP,还是web的APP(⊙o⊙)?

目前有 2 条留言 其中:访客:1 条, 博主:0 条 引用: 1

  1. chitarra acustica amplificata : 2012年04月11日12:22:06  -49楼 @回复 回复

    Hi! Great blog!

给我留言

留言无头像?


×
腾讯微博