Bladeren bron

更新platform

panxingxin 5 jaren geleden
bovenliggende
commit
fd4fd4ee61
37 gewijzigde bestanden met toevoegingen van 2018 en 39 verwijderingen
  1. 59 18
      platforms/android/android.json
  2. 2 1
      platforms/android/app/src/main/AndroidManifest.xml
  3. 411 0
      platforms/android/app/src/main/java/com/plugin/datepicker/DatePickerPlugin.java
  4. 48 0
      platforms/android/app/src/main/java/com/synconset/FakeR.java
  5. 381 0
      platforms/android/app/src/main/java/com/synconset/ImageFetcher.java
  6. 72 0
      platforms/android/app/src/main/java/com/synconset/ImagePicker.java
  7. 735 0
      platforms/android/app/src/main/java/com/synconset/MultiImageChooserActivity.java
  8. 36 5
      platforms/android/app/src/main/java/io/ionic/keyboard/IonicKeyboard.java
  9. 11 0
      platforms/android/app/src/main/res/anim/image_pop_in.xml
  10. BIN
      platforms/android/app/src/main/res/drawable-hdpi/image_bg.9.png
  11. BIN
      platforms/android/app/src/main/res/drawable-hdpi/loading_icon.png
  12. BIN
      platforms/android/app/src/main/res/drawable-mdpi/ic_action_discard_dark.png
  13. BIN
      platforms/android/app/src/main/res/drawable-mdpi/ic_action_discard_light.png
  14. BIN
      platforms/android/app/src/main/res/drawable-mdpi/ic_action_done_dark.png
  15. BIN
      platforms/android/app/src/main/res/drawable-mdpi/ic_action_done_light.png
  16. BIN
      platforms/android/app/src/main/res/drawable-mdpi/ic_launcher.png
  17. BIN
      platforms/android/app/src/main/res/drawable-xhdpi/ic_action_discard_dark.png
  18. BIN
      platforms/android/app/src/main/res/drawable-xhdpi/ic_action_discard_light.png
  19. BIN
      platforms/android/app/src/main/res/drawable-xhdpi/ic_action_done_dark.png
  20. BIN
      platforms/android/app/src/main/res/drawable-xhdpi/ic_action_done_light.png
  21. BIN
      platforms/android/app/src/main/res/drawable-xhdpi/ic_launcher.png
  22. 12 0
      platforms/android/app/src/main/res/drawable/grid_background.xml
  23. 27 0
      platforms/android/app/src/main/res/layout/actionbar_custom_view_done_discard.xml
  24. 33 0
      platforms/android/app/src/main/res/layout/actionbar_discard_button.xml
  25. 34 0
      platforms/android/app/src/main/res/layout/actionbar_done_button.xml
  26. 23 0
      platforms/android/app/src/main/res/layout/multiselectorgrid.xml
  27. 13 0
      platforms/android/app/src/main/res/values-de/multiimagechooser_strings_de.xml
  28. 13 0
      platforms/android/app/src/main/res/values-es/multiimagechooser_strings_es.xml
  29. 12 0
      platforms/android/app/src/main/res/values-fr/multiimagechooser_strings_fr.xml
  30. 13 0
      platforms/android/app/src/main/res/values-hu/multiimagechooser_strings_hu.xml
  31. 13 0
      platforms/android/app/src/main/res/values-ja/multiimagechooser_strings_ja.xml
  32. 13 0
      platforms/android/app/src/main/res/values-ko/multiimagechooser_strings_ko.xml
  33. 13 0
      platforms/android/app/src/main/res/values/multiimagechooser_strings_en.xml
  34. 5 0
      platforms/android/app/src/main/res/values/themes.xml
  35. 10 4
      platforms/android/app/src/main/res/xml/config.xml
  36. 28 10
      platforms/android/platform_www/cordova_plugins.js
  37. 1 1
      platforms/android/project.properties

+ 59 - 18
platforms/android/android.json

@@ -71,6 +71,14 @@
             {
               "xml": "<feature name=\"BarcodeScanner\"><param name=\"android-package\" value=\"com.phonegap.plugins.barcodescanner.BarcodeScanner\" /></feature>",
               "count": 1
+            },
+            {
+              "xml": "<feature name=\"ImagePicker\"><param name=\"android-package\" value=\"com.synconset.ImagePicker\" /></feature>",
+              "count": 1
+            },
+            {
+              "xml": "<feature name=\"DatePickerPlugin\"><param name=\"android-package\" value=\"com.plugin.datepicker.DatePickerPlugin\" /></feature>",
+              "count": 1
             }
           ],
           "/widget": [
@@ -115,6 +123,10 @@
             {
               "xml": "<activity android:label=\"Share\" android:name=\"com.google.zxing.client.android.encode.EncodeActivity\" />",
               "count": 1
+            },
+            {
+              "xml": "<activity android:label=\"@string/multi_app_name\" android:name=\"com.synconset.MultiImageChooserActivity\" android:theme=\"@android:style/Theme.Holo.Light\"></activity>",
+              "count": 1
             }
           ],
           "/manifest": [
@@ -124,7 +136,7 @@
             },
             {
               "xml": "<uses-permission android:name=\"android.permission.WRITE_EXTERNAL_STORAGE\" />",
-              "count": 1
+              "count": 2
             },
             {
               "xml": "<uses-permission android:name=\"android.permission.READ_EXTERNAL_STORAGE\" />",
@@ -184,10 +196,6 @@
               "xml": "<feature name=\"Badge\"><param name=\"android-package\" value=\"de.appplant.cordova.plugin.badge.Badge\" /></feature>",
               "count": 1
             },
-            {
-              "xml": "<feature name=\"Keyboard\"><param name=\"android-package\" onload=\"true\" value=\"io.ionic.keyboard.IonicKeyboard\" /></feature>",
-              "count": 1
-            },
             {
               "xml": "<allow-navigation href=\"http://localhost/*\" />",
               "count": 1
@@ -207,9 +215,18 @@
             {
               "xml": "<feature name=\"IonicWebView\"><param name=\"android-package\" value=\"com.ionicframework.cordova.webview.IonicWebView\" /></feature>",
               "count": 1
+            },
+            {
+              "xml": "<feature name=\"CDVIonicKeyboard\"><param name=\"android-package\" onload=\"true\" value=\"io.ionic.keyboard.CDVIonicKeyboard\" /></feature>",
+              "count": 1
             }
           ]
         }
+      },
+      "app/src/main/res/values/nativekeyboard.xml": {
+        "parents": {
+          "/*": []
+        }
       }
     }
   },
@@ -248,9 +265,6 @@
     "cordova-plugin-file-transfer": {
       "PACKAGE_NAME": "com.ionicframework.sgZongLi"
     },
-    "cordova-plugin-ionic-keyboard": {
-      "PACKAGE_NAME": "com.ionicframework.sgZongLi"
-    },
     "cordova-plugin-ionic-webview": {
       "ANDROID_SUPPORT_ANNOTATIONS_VERSION": "27.+",
       "PACKAGE_NAME": "com.ionicframework.sgZongLi"
@@ -273,6 +287,15 @@
     "phonegap-plugin-barcodescanner": {
       "ANDROID_SUPPORT_V4_VERSION": "27.+",
       "PACKAGE_NAME": "com.ionicframework.sgZongLi"
+    },
+    "cordova-plugin-image-picker": {
+      "PACKAGE_NAME": "com.ionicframework.sgZongLi"
+    },
+    "cordova-plugin-ionic-keyboard": {
+      "PACKAGE_NAME": "com.ionicframework.sgZongLi"
+    },
+    "cordova-plugin-datepicker": {
+      "PACKAGE_NAME": "com.ionicframework.sgZongLi"
     }
   },
   "dependent_plugins": {},
@@ -612,14 +635,6 @@
         "window.FileTransfer"
       ]
     },
-    {
-      "id": "cordova-plugin-ionic-keyboard.keyboard",
-      "file": "plugins/cordova-plugin-ionic-keyboard/www/android/keyboard.js",
-      "pluginId": "cordova-plugin-ionic-keyboard",
-      "clobbers": [
-        "window.Keyboard"
-      ]
-    },
     {
       "id": "cordova-plugin-ionic-webview.IonicWebView",
       "file": "plugins/cordova-plugin-ionic-webview/src/www/util.js",
@@ -685,6 +700,30 @@
       "clobbers": [
         "cordova.plugins.barcodeScanner"
       ]
+    },
+    {
+      "id": "cordova-plugin-image-picker.ImagePicker",
+      "file": "plugins/cordova-plugin-image-picker/www/imagepicker.js",
+      "pluginId": "cordova-plugin-image-picker",
+      "clobbers": [
+        "plugins.imagePicker"
+      ]
+    },
+    {
+      "id": "cordova-plugin-ionic-keyboard.keyboard",
+      "file": "plugins/cordova-plugin-ionic-keyboard/www/android/keyboard.js",
+      "pluginId": "cordova-plugin-ionic-keyboard",
+      "clobbers": [
+        "window.Keyboard"
+      ]
+    },
+    {
+      "id": "cordova-plugin-datepicker.DatePicker",
+      "file": "plugins/cordova-plugin-datepicker/www/android/DatePicker.js",
+      "pluginId": "cordova-plugin-datepicker",
+      "clobbers": [
+        "datePicker"
+      ]
     }
   ],
   "plugin_metadata": {
@@ -699,13 +738,15 @@
     "cordova-plugin-device": "2.0.2",
     "cordova-plugin-file-opener2": "2.2.1",
     "cordova-plugin-file-transfer": "1.7.1",
-    "cordova-plugin-ionic-keyboard": "2.1.3",
     "cordova-plugin-ionic-webview": "4.0.1",
     "cordova-plugin-local-notification": "0.9.0-beta.2",
     "cordova-plugin-splashscreen": "5.0.2",
     "cordova-plugin-statusbar": "2.4.2",
     "cordova-plugin-whitelist": "1.3.3",
     "cordova-sqlite-storage": "3.2.0",
-    "phonegap-plugin-barcodescanner": "8.1.0"
+    "phonegap-plugin-barcodescanner": "8.1.0",
+    "cordova-plugin-image-picker": "1.1.1",
+    "cordova-plugin-ionic-keyboard": "2.2.0",
+    "cordova-plugin-datepicker": "0.9.3"
   }
 }

+ 2 - 1
platforms/android/app/src/main/AndroidManifest.xml

@@ -1,5 +1,5 @@
 <?xml version='1.0' encoding='utf-8'?>
-<manifest android:hardwareAccelerated="true" android:versionCode="10008" android:versionName="1.0.8" package="com.ionicframework.sgZongLi" xmlns:android="http://schemas.android.com/apk/res/android">
+<manifest android:hardwareAccelerated="true" android:versionCode="10021" android:versionName="1.0.21" package="com.ionicframework.sgZongLi" xmlns:android="http://schemas.android.com/apk/res/android">
     <supports-screens android:anyDensity="true" android:largeScreens="true" android:normalScreens="true" android:resizeable="true" android:smallScreens="true" android:xlargeScreens="true" />
     <uses-permission android:name="android.permission.INTERNET" />
     <application android:hardwareAccelerated="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:networkSecurityConfig="@xml/network_config" android:supportsRtl="true">
@@ -24,6 +24,7 @@
         <activity android:exported="false" android:launchMode="singleInstance" android:name="de.appplant.cordova.plugin.localnotification.ClickReceiver" android:theme="@android:style/Theme.Translucent" />
         <activity android:clearTaskOnLaunch="true" android:configChanges="orientation|keyboardHidden|screenSize" android:exported="false" android:name="com.google.zxing.client.android.CaptureActivity" android:theme="@android:style/Theme.NoTitleBar.Fullscreen" android:windowSoftInputMode="stateAlwaysHidden" />
         <activity android:label="Share" android:name="com.google.zxing.client.android.encode.EncodeActivity" />
+        <activity android:label="@string/multi_app_name" android:name="com.synconset.MultiImageChooserActivity" android:theme="@android:style/Theme.Holo.Light" />
     </application>
     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
     <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

+ 411 - 0
platforms/android/app/src/main/java/com/plugin/datepicker/DatePickerPlugin.java

@@ -0,0 +1,411 @@
+/**
+ * @author Bikas Vaibhav (http://bikasv.com) 2013
+ * Rewrote the plug-in at https://github.com/phonegap/phonegap-plugins/tree/master/Android/DatePicker
+ * It can now accept `min` and `max` dates for DatePicker.
+ *
+ * @author Andre Moraes (https://github.com/andrelsmoraes)
+ * Refactored code, changed default mode to show date and time dialog.
+ * Added options `okText`, `cancelText`, `todayText`, `nowText`, `is24Hour`.
+ *
+ * @author Diego Silva (https://github.com/diego-silva)
+ * Added option `titleText`.
+ */
+
+package com.plugin.datepicker;
+
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.TimeZone;
+import java.util.Random;
+
+import org.apache.cordova.CallbackContext;
+import org.apache.cordova.CordovaPlugin;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import android.annotation.SuppressLint;
+import android.app.DatePickerDialog;
+import android.app.DatePickerDialog.OnDateSetListener;
+import android.app.TimePickerDialog;
+import android.app.TimePickerDialog.OnTimeSetListener;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.os.Build;
+import android.util.Log;
+import android.widget.DatePicker;
+import android.widget.DatePicker.OnDateChangedListener;
+import android.widget.TimePicker;
+
+@SuppressLint("NewApi")
+public class DatePickerPlugin extends CordovaPlugin {
+
+	private static final String ACTION_DATE = "date";
+	private static final String ACTION_TIME = "time";
+	private static final String RESULT_ERROR = "error";
+	private static final String RESULT_CANCEL = "cancel";
+	private final String pluginName = "DatePickerPlugin";
+	
+	// On some devices, onDateSet or onTimeSet are being called twice
+	private boolean called = false;
+	private boolean canceled = false;
+
+	@Override
+	public boolean execute(final String action, final JSONArray data, final CallbackContext callbackContext) {
+		Log.d(pluginName, "DatePicker called with options: " + data);
+		called = false;
+		canceled = false;
+		boolean result = false;
+
+		this.show(data, callbackContext);
+		result = true;
+
+		return result;
+	}
+
+	public synchronized void show(final JSONArray data, final CallbackContext callbackContext) {
+		DatePickerPlugin datePickerPlugin = this;
+		Context currentCtx = cordova.getActivity();
+		Runnable runnable;
+		JsonDate jsonDate = new JsonDate().fromJson(data);
+		    
+    // Retrieve Android theme
+    JSONObject options = data.optJSONObject(0);
+    int theme = options.optInt("androidTheme", 1);
+
+		if (ACTION_TIME.equalsIgnoreCase(jsonDate.action)) {
+			runnable = runnableTimeDialog(datePickerPlugin, theme, currentCtx,
+					callbackContext, jsonDate, Calendar.getInstance(TimeZone.getDefault()));
+
+		} else {
+			runnable = runnableDatePicker(datePickerPlugin, theme, currentCtx, callbackContext, jsonDate);
+		}
+
+		cordova.getActivity().runOnUiThread(runnable);
+	}
+	
+	private TimePicker timePicker;
+	private int timePickerHour = 0;
+	private int timePickerMinute = 0;
+	
+	private Runnable runnableTimeDialog(final DatePickerPlugin datePickerPlugin,
+			final int theme, final Context currentCtx, final CallbackContext callbackContext,
+			final JsonDate jsonDate, final Calendar calendarDate) {
+		return new Runnable() {
+			@Override
+			public void run() {
+				final TimeSetListener timeSetListener = new TimeSetListener(datePickerPlugin, callbackContext, calendarDate);
+				final TimePickerDialog timeDialog = new TimePickerDialog(currentCtx, theme, timeSetListener, jsonDate.hour,
+						jsonDate.minutes, jsonDate.is24Hour) {
+					public void onTimeChanged(TimePicker view, int hourOfDay, int minute) {
+						timePicker = view;
+						timePickerHour = hourOfDay;
+						timePickerMinute = minute;
+					}
+				};
+				if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
+					timeDialog.setCancelable(true);
+					timeDialog.setCanceledOnTouchOutside(false);
+					
+					if (!jsonDate.titleText.isEmpty()){
+						timeDialog.setTitle(jsonDate.titleText);
+					}
+					if (!jsonDate.nowText.isEmpty()){
+						timeDialog.setButton(DialogInterface.BUTTON_NEUTRAL, jsonDate.nowText, new DialogInterface.OnClickListener() {
+							@Override
+							public void onClick(DialogInterface dialog, int which) {
+								if (timePicker != null) {
+									Calendar now = Calendar.getInstance();
+									timeSetListener.onTimeSet(timePicker, now.get(Calendar.HOUR_OF_DAY), now.get(Calendar.MINUTE));
+								}
+							}
+						});
+			        }
+					String labelCancel = jsonDate.cancelText.isEmpty() ? currentCtx.getString(android.R.string.cancel) : jsonDate.cancelText; 
+					timeDialog.setButton(DialogInterface.BUTTON_NEGATIVE, labelCancel, new DialogInterface.OnClickListener() {
+						@Override
+						public void onClick(DialogInterface dialog, int which) {
+							canceled = true;
+							callbackContext.error(RESULT_CANCEL);
+						}
+					});
+					String labelOk = jsonDate.okText.isEmpty() ? currentCtx.getString(android.R.string.ok) : jsonDate.okText;
+					timeDialog.setButton(DialogInterface.BUTTON_POSITIVE, labelOk, timeDialog);
+				}
+				timeDialog.show();
+				timeDialog.updateTime(new Random().nextInt(23), new Random().nextInt(59));
+				timeDialog.updateTime(jsonDate.hour, jsonDate.minutes);
+			}
+		};
+	}
+	
+	private Runnable runnableDatePicker(
+			final DatePickerPlugin datePickerPlugin,
+			final int theme, final Context currentCtx,
+			final CallbackContext callbackContext, final JsonDate jsonDate) {
+		return new Runnable() {
+			@Override
+			public void run() {
+				final DateSetListener dateSetListener = new DateSetListener(datePickerPlugin, theme, callbackContext, jsonDate);
+				final DatePickerDialog dateDialog = new DatePickerDialog(currentCtx, theme, dateSetListener, jsonDate.year,
+						jsonDate.month, jsonDate.day);
+				if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
+					prepareDialog(dateDialog, dateSetListener, callbackContext, currentCtx, jsonDate);
+				}
+				else {
+					prepareDialogPreHoneycomb(dateDialog, callbackContext, currentCtx, jsonDate);
+				}
+				
+				dateDialog.show();
+			}
+		};
+	}
+	
+	private void prepareDialog(final DatePickerDialog dateDialog, final OnDateSetListener dateListener, 
+			final CallbackContext callbackContext, Context currentCtx, JsonDate jsonDate) {
+		dateDialog.setCancelable(true);
+		dateDialog.setCanceledOnTouchOutside(false);
+		if (!jsonDate.titleText.isEmpty()){
+			dateDialog.setTitle(jsonDate.titleText);
+		}
+		if (!jsonDate.todayText.isEmpty()){
+            dateDialog.setButton(DialogInterface.BUTTON_NEUTRAL, jsonDate.todayText, new DialogInterface.OnClickListener() {
+                @Override
+                public void onClick(DialogInterface dialog, int which) {
+                	Calendar now = Calendar.getInstance();
+                	DatePicker datePicker = dateDialog.getDatePicker();
+					dateListener.onDateSet(datePicker, now.get(Calendar.YEAR), now.get(Calendar.MONTH), now.get(Calendar.DAY_OF_MONTH));
+                }
+            });
+        }
+		String labelCancel = jsonDate.cancelText.isEmpty() ? currentCtx.getString(android.R.string.cancel) : jsonDate.cancelText; 
+		dateDialog.setButton(DialogInterface.BUTTON_NEGATIVE, labelCancel, new DialogInterface.OnClickListener() {
+            @Override
+            public void onClick(DialogInterface dialog, int which) {
+				canceled = true;
+				callbackContext.error(RESULT_CANCEL);
+            }
+        });
+		String labelOk = jsonDate.okText.isEmpty() ? currentCtx.getString(android.R.string.ok) : jsonDate.okText;
+		dateDialog.setButton(DialogInterface.BUTTON_POSITIVE, labelOk, new DialogInterface.OnClickListener() {
+            @Override
+            public void onClick(DialogInterface dialog, int which) {
+				DatePicker datePicker = dateDialog.getDatePicker();
+				datePicker.clearFocus();
+				dateListener.onDateSet(datePicker, datePicker.getYear(), datePicker.getMonth(), datePicker.getDayOfMonth());
+            }
+        });
+        
+        DatePicker dp = dateDialog.getDatePicker();
+		if(jsonDate.minDate > 0) {
+			dp.setMinDate(jsonDate.minDate);
+		}
+		if(jsonDate.maxDate > 0 && jsonDate.maxDate > jsonDate.minDate) {
+			dp.setMaxDate(jsonDate.maxDate);
+		}
+	}
+	
+	private void prepareDialogPreHoneycomb(DatePickerDialog dateDialog,
+			final CallbackContext callbackContext, Context currentCtx, final JsonDate jsonDate){
+		java.lang.reflect.Field mDatePickerField = null;
+		try {
+			mDatePickerField = dateDialog.getClass().getDeclaredField("mDatePicker");
+		} catch (NoSuchFieldException e) {
+			callbackContext.error(RESULT_ERROR);
+		}
+		mDatePickerField.setAccessible(true);
+		DatePicker pickerView = null;
+		try {
+			pickerView = (DatePicker) mDatePickerField.get(dateDialog);
+		} catch (IllegalArgumentException e) {
+			callbackContext.error(RESULT_ERROR);
+		} catch (IllegalAccessException e) {
+			callbackContext.error(RESULT_ERROR);
+		}
+
+		final Calendar startDate = Calendar.getInstance();
+		startDate.setTimeInMillis(jsonDate.minDate);
+		final Calendar endDate = Calendar.getInstance();
+		endDate.setTimeInMillis(jsonDate.maxDate);
+
+		final int minYear = startDate.get(Calendar.YEAR);
+	    final int minMonth = startDate.get(Calendar.MONTH);
+	    final int minDay = startDate.get(Calendar.DAY_OF_MONTH);
+	    final int maxYear = endDate.get(Calendar.YEAR);
+	    final int maxMonth = endDate.get(Calendar.MONTH);
+	    final int maxDay = endDate.get(Calendar.DAY_OF_MONTH);
+
+		if(startDate !=null || endDate != null) {
+			pickerView.init(jsonDate.year, jsonDate.month, jsonDate.day, new OnDateChangedListener() {
+                @Override
+				public void onDateChanged(DatePicker view, int year, int month, int day) {
+                	if(jsonDate.maxDate > 0 && jsonDate.maxDate > jsonDate.minDate) {
+	                	if(year > maxYear || month > maxMonth && year == maxYear || day > maxDay && year == maxYear && month == maxMonth){
+	                		view.updateDate(maxYear, maxMonth, maxDay);
+	                	}
+                	}
+                	if(jsonDate.minDate > 0) {
+	                	if(year < minYear || month < minMonth && year == minYear || day < minDay && year == minYear && month == minMonth) {
+	                		view.updateDate(minYear, minMonth, minDay);
+	                	}
+                	}
+            	}
+            });
+		}
+	}
+
+	private final class DateSetListener implements OnDateSetListener {
+		private JsonDate jsonDate;
+		private final DatePickerPlugin datePickerPlugin;
+		private final CallbackContext callbackContext;
+		private final int theme;
+
+		private DateSetListener(DatePickerPlugin datePickerPlugin, int theme, CallbackContext callbackContext, JsonDate jsonDate) {
+			this.datePickerPlugin = datePickerPlugin;
+			this.callbackContext = callbackContext;
+			this.jsonDate = jsonDate;
+      this.theme = theme;
+		}
+
+		/**
+		 * Return a string containing the date in the format YYYY/MM/DD or call TimeDialog if action != date
+		 */
+		@Override
+		public void onDateSet(final DatePicker view, final int year, final int monthOfYear, final int dayOfMonth) {
+			if (canceled || called) {
+				return;
+			}
+			called = true;
+			canceled = false;
+			
+			Log.d("onDateSet", "called: " + called);
+			Log.d("onDateSet", "canceled: " + canceled);
+			Log.d("onDateSet", "mode: " + jsonDate.action);
+			
+			if (ACTION_DATE.equalsIgnoreCase(jsonDate.action)) {
+				String returnDate = year + "/" + (monthOfYear + 1) + "/" + dayOfMonth;
+				Log.d("onDateSet", "returnDate: " + returnDate);
+				
+				callbackContext.success(returnDate);
+			
+			} else {
+				// Open time dialog
+				Calendar selectedDate = Calendar.getInstance();
+				selectedDate.set(Calendar.YEAR, year);
+				selectedDate.set(Calendar.MONTH, monthOfYear);
+				selectedDate.set(Calendar.DAY_OF_MONTH, dayOfMonth);
+				
+				cordova.getActivity().runOnUiThread(runnableTimeDialog(datePickerPlugin, theme, cordova.getActivity(),
+						callbackContext, jsonDate, selectedDate));
+			}
+		}
+	}
+
+	private final class TimeSetListener implements OnTimeSetListener {
+		private Calendar calendarDate;
+		private final CallbackContext callbackContext;
+
+		private TimeSetListener(DatePickerPlugin datePickerPlugin, CallbackContext callbackContext, Calendar selectedDate) {
+			this.callbackContext = callbackContext;
+			this.calendarDate = selectedDate != null ? selectedDate : Calendar.getInstance();
+		}
+
+		/**
+		 * Return the current date with the time modified as it was set in the
+		 * time picker.
+		 */
+		@Override
+		public void onTimeSet(final TimePicker view, final int hourOfDay, final int minute) {
+			if (canceled) {
+				return;
+			}
+			
+			calendarDate.set(Calendar.HOUR_OF_DAY, hourOfDay);
+			calendarDate.set(Calendar.MINUTE, minute);
+			calendarDate.set(Calendar.SECOND, 0);
+
+			SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
+			sdf.setTimeZone(TimeZone.getTimeZone("GMT"));
+			String toReturn = sdf.format(calendarDate.getTime());
+
+			callbackContext.success(toReturn);
+		}
+	}
+	
+	private final class JsonDate {
+		
+		private String action = ACTION_DATE;
+		private String titleText = "";
+		private String okText = "";
+		private String cancelText = "";
+		private String todayText = "";
+		private String nowText = "";
+		private long minDate = 0;
+		private long maxDate = 0;
+		private int month = 0;
+		private int day = 0;
+		private int year = 0;
+		private int hour = 0;
+		private int minutes = 0;
+		private boolean is24Hour = false;
+
+		public JsonDate() {
+			reset(Calendar.getInstance());
+		}
+
+		private void reset(Calendar c) {
+			year = c.get(Calendar.YEAR);
+			month = c.get(Calendar.MONTH);
+			day = c.get(Calendar.DAY_OF_MONTH);
+			hour = c.get(Calendar.HOUR_OF_DAY);
+			minutes = c.get(Calendar.MINUTE);
+		}
+
+		public JsonDate fromJson(JSONArray data) {
+			try {
+				JSONObject obj = data.getJSONObject(0);
+				action = isNotEmpty(obj, "mode") ? obj.getString("mode")
+						: ACTION_DATE;
+
+				minDate = isNotEmpty(obj, "minDate") ? obj.getLong("minDate") : 0l;
+				maxDate = isNotEmpty(obj, "maxDate") ? obj.getLong("maxDate") : 0l;
+
+				titleText = isNotEmpty(obj, "titleText") ? obj.getString("titleText") : "";
+				okText = isNotEmpty(obj, "okText") ? obj.getString("okText") : "";
+				cancelText = isNotEmpty(obj, "cancelText") ? obj
+						.getString("cancelText") : "";
+				todayText = isNotEmpty(obj, "todayText") ? obj
+						.getString("todayText") : "";
+				nowText = isNotEmpty(obj, "nowText") ? obj.getString("nowText")
+						: "";
+				is24Hour = isNotEmpty(obj, "is24Hour") ? obj.getBoolean("is24Hour")
+						: false;
+
+				String optionDate = obj.getString("date");
+
+				String[] datePart = optionDate.split("/");
+				month = Integer.parseInt(datePart[0]) - 1;
+				day = Integer.parseInt(datePart[1]);
+				year = Integer.parseInt(datePart[2]);
+				hour = Integer.parseInt(datePart[3]);
+				minutes = Integer.parseInt(datePart[4]);
+
+			} catch (JSONException e) {
+				reset(Calendar.getInstance());
+			}
+
+			return this;
+		}
+
+		public boolean isNotEmpty(JSONObject object, String key)
+				throws JSONException {
+			return object.has(key)
+					&& !object.isNull(key)
+					&& object.get(key).toString().length() > 0
+					&& !JSONObject.NULL.toString().equals(
+							object.get(key).toString());
+		}
+
+	}
+
+}

+ 48 - 0
platforms/android/app/src/main/java/com/synconset/FakeR.java

@@ -0,0 +1,48 @@
+/*
+The MIT License
+
+Copyright (c) 2010 Matt Kane
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+Code taken from: https://github.com/wildabeast/BarcodeScanner
+*/
+package com.synconset;
+
+import android.app.Activity;
+import android.content.Context;
+
+/**
+ * R replacement for PhoneGap Build.
+ *
+ * ([^.\w])R\.(\w+)\.(\w+)
+ * $1fakeR("$2", "$3")
+ *
+ * @author Maciej Nux Jaros
+ */
+public class FakeR {
+	private Context context;
+	private String packageName;
+
+	public FakeR(Activity activity) {
+		context = activity.getApplicationContext();
+		packageName = context.getPackageName();
+	}
+
+	public FakeR(Context context) {
+		this.context = context;
+		packageName = context.getPackageName();
+	}
+
+	public int getId(String group, String key) {
+		return context.getResources().getIdentifier(key, group, packageName);
+	}
+
+	public static int getId(Context context, String group, String key) {
+		return context.getResources().getIdentifier(key, group, context.getPackageName());
+	}
+}

+ 381 - 0
platforms/android/app/src/main/java/com/synconset/ImageFetcher.java

@@ -0,0 +1,381 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.synconset;
+
+import java.lang.ref.SoftReference;
+import java.lang.ref.WeakReference;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.RejectedExecutionException;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Color;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.Matrix;
+import android.os.AsyncTask;
+import android.os.Build;
+import android.os.Handler;
+import android.provider.MediaStore;
+import android.util.Log;
+import android.view.View;
+import android.view.animation.Animation;
+import android.view.animation.AnimationUtils;
+import android.widget.ImageView;
+
+/**
+ * This helper class download images from the Internet and binds those with the
+ * provided ImageView.
+ * 
+ * <p>
+ * It requires the INTERNET permission, which should be added to your
+ * application's manifest file.
+ * </p>
+ * 
+ * A local cache of downloaded images is maintained internally to improve
+ * performance.
+ */
+public class ImageFetcher {
+
+    private int colWidth;
+    private long origId;
+    private ExecutorService executor;
+
+    public ImageFetcher() {
+        executor = Executors.newCachedThreadPool();
+    }
+
+    public void fetch(Integer id, ImageView imageView, int colWidth, int rotate) {
+        resetPurgeTimer();
+        this.colWidth = colWidth;
+        this.origId = id;
+        Bitmap bitmap = getBitmapFromCache(id);
+
+        if (bitmap == null) {
+            forceDownload(id, imageView, rotate);
+        } else {
+            cancelPotentialDownload(id, imageView);
+            imageView.setImageBitmap(bitmap);
+        }
+    }
+
+    /**
+     * Same as download but the image is always downloaded and the cache is not
+     * used. Kept private at the moment as its interest is not clear.
+     */
+    private void forceDownload(Integer position, ImageView imageView, int rotate) {
+        if (position == null) {
+            imageView.setImageDrawable(null);
+            return;
+        }
+
+        if (cancelPotentialDownload(position, imageView)) {
+            BitmapFetcherTask task = new BitmapFetcherTask(imageView.getContext(), imageView, rotate);
+            DownloadedDrawable downloadedDrawable = new DownloadedDrawable(imageView.getContext(), task, origId);
+            imageView.setImageDrawable(downloadedDrawable);
+            imageView.setMinimumHeight(colWidth);
+
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
+                task.executeOnExecutor(executor, position);
+            } else {
+                try {
+                    task.execute(position);
+                } catch (RejectedExecutionException e) {
+                    // Oh :(
+                }
+            }
+
+        }
+    }
+
+    /**
+     * Returns true if the current download has been canceled or if there was no
+     * download in progress on this image view. Returns false if the download in
+     * progress deals with the same url. The download is not stopped in that
+     * case.
+     */
+    private static boolean cancelPotentialDownload(Integer position, ImageView imageView) {
+        BitmapFetcherTask bitmapDownloaderTask = getBitmapDownloaderTask(imageView);
+        long origId = getOrigId(imageView);
+
+        if (bitmapDownloaderTask != null) {
+            Integer bitmapPosition = bitmapDownloaderTask.position;
+            if ((bitmapPosition == null) || (!bitmapPosition.equals(position))) {
+                // Log.d("DAVID", "Canceling...");
+                MediaStore.Images.Thumbnails.cancelThumbnailRequest(imageView.getContext().getContentResolver(),
+                        origId, 12345);
+                bitmapDownloaderTask.cancel(true);
+            } else {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * @param imageView
+     *            Any imageView
+     * @return Retrieve the currently active download task (if any) associated
+     *         with this imageView. null if there is no such task.
+     */
+    private static BitmapFetcherTask getBitmapDownloaderTask(ImageView imageView) {
+        if (imageView != null) {
+            Drawable drawable = imageView.getDrawable();
+            if (drawable instanceof DownloadedDrawable) {
+                DownloadedDrawable downloadedDrawable = (DownloadedDrawable) drawable;
+                return downloadedDrawable.getBitmapDownloaderTask();
+            }
+        }
+        return null;
+    }
+
+    private static long getOrigId(ImageView imageView) {
+        if (imageView != null) {
+            Drawable drawable = imageView.getDrawable();
+            if (drawable instanceof DownloadedDrawable) {
+                DownloadedDrawable downloadedDrawable = (DownloadedDrawable) drawable;
+                return downloadedDrawable.getOrigId();
+            }
+        }
+        return -1;
+    }
+
+    /**
+     * The actual AsyncTask that will asynchronously download the image.
+     */
+    class BitmapFetcherTask extends AsyncTask<Integer, Void, Bitmap> {
+        private Integer position;
+        private final WeakReference<ImageView> imageViewReference;
+        private final Context mContext;
+        private final int rotate;
+
+        public BitmapFetcherTask(Context context, ImageView imageView, int rotate) {
+            imageViewReference = new WeakReference<ImageView>(imageView);
+            mContext = context;
+            this.rotate = rotate;
+        }
+
+        /**
+         * Actual download method.
+         */
+        @Override
+        protected Bitmap doInBackground(Integer... params) {
+        	try {
+	            position = params[0];
+	            if (isCancelled()) {
+	                return null;
+	            }
+	            Bitmap thumb = MediaStore.Images.Thumbnails.getThumbnail(mContext.getContentResolver(), position, 12345,
+	                    MediaStore.Images.Thumbnails.MINI_KIND, null);
+	            if (isCancelled()) {
+	                return null;
+	            }
+	            if (thumb == null) {
+	                return null;
+	            } else {
+	                if (isCancelled()) {
+	                    return null;
+	                } else {
+	                    if (rotate != 0) {
+	                        Matrix matrix = new Matrix();
+	                        matrix.setRotate(rotate);
+	                        thumb = Bitmap.createBitmap(thumb, 0, 0, thumb.getWidth(), thumb.getHeight(), matrix, true);
+	                    }
+	                    return thumb;
+	                }
+	            }
+        	}catch(OutOfMemoryError error) {
+        		clearCache();
+        		return null;
+        	}
+
+        }
+
+        private void setInvisible() {
+            // Log.d("COLLAGE", "Setting something invisible...");
+            if (imageViewReference != null) {
+                final ImageView imageView = imageViewReference.get();
+                BitmapFetcherTask bitmapDownloaderTask = getBitmapDownloaderTask(imageView);
+                if (this == bitmapDownloaderTask) {
+                    imageView.setVisibility(View.GONE);
+                    imageView.setClickable(false);
+                    imageView.setEnabled(false);
+                }
+            }
+        }
+
+        /**
+         * Once the image is downloaded, associates it to the imageView
+         */
+        @Override
+        protected void onPostExecute(Bitmap bitmap) {
+            if (isCancelled()) {
+                bitmap = null;
+            }
+            addBitmapToCache(position, bitmap);
+            if (imageViewReference != null) {
+                ImageView imageView = imageViewReference.get();
+                BitmapFetcherTask bitmapDownloaderTask = getBitmapDownloaderTask(imageView);
+                if (this == bitmapDownloaderTask) {
+                    imageView.setImageBitmap(bitmap);
+                    Animation anim = AnimationUtils.loadAnimation(imageView.getContext(), android.R.anim.fade_in);
+                    imageView.setAnimation(anim);
+                    anim.start();
+                }
+            } else {
+                setInvisible();
+            }
+        }
+    }
+
+    /**
+     * A fake Drawable that will be attached to the imageView while the download
+     * is in progress.
+     * 
+     * <p>
+     * Contains a reference to the actual download task, so that a download task
+     * can be stopped if a new binding is required, and makes sure that only the
+     * last started download process can bind its result, independently of the
+     * download finish order.
+     * </p>
+     */
+    static class DownloadedDrawable extends ColorDrawable {
+        private final WeakReference<BitmapFetcherTask> bitmapDownloaderTaskReference;
+        private long origId;
+
+        public DownloadedDrawable(Context mContext, BitmapFetcherTask bitmapDownloaderTask, long origId) {
+            super(Color.TRANSPARENT);
+            bitmapDownloaderTaskReference = new WeakReference<BitmapFetcherTask>(bitmapDownloaderTask);
+            this.origId = origId;
+        }
+
+        public long getOrigId() {
+            return origId;
+        }
+
+        public BitmapFetcherTask getBitmapDownloaderTask() {
+            return bitmapDownloaderTaskReference.get();
+        }
+    }
+
+    /*
+     * Cache-related fields and methods.
+     * 
+     * We use a hard and a soft cache. A soft reference cache is too aggressively cleared by the
+     * Garbage Collector.
+     */
+
+    private static final int HARD_CACHE_CAPACITY = 100;
+    private static final int DELAY_BEFORE_PURGE = 10 * 1000; // in milliseconds
+
+    // Hard cache, with a fixed maximum capacity and a life duration
+    private final HashMap<Integer, Bitmap> sHardBitmapCache = new LinkedHashMap<Integer, Bitmap>(
+            HARD_CACHE_CAPACITY / 2, 0.75f, true) {
+        @Override
+        protected boolean removeEldestEntry(LinkedHashMap.Entry<Integer, Bitmap> eldest) {
+            if (size() > HARD_CACHE_CAPACITY) {
+                // Entries push-out of hard reference cache are transferred to
+                // soft reference cache
+                sSoftBitmapCache.put(eldest.getKey(), new SoftReference<Bitmap>(eldest.getValue()));
+                return true;
+            } else
+                return false;
+        }
+    };
+
+    // Soft cache for bitmaps kicked out of hard cache
+    private final static ConcurrentHashMap<Integer, SoftReference<Bitmap>> sSoftBitmapCache = new ConcurrentHashMap<Integer, SoftReference<Bitmap>>(
+            HARD_CACHE_CAPACITY / 2);
+
+    private final Handler purgeHandler = new Handler();
+
+    private final Runnable purger = new Runnable() {
+        public void run() {
+            clearCache();
+        }
+    };
+
+    /**
+     * Adds this bitmap to the cache.
+     * 
+     * @param bitmap
+     *            The newly downloaded bitmap.
+     */
+    private void addBitmapToCache(Integer position, Bitmap bitmap) {
+        if (bitmap != null) {
+            synchronized (sHardBitmapCache) {
+                sHardBitmapCache.put(position, bitmap);
+            }
+        }
+    }
+
+    /**
+     * @param position
+     *            The URL of the image that will be retrieved from the cache.
+     * @return The cached bitmap or null if it was not found.
+     */
+    private Bitmap getBitmapFromCache(Integer position) {
+        // First try the hard reference cache
+        synchronized (sHardBitmapCache) {
+            final Bitmap bitmap = sHardBitmapCache.get(position);
+            if (bitmap != null) {
+                // Log.d("CACHE ****** ", "Hard hit!");
+                // Bitmap found in hard cache
+                // Move element to first position, so that it is removed last
+                return bitmap;
+            }
+        }
+
+        // Then try the soft reference cache
+        SoftReference<Bitmap> bitmapReference = sSoftBitmapCache.get(position);
+        if (bitmapReference != null) {
+            final Bitmap bitmap = bitmapReference.get();
+            if (bitmap != null) {
+                // Bitmap found in soft cache
+                // Log.d("CACHE ****** ", "Soft hit!");
+                return bitmap;
+            } else {
+                // Soft reference has been Garbage Collected
+                sSoftBitmapCache.remove(position);
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * Clears the image cache used internally to improve performance. Note that
+     * for memory efficiency reasons, the cache will automatically be cleared
+     * after a certain inactivity delay.
+     */
+    public void clearCache() {
+        sHardBitmapCache.clear();
+        sSoftBitmapCache.clear();
+    }
+
+    /**
+     * Allow a new delay before the automatic cache clear is done.
+     */
+    private void resetPurgeTimer() {
+        // purgeHandler.removeCallbacks(purger);
+        // purgeHandler.postDelayed(purger, DELAY_BEFORE_PURGE);
+    }
+}

+ 72 - 0
platforms/android/app/src/main/java/com/synconset/ImagePicker.java

@@ -0,0 +1,72 @@
+/**
+ * An Image Picker Plugin for Cordova/PhoneGap.
+ */
+package com.synconset;
+
+import org.apache.cordova.CallbackContext;
+import org.apache.cordova.CordovaPlugin;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.ArrayList;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.util.Log;
+
+public class ImagePicker extends CordovaPlugin {
+	public static String TAG = "ImagePicker";
+	 
+	private CallbackContext callbackContext;
+	private JSONObject params;
+	 
+	public boolean execute(String action, final JSONArray args, final CallbackContext callbackContext) throws JSONException {
+		 this.callbackContext = callbackContext;
+		 this.params = args.getJSONObject(0);
+		if (action.equals("getPictures")) {
+			Intent intent = new Intent(cordova.getActivity(), MultiImageChooserActivity.class);
+			int max = 20;
+			int desiredWidth = 0;
+			int desiredHeight = 0;
+			int quality = 100;
+			if (this.params.has("maximumImagesCount")) {
+				max = this.params.getInt("maximumImagesCount");
+			}
+			if (this.params.has("width")) {
+				desiredWidth = this.params.getInt("width");
+			}
+			if (this.params.has("height")) {
+				desiredHeight = this.params.getInt("height");
+			}
+			if (this.params.has("quality")) {
+				quality = this.params.getInt("quality");
+			}
+			intent.putExtra("MAX_IMAGES", max);
+			intent.putExtra("WIDTH", desiredWidth);
+			intent.putExtra("HEIGHT", desiredHeight);
+			intent.putExtra("QUALITY", quality);
+			if (this.cordova != null) {
+				this.cordova.startActivityForResult((CordovaPlugin) this, intent, 0);
+			}
+		}
+		return true;
+	}
+	
+	public void onActivityResult(int requestCode, int resultCode, Intent data) {
+		if (resultCode == Activity.RESULT_OK && data != null) {
+			ArrayList<String> fileNames = data.getStringArrayListExtra("MULTIPLEFILENAMES");
+			JSONArray res = new JSONArray(fileNames);
+			this.callbackContext.success(res);
+		} else if (resultCode == Activity.RESULT_CANCELED && data != null) {
+			String error = data.getStringExtra("ERRORMESSAGE");
+			this.callbackContext.error(error);
+		} else if (resultCode == Activity.RESULT_CANCELED) {
+			JSONArray res = new JSONArray();
+			this.callbackContext.success(res);
+		} else {
+			this.callbackContext.error("No images selected");
+		}
+	}
+}

+ 735 - 0
platforms/android/app/src/main/java/com/synconset/MultiImageChooserActivity.java

@@ -0,0 +1,735 @@
+/*
+ * Copyright (c) 2012, David Erosa
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without 
+ * modification, are permitted provided that the following  conditions are met:
+ *
+ *   Redistributions of source code must retain the above copyright notice, 
+ *      this list of conditions and the following disclaimer.
+ *   Redistributions in binary form must reproduce the above copyright notice, 
+ *      this list of conditions and the following  disclaimer in the 
+ *      documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING,  BUT NOT LIMITED TO, THE 
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 
+ * ARE DISCLAIMED. IN NO EVENT  SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR  BUSINESS 
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDIN G NEGLIGENCE OR OTHERWISE) 
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
+ * POSSIBILITY OF SUCH  DAMAGE
+ *
+ * Code modified by Andrew Stephan for Sync OnSet
+ *
+ */
+
+package com.synconset;
+
+import java.net.URI;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+
+import com.synconset.FakeR;
+import android.app.Activity;
+import android.app.ActionBar;
+import android.app.AlertDialog;
+import android.app.LoaderManager;
+import android.app.ProgressDialog;
+import android.content.Context;
+import android.content.CursorLoader;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.Loader;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Color;
+import android.graphics.Matrix;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.provider.MediaStore;
+import android.util.Log;
+import android.util.SparseBooleanArray;
+import android.view.Display;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AbsListView;
+import android.widget.AbsListView.OnScrollListener;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.BaseAdapter;
+import android.widget.GridView;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import android.os.Build;
+import android.content.pm.PackageManager;
+import android.Manifest;
+import java.util.ArrayList;
+import java.util.List;
+
+public class MultiImageChooserActivity extends Activity implements OnItemClickListener,
+        LoaderManager.LoaderCallbacks<Cursor> {
+    private static final String TAG = "ImagePicker";
+
+    public static final int NOLIMIT = -1;
+    public static final String MAX_IMAGES_KEY = "MAX_IMAGES";
+    public static final String WIDTH_KEY = "WIDTH";
+    public static final String HEIGHT_KEY = "HEIGHT";
+    public static final String QUALITY_KEY = "QUALITY";
+
+    private ImageAdapter ia;
+
+    private Cursor imagecursor, actualimagecursor;
+    private int image_column_index, image_column_orientation, actual_image_column_index, orientation_column_index;
+    private int colWidth;
+
+    private static final int CURSORLOADER_THUMBS = 0;
+    private static final int CURSORLOADER_REAL = 1;
+
+    private Map<String, Integer> fileNames = new HashMap<String, Integer>();
+
+    private SparseBooleanArray checkStatus = new SparseBooleanArray();
+
+    private int maxImages;
+    private int maxImageCount;
+
+    private int desiredWidth;
+    private int desiredHeight;
+    private int quality;
+
+    private GridView gridView;
+
+    private final ImageFetcher fetcher = new ImageFetcher();
+
+    private int selectedColor = 0xff32b2e1;
+    private boolean shouldRequestThumb = true;
+
+    private FakeR fakeR;
+    private ProgressDialog progress;
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        fakeR = new FakeR(this);
+        setContentView(fakeR.getId("layout", "multiselectorgrid"));
+
+        boolean permission_granted = true;
+
+        if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.M){
+            int hasReadExternalStoragePermission = checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE);
+            List<String> m_permissions = new ArrayList<String>();
+            if (hasReadExternalStoragePermission != PackageManager.PERMISSION_GRANTED) {
+                m_permissions.add(Manifest.permission.READ_EXTERNAL_STORAGE);
+                permission_granted = false;
+            }
+            if (m_permissions.size() > 0){
+                String[] m_permissions_string = new String[m_permissions.size()];
+                m_permissions.toArray(m_permissions_string);
+                requestPermissions(m_permissions_string, 1001);
+            }
+        }
+        if (permission_granted){
+            proceedLoading();
+        }
+    }
+
+    private void proceedLoading(){
+        fileNames.clear();
+
+        maxImages = getIntent().getIntExtra(MAX_IMAGES_KEY, NOLIMIT);
+        desiredWidth = getIntent().getIntExtra(WIDTH_KEY, 0);
+        desiredHeight = getIntent().getIntExtra(HEIGHT_KEY, 0);
+        quality = getIntent().getIntExtra(QUALITY_KEY, 0);
+        maxImageCount = maxImages;
+
+        Display display = getWindowManager().getDefaultDisplay();
+        int width = display.getWidth();
+
+        colWidth = width / 4;
+
+        gridView = (GridView) findViewById(fakeR.getId("id", "gridview"));
+        gridView.setOnItemClickListener(this);
+        gridView.setOnScrollListener(new OnScrollListener() {
+            private int lastFirstItem = 0;
+            private long timestamp = System.currentTimeMillis();
+
+            @Override
+            public void onScrollStateChanged(AbsListView view, int scrollState) {
+                if (scrollState == SCROLL_STATE_IDLE) {
+                    shouldRequestThumb = true;
+                    ia.notifyDataSetChanged();
+                }
+            }
+
+            @Override
+            public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
+                float dt = System.currentTimeMillis() - timestamp;
+                if (firstVisibleItem != lastFirstItem) {
+                    double speed = 1 / dt * 1000;
+                    lastFirstItem = firstVisibleItem;
+                    timestamp = System.currentTimeMillis();
+
+                    // Limit if we go faster than a page a second
+                    shouldRequestThumb = speed < visibleItemCount;
+                }
+            }
+        });
+
+        ia = new ImageAdapter(this);
+        gridView.setAdapter(ia);
+
+        LoaderManager.enableDebugLogging(false);
+        getLoaderManager().initLoader(CURSORLOADER_THUMBS, null, this);
+        getLoaderManager().initLoader(CURSORLOADER_REAL, null, this);
+        setupHeader();
+        updateAcceptButton();
+        progress = new ProgressDialog(this);
+        progress.setTitle("Processing Images");
+        progress.setMessage("This may take a few moments");
+    }
+
+    @Override
+    public void onItemClick(AdapterView<?> arg0, View view, int position, long id) {
+        String name = getImageName(position);
+        int rotation = getImageRotation(position);
+
+        if (name == null) {
+            return;
+        }
+        boolean isChecked = !isChecked(position);
+        if (maxImages == 0 && isChecked) {
+            isChecked = false;
+            AlertDialog.Builder builder = new AlertDialog.Builder(this);
+            builder.setTitle("Maximum " + maxImageCount + " Photos");
+            builder.setMessage("You can only select " + maxImageCount + " photos at a time.");
+            builder.setPositiveButton("OK", new DialogInterface.OnClickListener() {
+                public void onClick(DialogInterface dialog, int which) {
+                    dialog.cancel();
+                }
+            });
+            AlertDialog alert = builder.create();
+            alert.show();
+        } else if (isChecked) {
+            fileNames.put(name, new Integer(rotation));
+            if (maxImageCount == 1) {
+                this.selectClicked(null);
+            } else {
+                maxImages--;
+                ImageView imageView = (ImageView)view;
+                if (android.os.Build.VERSION.SDK_INT>=16) {
+                    imageView.setImageAlpha(128);
+                } else {
+                    imageView.setAlpha(128);
+                }
+                view.setBackgroundColor(selectedColor);
+            }
+        } else {
+            fileNames.remove(name);
+            maxImages++;
+            ImageView imageView = (ImageView)view;
+            if (android.os.Build.VERSION.SDK_INT>=16) {
+                imageView.setImageAlpha(255);
+            } else {
+                imageView.setAlpha(255);
+            }
+            view.setBackgroundColor(Color.TRANSPARENT);
+        }
+
+        checkStatus.put(position, isChecked);
+        updateAcceptButton();
+    }
+
+    @Override
+    public Loader<Cursor> onCreateLoader(int cursorID, Bundle arg1) {
+        CursorLoader cl = null;
+
+        ArrayList<String> img = new ArrayList<String>();
+        switch (cursorID) {
+
+            case CURSORLOADER_THUMBS:
+                img.add(MediaStore.Images.Media._ID);
+                img.add(MediaStore.Images.Media.ORIENTATION);
+                break;
+            case CURSORLOADER_REAL:
+                img.add(MediaStore.Images.Thumbnails.DATA);
+                img.add(MediaStore.Images.Media.ORIENTATION);
+                break;
+            default:
+                break;
+        }
+
+        cl = new CursorLoader(MultiImageChooserActivity.this, MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
+                img.toArray(new String[img.size()]), null, null, "DATE_MODIFIED DESC");
+        return cl;
+    }
+
+    @Override
+    public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
+        if (cursor == null) {
+            // NULL cursor. This usually means there's no image database yet....
+            return;
+        }
+
+        switch (loader.getId()) {
+            case CURSORLOADER_THUMBS:
+                imagecursor = cursor;
+                image_column_index = imagecursor.getColumnIndex(MediaStore.Images.Media._ID);
+                image_column_orientation = imagecursor.getColumnIndex(MediaStore.Images.Media.ORIENTATION);
+                ia.notifyDataSetChanged();
+                break;
+            case CURSORLOADER_REAL:
+                actualimagecursor = cursor;
+                String[] columns = actualimagecursor.getColumnNames();
+                actual_image_column_index = actualimagecursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA);
+                orientation_column_index = actualimagecursor.getColumnIndexOrThrow(MediaStore.Images.Media.ORIENTATION);
+                break;
+            default:
+                break;
+        }
+    }
+
+    @Override
+    public void onLoaderReset(Loader<Cursor> loader) {
+        if (loader.getId() == CURSORLOADER_THUMBS) {
+            imagecursor = null;
+        } else if (loader.getId() == CURSORLOADER_REAL) {
+            actualimagecursor = null;
+        }
+    }
+
+    public void cancelClicked(View ignored) {
+        setResult(RESULT_CANCELED);
+        finish();
+    }
+
+    public void selectClicked(View ignored) {
+        ((TextView) getActionBar().getCustomView().findViewById(fakeR.getId("id", "actionbar_done_textview"))).setEnabled(false);
+        getActionBar().getCustomView().findViewById(fakeR.getId("id", "actionbar_done")).setEnabled(false);
+        progress.show();
+        Intent data = new Intent();
+        if (fileNames.isEmpty()) {
+            this.setResult(RESULT_CANCELED);
+            progress.dismiss();
+            finish();
+        } else {
+            new ResizeImagesTask().execute(fileNames.entrySet());
+        }
+    }
+
+
+    /*********************
+     * Helper Methods
+     ********************/
+    private void updateAcceptButton() {
+        ((TextView) getActionBar().getCustomView().findViewById(fakeR.getId("id", "actionbar_done_textview")))
+                .setEnabled(fileNames.size() != 0);
+        getActionBar().getCustomView().findViewById(fakeR.getId("id", "actionbar_done")).setEnabled(fileNames.size() != 0);
+    }
+
+    private void setupHeader() {
+        // From Roman Nkk's code
+        // https://plus.google.com/113735310430199015092/posts/R49wVvcDoEW
+        // Inflate a "Done/Discard" custom action bar view
+        /*
+         * Copyright 2013 The Android Open Source Project
+         *
+         * Licensed under the Apache License, Version 2.0 (the "License");
+         * you may not use this file except in compliance with the License.
+         * You may obtain a copy of the License at
+         *
+         *     http://www.apache.org/licenses/LICENSE-2.0
+         *
+         * Unless required by applicable law or agreed to in writing, software
+         * distributed under the License is distributed on an "AS IS" BASIS,
+         * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+         * See the License for the specific language governing permissions and
+         * limitations under the License.
+         */
+        LayoutInflater inflater = (LayoutInflater) getActionBar().getThemedContext().getSystemService(
+                LAYOUT_INFLATER_SERVICE);
+        final View customActionBarView = inflater.inflate(fakeR.getId("layout", "actionbar_custom_view_done_discard"), null);
+        customActionBarView.findViewById(fakeR.getId("id", "actionbar_done")).setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                // "Done"
+                selectClicked(null);
+            }
+        });
+        customActionBarView.findViewById(fakeR.getId("id", "actionbar_discard")).setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                finish();
+            }
+        });
+
+        // Show the custom action bar view and hide the normal Home icon and title.
+        final ActionBar actionBar = getActionBar();
+        actionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM, ActionBar.DISPLAY_SHOW_CUSTOM
+                | ActionBar.DISPLAY_SHOW_HOME | ActionBar.DISPLAY_SHOW_TITLE);
+        actionBar.setCustomView(customActionBarView, new ActionBar.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
+                ViewGroup.LayoutParams.MATCH_PARENT));
+    }
+
+    private String getImageName(int position) {
+        actualimagecursor.moveToPosition(position);
+        String name = null;
+
+        try {
+            name = actualimagecursor.getString(actual_image_column_index);
+        } catch (Exception e) {
+            return null;
+        }
+        return name;
+    }
+
+    private int getImageRotation(int position) {
+        actualimagecursor.moveToPosition(position);
+        int rotation = 0;
+
+        try {
+            rotation = actualimagecursor.getInt(orientation_column_index);
+        } catch (Exception e) {
+            return rotation;
+        }
+        return rotation;
+    }
+
+    public boolean isChecked(int position) {
+        boolean ret = checkStatus.get(position);
+        return ret;
+    }
+
+
+    /*********************
+     * Nested Classes
+     ********************/
+    private class SquareImageView extends ImageView {
+        public SquareImageView(Context context) {
+            super(context);
+        }
+
+        @Override
+        public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+            super.onMeasure(widthMeasureSpec, widthMeasureSpec);
+        }
+    }
+
+
+    private class ImageAdapter extends BaseAdapter {
+        private final Bitmap mPlaceHolderBitmap;
+
+        public ImageAdapter(Context c) {
+            Bitmap tmpHolderBitmap = BitmapFactory.decodeResource(getResources(), fakeR.getId("drawable", "loading_icon"));
+            mPlaceHolderBitmap = Bitmap.createScaledBitmap(tmpHolderBitmap, colWidth, colWidth, false);
+            if (tmpHolderBitmap != mPlaceHolderBitmap) {
+                tmpHolderBitmap.recycle();
+                tmpHolderBitmap = null;
+            }
+        }
+
+        public int getCount() {
+            if (imagecursor != null) {
+                return imagecursor.getCount();
+            } else {
+                return 0;
+            }
+        }
+
+        public Object getItem(int position) {
+            return position;
+        }
+
+        public long getItemId(int position) {
+            return position;
+        }
+
+        // create a new ImageView for each item referenced by the Adapter
+        public View getView(int pos, View convertView, ViewGroup parent) {
+
+            if (convertView == null) {
+                ImageView temp = new SquareImageView(MultiImageChooserActivity.this);
+                temp.setScaleType(ImageView.ScaleType.CENTER_CROP);
+                convertView = (View)temp;
+            }
+
+            ImageView imageView = (ImageView)convertView;
+            imageView.setImageBitmap(null);
+
+            final int position = pos;
+
+            if (!imagecursor.moveToPosition(position)) {
+                return imageView;
+            }
+
+            if (image_column_index == -1) {
+                return imageView;
+            }
+
+            final int id = imagecursor.getInt(image_column_index);
+            final int rotate = imagecursor.getInt(image_column_orientation);
+            if (isChecked(pos)) {
+                if (android.os.Build.VERSION.SDK_INT>=16) {
+                    imageView.setImageAlpha(128);
+                } else {
+                    imageView.setAlpha(128);
+                }
+                imageView.setBackgroundColor(selectedColor);
+            } else {
+                if (android.os.Build.VERSION.SDK_INT>=16) {
+                    imageView.setImageAlpha(255);
+                } else {
+                    imageView.setAlpha(255);
+                }
+                imageView.setBackgroundColor(Color.TRANSPARENT);
+            }
+            if (shouldRequestThumb) {
+                fetcher.fetch(Integer.valueOf(id), imageView, colWidth, rotate);
+            }
+
+            return imageView;
+        }
+    }
+
+
+    private class ResizeImagesTask extends AsyncTask<Set<Entry<String, Integer>>, Void, ArrayList<String>> {
+        private Exception asyncTaskError = null;
+
+        @Override
+        protected ArrayList<String> doInBackground(Set<Entry<String, Integer>>... fileSets) {
+            Set<Entry<String, Integer>> fileNames = fileSets[0];
+            ArrayList<String> al = new ArrayList<String>();
+            try {
+                Iterator<Entry<String, Integer>> i = fileNames.iterator();
+                Bitmap bmp;
+                while(i.hasNext()) {
+                    Entry<String, Integer> imageInfo = i.next();
+                    File file = new File(imageInfo.getKey());
+                    int rotate = imageInfo.getValue().intValue();
+                    BitmapFactory.Options options = new BitmapFactory.Options();
+                    options.inSampleSize = 1;
+                    options.inJustDecodeBounds = true;
+                    BitmapFactory.decodeFile(file.getAbsolutePath(), options);
+                    int width = options.outWidth;
+                    int height = options.outHeight;
+                    float scale = calculateScale(width, height);
+                    if (scale < 1) {
+                        int finalWidth = (int)(width * scale);
+                        int finalHeight = (int)(height * scale);
+                        int inSampleSize = calculateInSampleSize(options, finalWidth, finalHeight);
+                        options = new BitmapFactory.Options();
+                        options.inSampleSize = inSampleSize;
+                        try {
+                            bmp = this.tryToGetBitmap(file, options, rotate, true);
+                        } catch (OutOfMemoryError e) {
+                            options.inSampleSize = calculateNextSampleSize(options.inSampleSize);
+                            try {
+                                bmp = this.tryToGetBitmap(file, options, rotate, false);
+                            } catch (OutOfMemoryError e2) {
+                                throw new IOException("Unable to load image into memory.");
+                            }
+                        }
+                    } else {
+                        try {
+                            bmp = this.tryToGetBitmap(file, null, rotate, false);
+                        } catch(OutOfMemoryError e) {
+                            options = new BitmapFactory.Options();
+                            options.inSampleSize = 2;
+                            try {
+                                bmp = this.tryToGetBitmap(file, options, rotate, false);
+                            } catch(OutOfMemoryError e2) {
+                                options = new BitmapFactory.Options();
+                                options.inSampleSize = 4;
+                                try {
+                                    bmp = this.tryToGetBitmap(file, options, rotate, false);
+                                } catch (OutOfMemoryError e3) {
+                                    throw new IOException("Unable to load image into memory.");
+                                }
+                            }
+                        }
+                    }
+
+                    file = this.storeImage(bmp, file.getName());
+                    al.add(Uri.fromFile(file).toString());
+                }
+                return al;
+            } catch(IOException e) {
+                try {
+                    asyncTaskError = e;
+                    for (int i = 0; i < al.size(); i++) {
+                        URI uri = new URI(al.get(i));
+                        File file = new File(uri);
+                        file.delete();
+                    }
+                } catch(Exception exception) {
+                    // the finally does what we want to do
+                } finally {
+                    return new ArrayList<String>();
+                }
+            }
+        }
+
+        @Override
+        protected void onPostExecute(ArrayList<String> al) {
+            Intent data = new Intent();
+
+            if (asyncTaskError != null) {
+                Bundle res = new Bundle();
+                res.putString("ERRORMESSAGE", asyncTaskError.getMessage());
+                data.putExtras(res);
+                setResult(RESULT_CANCELED, data);
+            } else if (al.size() > 0) {
+                Bundle res = new Bundle();
+                res.putStringArrayList("MULTIPLEFILENAMES", al);
+                if (imagecursor != null) {
+                    res.putInt("TOTALFILES", imagecursor.getCount());
+                }
+                data.putExtras(res);
+                setResult(RESULT_OK, data);
+            } else {
+                setResult(RESULT_CANCELED, data);
+            }
+
+            progress.dismiss();
+            finish();
+        }
+
+        private Bitmap tryToGetBitmap(File file, BitmapFactory.Options options, int rotate, boolean shouldScale) throws IOException, OutOfMemoryError {
+            Bitmap bmp;
+            if (options == null) {
+                bmp = BitmapFactory.decodeFile(file.getAbsolutePath());
+            } else {
+                bmp = BitmapFactory.decodeFile(file.getAbsolutePath(), options);
+            }
+            if (bmp == null) {
+                throw new IOException("The image file could not be opened.");
+            }
+            if (options != null && shouldScale) {
+                float scale = calculateScale(options.outWidth, options.outHeight);
+                bmp = this.getResizedBitmap(bmp, scale);
+            }
+            if (rotate != 0) {
+                Matrix matrix = new Matrix();
+                matrix.setRotate(rotate);
+                bmp = Bitmap.createBitmap(bmp, 0, 0, bmp.getWidth(), bmp.getHeight(), matrix, true);
+            }
+            return bmp;
+        }
+
+        /*
+        * The following functions are originally from
+        * https://github.com/raananw/PhoneGap-Image-Resizer
+        * 
+        * They have been modified by Andrew Stephan for Sync OnSet
+        *
+        * The software is open source, MIT Licensed.
+        * Copyright (C) 2012, webXells GmbH All Rights Reserved.
+        */
+        private File storeImage(Bitmap bmp, String fileName) throws IOException {
+            int index = fileName.lastIndexOf('.');
+            String name = fileName.substring(0, index);
+            String ext = fileName.substring(index);
+            File file = File.createTempFile("tmp_" + name, ext);
+            OutputStream outStream = new FileOutputStream(file);
+            if (ext.compareToIgnoreCase(".png") == 0) {
+                bmp.compress(Bitmap.CompressFormat.PNG, quality, outStream);
+            } else {
+                bmp.compress(Bitmap.CompressFormat.JPEG, quality, outStream);
+            }
+            outStream.flush();
+            outStream.close();
+            return file;
+        }
+
+        private Bitmap getResizedBitmap(Bitmap bm, float factor) {
+            int width = bm.getWidth();
+            int height = bm.getHeight();
+            // create a matrix for the manipulation
+            Matrix matrix = new Matrix();
+            // resize the bit map
+            matrix.postScale(factor, factor);
+            // recreate the new Bitmap
+            Bitmap resizedBitmap = Bitmap.createBitmap(bm, 0, 0, width, height, matrix, false);
+            return resizedBitmap;
+        }
+    }
+
+    private int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
+        // Raw height and width of image
+        final int height = options.outHeight;
+        final int width = options.outWidth;
+        int inSampleSize = 1;
+
+        if (height > reqHeight || width > reqWidth) {
+            final int halfHeight = height / 2;
+            final int halfWidth = width / 2;
+
+            // Calculate the largest inSampleSize value that is a power of 2 and keeps both
+            // height and width larger than the requested height and width.
+            while ((halfHeight / inSampleSize) > reqHeight && (halfWidth / inSampleSize) > reqWidth) {
+                inSampleSize *= 2;
+            }
+        }
+
+        return inSampleSize;
+    }
+
+    private int calculateNextSampleSize(int sampleSize) {
+        double logBaseTwo = (int)(Math.log(sampleSize) / Math.log(2));
+        return (int)Math.pow(logBaseTwo + 1, 2);
+    }
+
+    private float calculateScale(int width, int height) {
+        float widthScale = 1.0f;
+        float heightScale = 1.0f;
+        float scale = 1.0f;
+        if (desiredWidth > 0 || desiredHeight > 0) {
+            if (desiredHeight == 0 && desiredWidth < width) {
+                scale = (float)desiredWidth/width;
+            } else if (desiredWidth == 0 && desiredHeight < height) {
+                scale = (float)desiredHeight/height;
+            } else {
+                if (desiredWidth > 0 && desiredWidth < width) {
+                    widthScale = (float)desiredWidth/width;
+                }
+                if (desiredHeight > 0 && desiredHeight < height) {
+                    heightScale = (float)desiredHeight/height;
+                }
+                if (widthScale < heightScale) {
+                    scale = widthScale;
+                } else {
+                    scale = heightScale;
+                }
+            }
+        }
+
+        return scale;
+    }
+
+    @Override
+    public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) {
+        if (requestCode == 1001){
+            if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+                proceedLoading();
+            } else {
+                finish();
+            }
+            return;
+        }
+    }
+}

+ 36 - 5
platforms/android/app/src/main/java/io/ionic/keyboard/IonicKeyboard.java

@@ -13,6 +13,7 @@ import android.content.Context;
 import android.graphics.Rect;
 import android.util.DisplayMetrics;
 import android.view.View;
+import android.view.ViewTreeObserver;
 import android.view.ViewTreeObserver.OnGlobalLayoutListener;
 import android.view.inputmethod.InputMethodManager;
 
@@ -20,10 +21,14 @@ import android.view.inputmethod.InputMethodManager;
 import android.view.Display;
 import android.graphics.Point;
 import android.os.Build;
+import android.widget.FrameLayout;
 
-public class IonicKeyboard extends CordovaPlugin {
+public class CDVIonicKeyboard extends CordovaPlugin {
     private OnGlobalLayoutListener list;
     private View rootView;
+    private View mChildOfContent;
+    private int usableHeightPrevious;
+    private FrameLayout.LayoutParams frameLayoutParams;
 
     public void initialize(CordovaInterface cordova, CordovaWebView webView) {
         super.initialize(cordova, webView);
@@ -66,11 +71,16 @@ public class IonicKeyboard extends CordovaPlugin {
                     final float density = dm.density;
 
                     //http://stackoverflow.com/a/4737265/1091751 detect if keyboard is showing
-                    rootView = cordova.getActivity().getWindow().getDecorView().findViewById(android.R.id.content).getRootView();
+                    FrameLayout content = (FrameLayout) cordova.getActivity().findViewById(android.R.id.content);
+                    rootView = content.getRootView();
                     list = new OnGlobalLayoutListener() {
                         int previousHeightDiff = 0;
                         @Override
                         public void onGlobalLayout() {
+                            boolean resize = preferences.getBoolean("resizeOnFullScreen", false);
+                            if (resize) {
+                                possiblyResizeChildOfContent();
+                            }
                             Rect r = new Rect();
                             //r will be populated with the coordinates of your view that area still visible.
                             rootView.getWindowVisibleDisplayFrame(r);
@@ -110,12 +120,33 @@ public class IonicKeyboard extends CordovaPlugin {
                                 callbackContext.sendPluginResult(result);
                             }
                             previousHeightDiff = pixelHeightDiff;
-                         }
+                        }
+
+                        private void possiblyResizeChildOfContent() {
+                            int usableHeightNow = computeUsableHeight();
+                            if (usableHeightNow != usableHeightPrevious) {
+                                int usableHeightSansKeyboard = mChildOfContent.getRootView().getHeight();
+                                int heightDifference = usableHeightSansKeyboard - usableHeightNow;
+                                if (heightDifference > (usableHeightSansKeyboard/4)) {
+                                    frameLayoutParams.height = usableHeightSansKeyboard - heightDifference;
+                                } else {
+                                    frameLayoutParams.height = usableHeightSansKeyboard;
+                                }
+                                mChildOfContent.requestLayout();
+                                usableHeightPrevious = usableHeightNow;
+                            }
+                        }
+
+                        private int computeUsableHeight() {
+                            Rect r = new Rect();
+                            mChildOfContent.getWindowVisibleDisplayFrame(r);
+                            return (r.bottom - r.top);
+                        }
                     };
 
+                    mChildOfContent = content.getChildAt(0);
                     rootView.getViewTreeObserver().addOnGlobalLayoutListener(list);
-
-
+                    frameLayoutParams = (FrameLayout.LayoutParams) mChildOfContent.getLayoutParams();
                     PluginResult dataResult = new PluginResult(PluginResult.Status.OK);
                     dataResult.setKeepCallback(true);
                     callbackContext.sendPluginResult(dataResult);

+ 11 - 0
platforms/android/app/src/main/res/anim/image_pop_in.xml

@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<scale xmlns:android="http://schemas.android.com/apk/res/android"
+    android:duration="100"
+    android:fillAfter="false"
+    android:fromXScale="0.5"
+    android:fromYScale="0.5"
+    android:interpolator="@android:anim/overshoot_interpolator"
+    android:pivotX="50%"
+    android:pivotY="50%"
+    android:toXScale="1"
+    android:toYScale="1" />

BIN
platforms/android/app/src/main/res/drawable-hdpi/image_bg.9.png


BIN
platforms/android/app/src/main/res/drawable-hdpi/loading_icon.png


BIN
platforms/android/app/src/main/res/drawable-mdpi/ic_action_discard_dark.png


BIN
platforms/android/app/src/main/res/drawable-mdpi/ic_action_discard_light.png


BIN
platforms/android/app/src/main/res/drawable-mdpi/ic_action_done_dark.png


BIN
platforms/android/app/src/main/res/drawable-mdpi/ic_action_done_light.png


BIN
platforms/android/app/src/main/res/drawable-mdpi/ic_launcher.png


BIN
platforms/android/app/src/main/res/drawable-xhdpi/ic_action_discard_dark.png


BIN
platforms/android/app/src/main/res/drawable-xhdpi/ic_action_discard_light.png


BIN
platforms/android/app/src/main/res/drawable-xhdpi/ic_action_done_dark.png


BIN
platforms/android/app/src/main/res/drawable-xhdpi/ic_action_done_light.png


BIN
platforms/android/app/src/main/res/drawable-xhdpi/ic_launcher.png


+ 12 - 0
platforms/android/app/src/main/res/drawable/grid_background.xml

@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="rectangle" >
+    <gradient
+        xmlns:android="http://schemas.android.com/apk/res/android"
+        android:angle="90"
+        android:centerX="0"
+        android:centerY="0"
+      android:startColor="#FF000000"
+      android:endColor="#FF8E8E8E"
+        android:type="linear" />
+</shape>

+ 27 - 0
platforms/android/app/src/main/res/layout/actionbar_custom_view_done_discard.xml

@@ -0,0 +1,27 @@
+<!--
+  Copyright 2012 Roman Nurik
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="horizontal"
+    android:divider="?android:attr/dividerVertical"
+    android:showDividers="middle"
+    android:dividerPadding="12dp">
+
+    <include layout="@layout/actionbar_discard_button" />
+    <include layout="@layout/actionbar_done_button" />
+</LinearLayout>

+ 33 - 0
platforms/android/app/src/main/res/layout/actionbar_discard_button.xml

@@ -0,0 +1,33 @@
+<!--
+  Copyright 2012 Roman Nurik
+  
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+  
+      http://www.apache.org/licenses/LICENSE-2.0
+  
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/actionbar_discard"
+    android:layout_height="match_parent"
+    android:layout_weight="1"
+    android:layout_width="0dp"
+    style="?android:actionButtonStyle" >
+    <TextView
+        android:drawableLeft="@drawable/ic_action_discard_light"
+        android:drawablePadding="8dp"
+        android:gravity="center_vertical"
+        android:id="@+id/actionbar_discard_textview"
+        android:layout_gravity="center"
+        android:layout_height="wrap_content"
+        android:layout_width="wrap_content"
+        android:paddingRight="20dp"
+        android:text="@string/discard"
+        style="?android:actionBarTabTextStyle" />
+</FrameLayout>

+ 34 - 0
platforms/android/app/src/main/res/layout/actionbar_done_button.xml

@@ -0,0 +1,34 @@
+<!--
+  Copyright 2012 Roman Nurik
+  
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+  
+      http://www.apache.org/licenses/LICENSE-2.0
+  
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    style="?android:actionButtonStyle"
+    android:id="@+id/actionbar_done"
+    android:layout_width="0dp"
+    android:layout_height="match_parent"
+    android:layout_weight="1">
+
+    <TextView style="?android:actionBarTabTextStyle"
+        android:id="@+id/actionbar_done_textview"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center"
+        android:paddingRight="20dp"
+        android:drawableLeft="@drawable/ic_action_done_light"
+        android:drawablePadding="8dp"
+        android:gravity="center_vertical"
+        android:text="@string/done" />
+</FrameLayout>

+ 23 - 0
platforms/android/app/src/main/res/layout/multiselectorgrid.xml

@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_height="fill_parent"
+    android:layout_width="fill_parent"
+    android:orientation="vertical" > <!-- android:background="@drawable/image_bg" -->
+
+    <GridView
+        android:id="@+id/gridview"
+        android:layout_width="fill_parent"
+        android:layout_height="0dp"
+        android:layout_weight="1"
+        android:columnWidth="0dip"
+        android:fadingEdgeLength="10dip"
+        android:fastScrollEnabled="true"
+        android:gravity="center"
+        android:horizontalSpacing="8dip"
+        android:numColumns="3"
+        android:requiresFadingEdge="vertical"
+        android:scrollingCache="true"
+        android:stretchMode="columnWidth"
+        android:verticalSpacing="8dip" />
+
+</LinearLayout>

+ 13 - 0
platforms/android/app/src/main/res/values-de/multiimagechooser_strings_de.xml

@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string name="multi_app_name">MultiImageChooser</string>
+    <string name="free_version_label">Kostenlose Version - Sie können noch %d Bilder wählen</string>
+    <string name="error_database">Beim Öffnen der Bilder-Datenbank kam es zu einem Fehler.</string>
+    <string name="requesting_thumbnails">Lade Vorschaubilder, bitte warten</string>
+    <string name="processing_images_header">Verarbeite Bildauswahl</string>
+    <string name="processing_images_message">Dies kann einen kurzen Augenblick dauern.</string>
+    <string name="maximum_selection_count_error_header">Auswahllimit erreicht</string>
+    <string name="maximum_selection_count_error_message">Sie können maximal %d Bilder auf einmal auswählen.</string>
+    <string name="discard">Abbrechen</string>
+    <string name="done">OK</string>
+</resources>

+ 13 - 0
platforms/android/app/src/main/res/values-es/multiimagechooser_strings_es.xml

@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string name="multi_app_name">MultiImageChooser</string>
+    <string name="requesting_thumbnails">Solicitando miniaturas. Por favor, espere…</string>
+    <string name="free_version_label">Versión gratuita - Imágenes restantes: %d</string>
+    <string name="error_database">Error al abrir la base de datos de imágenes.</string>
+    <string name="processing_images_header">Processing Images</string>
+    <string name="processing_images_message">This may take a few moments</string>
+    <string name="maximum_selection_count_error_header">Limit reached</string>
+    <string name="maximum_selection_count_error_message">You can only select %d photos at once.</string>
+    <string name="discard">Cancelar</string>
+    <string name="done">OK</string>
+</resources>

+ 12 - 0
platforms/android/app/src/main/res/values-fr/multiimagechooser_strings_fr.xml

@@ -0,0 +1,12 @@
+<resources>
+    <string name="multi_app_name">MultiImageChooser</string>
+    <string name="free_version_label">Version gratuite - Images restantes:%d"</string>
+    <string name="error_database">"Il y eu une erreur avec les images. Veuillez nous signaler le problème."</string>
+    <string name="requesting_thumbnails">Récupération des vignettes, soyez patient</string>
+    <string name="processing_images_header">Processing Images</string>
+    <string name="processing_images_message">This may take a few moments</string>
+    <string name="maximum_selection_count_error_header">Limit reached</string>
+    <string name="maximum_selection_count_error_message">You can only select %d photos at once.</string>
+    <string name="discard">Annuler</string>
+    <string name="done">OK</string>
+</resources>

+ 13 - 0
platforms/android/app/src/main/res/values-hu/multiimagechooser_strings_hu.xml

@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string name="multi_app_name">MultiImageChooser</string>
+    <string name="free_version_label">Ingyenes verzió - hátralévő képek: %d</string>
+    <string name="error_database">Képadatbázis megnyitási hiba történt. Kérjük, jelentse a problémát.</string>
+    <string name="requesting_thumbnails">Miniatűrök lekérése, kérjük legyen türelemmel</string>
+    <string name="processing_images_header">Processing Images</string>
+    <string name="processing_images_message">This may take a few moments</string>
+    <string name="maximum_selection_count_error_header">Limit reached</string>
+    <string name="maximum_selection_count_error_message">You can only select %d photos at once.</string>
+    <string name="discard">Cancel</string>
+    <string name="done">OK</string>
+</resources>

+ 13 - 0
platforms/android/app/src/main/res/values-ja/multiimagechooser_strings_ja.xml

@@ -0,0 +1,13 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<resources>
+    <string name="multi_app_name">MultiImageChooser</string>
+    <string name="free_version_label">無料版 - 残りの画像: %d</string>
+    <string name="error_database">画像データベースを開く際にエラーがありました。問題を報告してください。</string>
+    <string name="requesting_thumbnails">サムネイルをリクエスト中です。お待ちください。</string>
+    <string name="processing_images_header">Processing Images</string>
+    <string name="processing_images_message">This may take a few moments</string>
+    <string name="maximum_selection_count_error_header">Limit reached</string>
+    <string name="maximum_selection_count_error_message">You can only select %d photos at once.</string>
+    <string name="discard">Cancel</string>
+    <string name="done">OK</string>
+</resources>

+ 13 - 0
platforms/android/app/src/main/res/values-ko/multiimagechooser_strings_ko.xml

@@ -0,0 +1,13 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<resources>
+    <string name="multi_app_name">MultiImageChooser</string>
+    <string name="free_version_label">무료 버전 - 남은 이미지: %d</string>
+    <string name="error_database">이미지 데이터베이스를 여는 데 오류가 발생했습니다. 문제를 보고하세요.</string>
+    <string name="requesting_thumbnails">썸네일 요청 중. 기다려주세요</string>
+    <string name="processing_images_header">Processing Images</string>
+    <string name="processing_images_message">This may take a few moments</string>
+    <string name="maximum_selection_count_error_header">Limit reached</string>
+    <string name="maximum_selection_count_error_message">You can only select %d photos at once.</string>
+    <string name="discard">Cancel</string>
+    <string name="done">OK</string>
+</resources>

+ 13 - 0
platforms/android/app/src/main/res/values/multiimagechooser_strings_en.xml

@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation">
+    <string name="multi_app_name">MultiImageChooser</string>
+    <string name="free_version_label">Free version - Images left: %d</string>
+    <string name="error_database">There was an error opening the images database. Please report the problem.</string>
+    <string name="requesting_thumbnails">Requesting thumbnails, please be patient</string>
+    <string name="processing_images_header">Processing Images</string>
+    <string name="processing_images_message">This may take a few moments</string>
+    <string name="maximum_selection_count_error_header">Limit reached</string>
+    <string name="maximum_selection_count_error_message">You can only select %d photos at once.</string>
+    <string name="discard">Cancel</string>
+    <string name="done">OK</string>
+</resources>

+ 5 - 0
platforms/android/app/src/main/res/values/themes.xml

@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <style name="MyTheme" parent="android:Theme.Holo.Light">
+    </style>
+</resources>

+ 10 - 4
platforms/android/app/src/main/res/xml/config.xml

@@ -1,5 +1,5 @@
 <?xml version='1.0' encoding='utf-8'?>
-<widget id="com.ionicframework.sgZongLi" version="1.0.8" xmlns="http://www.w3.org/ns/widgets" xmlns:cdv="http://cordova.apache.org/ns/1.0">
+<widget id="com.ionicframework.sgZongLi" version="1.0.21" xmlns="http://www.w3.org/ns/widgets" xmlns:cdv="http://cordova.apache.org/ns/1.0">
     <feature name="PhotoViewer">
         <param name="android-package" value="com.sarriaroman.PhotoViewer.PhotoViewer" />
     </feature>
@@ -50,21 +50,27 @@
     <feature name="BarcodeScanner">
         <param name="android-package" value="com.phonegap.plugins.barcodescanner.BarcodeScanner" />
     </feature>
+    <feature name="ImagePicker">
+        <param name="android-package" value="com.synconset.ImagePicker" />
+    </feature>
+    <feature name="DatePickerPlugin">
+        <param name="android-package" value="com.plugin.datepicker.DatePickerPlugin" />
+    </feature>
     <feature name="BluetoothSerial">
         <param name="android-package" value="com.megster.cordova.BluetoothSerial" />
     </feature>
     <feature name="Badge">
         <param name="android-package" value="de.appplant.cordova.plugin.badge.Badge" />
     </feature>
-    <feature name="Keyboard">
-        <param name="android-package" onload="true" value="io.ionic.keyboard.IonicKeyboard" />
-    </feature>
     <allow-navigation href="http://localhost/*" />
     <allow-navigation href="https://localhost/*" />
     <allow-navigation href="ionic://*" />
     <feature name="IonicWebView">
         <param name="android-package" value="com.ionicframework.cordova.webview.IonicWebView" />
     </feature>
+    <feature name="CDVIonicKeyboard">
+        <param name="android-package" onload="true" value="io.ionic.keyboard.CDVIonicKeyboard" />
+    </feature>
     <name>SG综礼</name>
     <description>An awesome Ionic/Cordova app.</description>
     <author email="hi@ionicframework.com" href="http://ionicframework.com/">Ionic Framework Team</author>

+ 28 - 10
platforms/android/platform_www/cordova_plugins.js

@@ -335,14 +335,6 @@ cordova.define('cordova/plugin_list', function(require, exports, module) {
         "window.FileTransfer"
       ]
     },
-    {
-      "id": "cordova-plugin-ionic-keyboard.keyboard",
-      "file": "plugins/cordova-plugin-ionic-keyboard/www/android/keyboard.js",
-      "pluginId": "cordova-plugin-ionic-keyboard",
-      "clobbers": [
-        "window.Keyboard"
-      ]
-    },
     {
       "id": "cordova-plugin-ionic-webview.IonicWebView",
       "file": "plugins/cordova-plugin-ionic-webview/src/www/util.js",
@@ -408,6 +400,30 @@ cordova.define('cordova/plugin_list', function(require, exports, module) {
       "clobbers": [
         "cordova.plugins.barcodeScanner"
       ]
+    },
+    {
+      "id": "cordova-plugin-image-picker.ImagePicker",
+      "file": "plugins/cordova-plugin-image-picker/www/imagepicker.js",
+      "pluginId": "cordova-plugin-image-picker",
+      "clobbers": [
+        "plugins.imagePicker"
+      ]
+    },
+    {
+      "id": "cordova-plugin-ionic-keyboard.keyboard",
+      "file": "plugins/cordova-plugin-ionic-keyboard/www/android/keyboard.js",
+      "pluginId": "cordova-plugin-ionic-keyboard",
+      "clobbers": [
+        "window.Keyboard"
+      ]
+    },
+    {
+      "id": "cordova-plugin-datepicker.DatePicker",
+      "file": "plugins/cordova-plugin-datepicker/www/android/DatePicker.js",
+      "pluginId": "cordova-plugin-datepicker",
+      "clobbers": [
+        "datePicker"
+      ]
     }
   ];
   module.exports.metadata = {
@@ -422,13 +438,15 @@ cordova.define('cordova/plugin_list', function(require, exports, module) {
     "cordova-plugin-device": "2.0.2",
     "cordova-plugin-file-opener2": "2.2.1",
     "cordova-plugin-file-transfer": "1.7.1",
-    "cordova-plugin-ionic-keyboard": "2.1.3",
     "cordova-plugin-ionic-webview": "4.0.1",
     "cordova-plugin-local-notification": "0.9.0-beta.2",
     "cordova-plugin-splashscreen": "5.0.2",
     "cordova-plugin-statusbar": "2.4.2",
     "cordova-plugin-whitelist": "1.3.3",
     "cordova-sqlite-storage": "3.2.0",
-    "phonegap-plugin-barcodescanner": "8.1.0"
+    "phonegap-plugin-barcodescanner": "8.1.0",
+    "cordova-plugin-image-picker": "1.1.1",
+    "cordova-plugin-ionic-keyboard": "2.2.0",
+    "cordova-plugin-datepicker": "0.9.3"
   };
 });

+ 1 - 1
platforms/android/project.properties

@@ -20,4 +20,4 @@ cordova.system.library.4=com.android.support:support-annotations:27.+
 cordova.system.library.5=com.android.support:support-v4:26.+
 cordova.gradle.include.3=cordova-plugin-local-notification/sgZongLi-localnotification.gradle
 cordova.gradle.include.4=phonegap-plugin-barcodescanner/sgZongLi-barcodescanner.gradle
-cordova.system.library.6=com.android.support:support-v4:27.+
+cordova.system.library.6=com.android.support:support-v4:27.+