Open source Android date and time pickers

Reviewed By: bestander

Differential Revision: D2856486

fb-gh-sync-id: 0bb81136289e2f121387649765ba682103e4701b
This commit is contained in:
Martin Konicek
2016-01-26 10:29:47 -08:00
committed by facebook-github-bot-8
parent 5f0ef12cb5
commit 9a0539d2c4
29 changed files with 1275 additions and 41 deletions

View File

@@ -9,8 +9,6 @@
package com.facebook.react.bridge;
import javax.annotation.Nullable;
/**
* Interface that represents a JavaScript Promise which can be passed to the native module as a
* method parameter.
@@ -19,10 +17,38 @@ import javax.annotation.Nullable;
* will be marked as "remoteAsync" and will return a promise when invoked from JavaScript.
*/
public interface Promise {
/**
* Successfully resolve the Promise.
*/
void resolve(Object value);
void reject(Throwable reason);
/**
* Report an error which wasn't caused by an exception.
*/
void reject(String code, String message);
/**
* Report an exception.
*/
void reject(String code, Throwable e);
/**
* Report an exception with a custom error message.
*/
void reject(String code, String message, Throwable e);
/**
* Report an error which wasn't caused by an exception.
* @deprecated Prefer passing a module-specific error code to JS.
* Using this method will pass the error code "EUNSPECIFIED".
*/
@Deprecated
void reject(String reason);
void reject(String code, Throwable extra);
void reject(String code, String reason, @Nullable Throwable extra);
void reject(String message);
/**
* Report an exception, with default error code.
* Useful in catch-all scenarios where it's unclear why the error occurred.
*/
void reject(Throwable reason);
}

View File

@@ -34,23 +34,28 @@ public class PromiseImpl implements Promise {
}
@Override
public void reject(Throwable reason) {
reject(DEFAULT_ERROR, reason.getMessage(), reason);
public void reject(String code, String message) {
reject(code, message, /*Throwable*/null);
}
@Override
@Deprecated
public void reject(String reason) {
reject(DEFAULT_ERROR, reason, null);
public void reject(String message) {
reject(DEFAULT_ERROR, message, /*Throwable*/null);
}
@Override
public void reject(String code, Throwable extra) {
reject(code, extra.getMessage(), extra);
public void reject(String code, Throwable e) {
reject(code, e.getMessage(), e);
}
@Override
public void reject(String code, String reason, @Nullable Throwable extra) {
public void reject(Throwable e) {
reject(DEFAULT_ERROR, e.getMessage(), e);
}
@Override
public void reject(String code, String message, @Nullable Throwable e) {
if (mReject != null) {
if (code == null) {
code = DEFAULT_ERROR;
@@ -60,7 +65,7 @@ public class PromiseImpl implements Promise {
// error instance.
WritableNativeMap errorInfo = new WritableNativeMap();
errorInfo.putString("code", code);
errorInfo.putString("message", reason);
errorInfo.putString("message", message);
// TODO(8850038): add the stack trace info in, need to figure out way to serialize that
mReject.invoke(errorInfo);
}

View File

@@ -59,7 +59,6 @@ import com.facebook.react.common.ReactConstants;
*/
public class CameraRollManager extends ReactContextBaseJavaModule {
private static final String TAG = "Catalyst/CameraRollManager";
private static final String ERROR_UNABLE_TO_LOAD = "E_UNABLE_TO_LOAD";
private static final String ERROR_UNABLE_TO_LOAD_PERMISSION = "E_UNABLE_TO_LOAD_PERMISSION";
private static final String ERROR_UNABLE_TO_SAVE = "E_UNABLE_TO_SAVE";
@@ -145,7 +144,7 @@ public class CameraRollManager extends ReactContextBaseJavaModule {
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES);
pictures.mkdirs();
if (!pictures.isDirectory()) {
mPromise.reject(ERROR_UNABLE_TO_LOAD, "External storage pictures directory not available", null);
mPromise.reject(ERROR_UNABLE_TO_LOAD, "External storage pictures directory not available");
return;
}
File dest = new File(pictures, source.getName());
@@ -178,7 +177,7 @@ public class CameraRollManager extends ReactContextBaseJavaModule {
if (uri != null) {
mPromise.resolve(uri.toString());
} else {
mPromise.reject(ERROR_UNABLE_TO_SAVE, "Could not add image to gallery", null);
mPromise.reject(ERROR_UNABLE_TO_SAVE, "Could not add image to gallery");
}
}
});
@@ -302,7 +301,7 @@ public class CameraRollManager extends ReactContextBaseJavaModule {
Images.Media.DATE_TAKEN + " DESC, " + Images.Media.DATE_MODIFIED + " DESC LIMIT " +
(mFirst + 1)); // set LIMIT to first + 1 so that we know how to populate page_info
if (photos == null) {
mPromise.reject(ERROR_UNABLE_TO_LOAD, "Could not get photos", null);
mPromise.reject(ERROR_UNABLE_TO_LOAD, "Could not get photos");
} else {
try {
putEdges(resolver, photos, response, mFirst);
@@ -412,7 +411,7 @@ public class CameraRollManager extends ReactContextBaseJavaModule {
width = options.outWidth;
height = options.outHeight;
} catch (IOException e) {
FLog.e(TAG, "Could not get width/height for " + photoUri.toString(), e);
FLog.e(ReactConstants.TAG, "Could not get width/height for " + photoUri.toString(), e);
return false;
}
}

View File

@@ -0,0 +1,20 @@
include_defs('//ReactAndroid/DEFS')
android_library(
name = 'datepicker',
srcs = glob(['**/*.java']),
deps = [
react_native_target('java/com/facebook/react/bridge:bridge'),
react_native_target('java/com/facebook/react/common:common'),
react_native_dep('third-party/android/support/v4:lib-support-v4'),
react_native_dep('third-party/java/infer-annotations:infer-annotations'),
react_native_dep('third-party/java/jsr-305:jsr-305'),
],
visibility = [
'PUBLIC',
],
)
project_config(
src_target = ':datepicker',
)

View File

@@ -0,0 +1,103 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.react.modules.datepicker;
import javax.annotation.Nullable;
import java.util.Calendar;
import android.annotation.SuppressLint;
import android.app.DatePickerDialog;
import android.app.DatePickerDialog.OnDateSetListener;
import android.app.Dialog;
import android.app.DialogFragment;
import android.content.Context;
import android.content.DialogInterface;
import android.content.DialogInterface.OnDismissListener;
import android.os.Bundle;
import android.widget.DatePicker;
@SuppressLint("ValidFragment")
public class DatePickerDialogFragment extends DialogFragment {
/**
* Minimum date supported by {@link DatePicker}, 01 Jan 1900
*/
private static final long DEFAULT_MIN_DATE = -2208988800001l;
@Nullable
private OnDateSetListener mOnDateSetListener;
@Nullable
private OnDismissListener mOnDismissListener;
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
Bundle args = getArguments();
return createDialog(args, getActivity(), mOnDateSetListener);
}
/*package*/ static Dialog createDialog(
Bundle args, Context activityContext, @Nullable OnDateSetListener onDateSetListener) {
final Calendar c = Calendar.getInstance();
if (args != null && args.containsKey(DatePickerDialogModule.ARG_DATE)) {
c.setTimeInMillis(args.getLong(DatePickerDialogModule.ARG_DATE));
}
final int year = c.get(Calendar.YEAR);
final int month = c.get(Calendar.MONTH);
final int day = c.get(Calendar.DAY_OF_MONTH);
final DatePickerDialog dialog =
new DismissableDatePickerDialog(activityContext, onDateSetListener, year, month, day);
final DatePicker datePicker = dialog.getDatePicker();
if (args != null && args.containsKey(DatePickerDialogModule.ARG_MINDATE)) {
// Set minDate to the beginning of the day. We need this because of clowniness in datepicker
// that causes it to throw an exception if minDate is greater than the internal timestamp
// that it generates from the y/m/d passed in the constructor.
c.setTimeInMillis(args.getLong(DatePickerDialogModule.ARG_MINDATE));
c.set(Calendar.HOUR_OF_DAY, 0);
c.set(Calendar.MINUTE, 0);
c.set(Calendar.SECOND, 0);
c.set(Calendar.MILLISECOND, 0);
datePicker.setMinDate(c.getTimeInMillis());
} else {
// This is to work around a bug in DatePickerDialog where it doesn't display a title showing
// the date under certain conditions.
datePicker.setMinDate(DEFAULT_MIN_DATE);
}
if (args != null && args.containsKey(DatePickerDialogModule.ARG_MAXDATE)) {
// Set maxDate to the end of the day, same reason as for minDate.
c.setTimeInMillis(args.getLong(DatePickerDialogModule.ARG_MAXDATE));
c.set(Calendar.HOUR_OF_DAY, 23);
c.set(Calendar.MINUTE, 59);
c.set(Calendar.SECOND, 59);
c.set(Calendar.MILLISECOND, 999);
datePicker.setMaxDate(c.getTimeInMillis());
}
return dialog;
}
@Override
public void onDismiss(DialogInterface dialog) {
super.onDismiss(dialog);
if (mOnDismissListener != null) {
mOnDismissListener.onDismiss(dialog);
}
}
/*package*/ void setOnDateSetListener(@Nullable OnDateSetListener onDateSetListener) {
mOnDateSetListener = onDateSetListener;
}
/*package*/ void setOnDismissListener(@Nullable OnDismissListener onDismissListener) {
mOnDismissListener = onDismissListener;
}
}

View File

@@ -0,0 +1,176 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.react.modules.datepicker;
import javax.annotation.Nullable;
import java.util.Map;
import android.app.Activity;
import android.app.DatePickerDialog.OnDateSetListener;
import android.app.DialogFragment;
import android.app.FragmentManager;
import android.content.DialogInterface;
import android.content.DialogInterface.OnDismissListener;
import android.os.Bundle;
import android.widget.DatePicker;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.bridge.WritableNativeMap;
import com.facebook.react.common.annotations.VisibleForTesting;
/**
* {@link NativeModule} that allows JS to show a native date picker dialog and get called back when
* the user selects a date.
*/
public class DatePickerDialogModule extends ReactContextBaseJavaModule {
@VisibleForTesting
public static final String FRAGMENT_TAG = "DatePickerAndroid";
private static final String ERROR_NO_ACTIVITY = "E_NO_ACTIVITY";
/* package */ static final String ARG_DATE = "date";
/* package */ static final String ARG_MINDATE = "minDate";
/* package */ static final String ARG_MAXDATE = "maxDate";
/* package */ static final String ACTION_DATE_SET = "dateSetAction";
/* package */ static final String ACTION_DISMISSED = "dismissedAction";
public DatePickerDialogModule(ReactApplicationContext reactContext) {
super(reactContext);
}
@Override
public String getName() {
return "DatePickerAndroid";
}
private class DatePickerDialogListener implements OnDateSetListener, OnDismissListener {
private final Promise mPromise;
private boolean mPromiseResolved = false;
public DatePickerDialogListener(final Promise promise) {
mPromise = promise;
}
@Override
public void onDateSet(DatePicker view, int year, int month, int day) {
if (!mPromiseResolved && getReactApplicationContext().hasActiveCatalystInstance()) {
WritableMap result = new WritableNativeMap();
result.putString("action", ACTION_DATE_SET);
result.putInt("year", year);
result.putInt("month", month);
result.putInt("day", day);
mPromise.resolve(result);
mPromiseResolved = true;
}
}
@Override
public void onDismiss(DialogInterface dialog) {
if (!mPromiseResolved && getReactApplicationContext().hasActiveCatalystInstance()) {
WritableMap result = new WritableNativeMap();
result.putString("action", ACTION_DISMISSED);
mPromise.resolve(result);
mPromiseResolved = true;
}
}
}
/**
* Show a date picker dialog.
*
* @param options a map containing options. Available keys are:
*
* <ul>
* <li>{@code date} (timestamp in milliseconds) the date to show by default</li>
* <li>
* {@code minDate} (timestamp in milliseconds) the minimum date the user should be allowed
* to select
* </li>
* <li>
* {@code maxDate} (timestamp in milliseconds) the maximum date the user should be allowed
* to select
* </li>
* </ul>
*
* @param promise This will be invoked with parameters action, year,
* month (0-11), day, where action is {@code dateSetAction} or
* {@code dismissedAction}, depending on what the user did. If the action is
* dismiss, year, month and date are undefined.
*/
@ReactMethod
public void open(@Nullable final ReadableMap options, Promise promise) {
Activity activity = getCurrentActivity();
if (activity == null) {
promise.reject(
ERROR_NO_ACTIVITY,
"Tried to open a DatePicker dialog while not attached to an Activity");
return;
}
// We want to support both android.app.Activity and the pre-Honeycomb FragmentActivity
// (for apps that use it for legacy reasons). This unfortunately leads to some code duplication.
if (activity instanceof android.support.v4.app.FragmentActivity) {
android.support.v4.app.FragmentManager fragmentManager =
((android.support.v4.app.FragmentActivity) activity).getSupportFragmentManager();
android.support.v4.app.DialogFragment oldFragment =
(android.support.v4.app.DialogFragment)fragmentManager.findFragmentByTag(FRAGMENT_TAG);
if (oldFragment != null) {
oldFragment.dismiss();
}
SupportDatePickerDialogFragment fragment = new SupportDatePickerDialogFragment();
if (options != null) {
final Bundle args = createFragmentArguments(options);
fragment.setArguments(args);
}
final DatePickerDialogListener listener = new DatePickerDialogListener(promise);
fragment.setOnDismissListener(listener);
fragment.setOnDateSetListener(listener);
fragment.show(fragmentManager, FRAGMENT_TAG);
} else {
FragmentManager fragmentManager = activity.getFragmentManager();
DialogFragment oldFragment = (DialogFragment)fragmentManager.findFragmentByTag(FRAGMENT_TAG);
if (oldFragment != null) {
oldFragment.dismiss();
}
DatePickerDialogFragment fragment = new DatePickerDialogFragment();
if (options != null) {
final Bundle args = createFragmentArguments(options);
fragment.setArguments(args);
}
final DatePickerDialogListener listener = new DatePickerDialogListener(promise);
fragment.setOnDismissListener(listener);
fragment.setOnDateSetListener(listener);
fragment.show(fragmentManager, FRAGMENT_TAG);
}
}
private Bundle createFragmentArguments(ReadableMap options) {
final Bundle args = new Bundle();
if (options.hasKey(ARG_DATE) && !options.isNull(ARG_DATE)) {
args.putLong(ARG_DATE, (long) options.getDouble(ARG_DATE));
}
if (options.hasKey(ARG_MINDATE) && !options.isNull(ARG_MINDATE)) {
args.putLong(ARG_MINDATE, (long) options.getDouble(ARG_MINDATE));
}
if (options.hasKey(ARG_MAXDATE) && !options.isNull(ARG_MAXDATE)) {
args.putLong(ARG_MAXDATE, (long) options.getDouble(ARG_MAXDATE));
}
return args;
}
}

View File

@@ -0,0 +1,57 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.react.modules.datepicker;
import javax.annotation.Nullable;
import android.app.DatePickerDialog;
import android.content.Context;
import android.os.Build;
/**
* <p>
* Certain versions of Android (Jellybean-KitKat) have a bug where when dismissed, the
* {@link DatePickerDialog} still calls the OnDateSetListener. This class works around that issue.
* </p>
*
* <p>
* See: <a href="https://code.google.com/p/android/issues/detail?id=34833">Issue 34833</a>
* </p>
*/
public class DismissableDatePickerDialog extends DatePickerDialog {
public DismissableDatePickerDialog(
Context context,
@Nullable OnDateSetListener callback,
int year,
int monthOfYear,
int dayOfMonth) {
super(context, callback, year, monthOfYear, dayOfMonth);
}
public DismissableDatePickerDialog(
Context context,
int theme,
@Nullable OnDateSetListener callback,
int year,
int monthOfYear,
int dayOfMonth) {
super(context, theme, callback, year, monthOfYear, dayOfMonth);
}
@Override
protected void onStop() {
// do *not* call super.onStop() on KitKat on lower, as that would erroneously call the
// OnDateSetListener when the dialog is dismissed, or call it twice when "OK" is pressed.
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.KITKAT) {
super.onStop();
}
}
}

View File

@@ -0,0 +1,51 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.react.modules.datepicker;
import javax.annotation.Nullable;
import android.annotation.SuppressLint;
import android.app.DatePickerDialog.OnDateSetListener;
import android.app.Dialog;
import android.content.DialogInterface;
import android.content.DialogInterface.OnDismissListener;
import android.os.Bundle;
import android.support.v4.app.DialogFragment;
@SuppressLint("ValidFragment")
public class SupportDatePickerDialogFragment extends DialogFragment {
@Nullable
private OnDateSetListener mOnDateSetListener;
@Nullable
private OnDismissListener mOnDismissListener;
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
final Bundle args = getArguments();
return DatePickerDialogFragment.createDialog(args, getActivity(), mOnDateSetListener);
}
@Override
public void onDismiss(DialogInterface dialog) {
super.onDismiss(dialog);
if (mOnDismissListener != null) {
mOnDismissListener.onDismiss(dialog);
}
}
/*package*/ void setOnDateSetListener(@Nullable OnDateSetListener onDateSetListener) {
mOnDateSetListener = onDateSetListener;
}
/*package*/ void setOnDismissListener(@Nullable OnDismissListener onDismissListener) {
mOnDismissListener = onDismissListener;
}
}

View File

@@ -0,0 +1,20 @@
include_defs('//ReactAndroid/DEFS')
android_library(
name = 'timepicker',
srcs = glob(['**/*.java']),
deps = [
react_native_target('java/com/facebook/react/bridge:bridge'),
react_native_target('java/com/facebook/react/common:common'),
react_native_dep('third-party/android/support/v4:lib-support-v4'),
react_native_dep('third-party/java/infer-annotations:infer-annotations'),
react_native_dep('third-party/java/jsr-305:jsr-305'),
],
visibility = [
'PUBLIC',
],
)
project_config(
src_target = ':timepicker',
)

View File

@@ -0,0 +1,58 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.react.modules.timepicker;
import javax.annotation.Nullable;
import android.app.TimePickerDialog;
import android.content.Context;
import android.os.Build;
/**
* <p>
* Certain versions of Android (Jellybean-KitKat) have a bug where when dismissed, the
* {@link TimePickerDialog} still calls the OnTimeSetListener. This class works around that issue
* by *not* calling super.onStop on KitKat on lower, as that would erroneously call the
* OnTimeSetListener when the dialog is dismissed, or call it twice when "OK" is pressed.
* </p>
*
* <p>
* See: <a href="https://code.google.com/p/android/issues/detail?id=34833">Issue 34833</a>
* </p>
*/
public class DismissableTimePickerDialog extends TimePickerDialog {
public DismissableTimePickerDialog(
Context context,
@Nullable OnTimeSetListener callback,
int hourOfDay,
int minute,
boolean is24HourView) {
super(context, callback, hourOfDay, minute, is24HourView);
}
public DismissableTimePickerDialog(
Context context,
int theme,
@Nullable OnTimeSetListener callback,
int hourOfDay,
int minute,
boolean is24HourView) {
super(context, theme, callback, hourOfDay, minute, is24HourView);
}
@Override
protected void onStop() {
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.KITKAT) {
super.onStop();
}
}
}

View File

@@ -0,0 +1,50 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.react.modules.timepicker;
import javax.annotation.Nullable;
import android.app.Dialog;
import android.app.TimePickerDialog.OnTimeSetListener;
import android.content.DialogInterface;
import android.content.DialogInterface.OnDismissListener;
import android.os.Bundle;
import android.support.v4.app.DialogFragment;
@SuppressWarnings("ValidFragment")
public class SupportTimePickerDialogFragment extends DialogFragment {
@Nullable
private OnTimeSetListener mOnTimeSetListener;
@Nullable
private OnDismissListener mOnDismissListener;
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
final Bundle args = getArguments();
return TimePickerDialogFragment.createDialog(args, getActivity(), mOnTimeSetListener);
}
@Override
public void onDismiss(DialogInterface dialog) {
super.onDismiss(dialog);
if (mOnDismissListener != null) {
mOnDismissListener.onDismiss(dialog);
}
}
public void setOnDismissListener(@Nullable OnDismissListener onDismissListener) {
mOnDismissListener = onDismissListener;
}
public void setOnTimeSetListener(@Nullable OnTimeSetListener onTimeSetListener) {
mOnTimeSetListener = onTimeSetListener;
}
}

View File

@@ -0,0 +1,78 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.react.modules.timepicker;
import javax.annotation.Nullable;
import java.util.Calendar;
import android.app.Dialog;
import android.app.DialogFragment;
import android.app.TimePickerDialog.OnTimeSetListener;
import android.content.Context;
import android.content.DialogInterface;
import android.content.DialogInterface.OnDismissListener;
import android.os.Bundle;
import android.text.format.DateFormat;
@SuppressWarnings("ValidFragment")
public class TimePickerDialogFragment extends DialogFragment {
@Nullable
private OnTimeSetListener mOnTimeSetListener;
@Nullable
private OnDismissListener mOnDismissListener;
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
final Bundle args = getArguments();
return createDialog(args, getActivity(), mOnTimeSetListener);
}
/*package*/ static Dialog createDialog(
Bundle args, Context activityContext, @Nullable OnTimeSetListener onTimeSetListener
) {
final Calendar now = Calendar.getInstance();
int hour = now.get(Calendar.HOUR_OF_DAY);
int minute = now.get(Calendar.MINUTE);
boolean is24hour = DateFormat.is24HourFormat(activityContext);
if (args != null) {
hour = args.getInt(TimePickerDialogModule.ARG_HOUR, now.get(Calendar.HOUR_OF_DAY));
minute = args.getInt(TimePickerDialogModule.ARG_MINUTE, now.get(Calendar.MINUTE));
is24hour = args.getBoolean(
TimePickerDialogModule.ARG_IS24HOUR,
DateFormat.is24HourFormat(activityContext));
}
return new DismissableTimePickerDialog(
activityContext,
onTimeSetListener,
hour,
minute,
is24hour);
}
@Override
public void onDismiss(DialogInterface dialog) {
super.onDismiss(dialog);
if (mOnDismissListener != null) {
mOnDismissListener.onDismiss(dialog);
}
}
public void setOnDismissListener(@Nullable OnDismissListener onDismissListener) {
mOnDismissListener = onDismissListener;
}
public void setOnTimeSetListener(@Nullable OnTimeSetListener onTimeSetListener) {
mOnTimeSetListener = onTimeSetListener;
}
}

View File

@@ -0,0 +1,153 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.react.modules.timepicker;
import javax.annotation.Nullable;
import java.util.Map;
import android.app.Activity;
import android.app.DialogFragment;
import android.app.FragmentManager;
import android.app.TimePickerDialog.OnTimeSetListener;
import android.content.DialogInterface;
import android.content.DialogInterface.OnDismissListener;
import android.os.Bundle;
import android.widget.TimePicker;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.bridge.WritableNativeMap;
import com.facebook.react.common.annotations.VisibleForTesting;
/**
* {@link NativeModule} that allows JS to show a native time picker dialog and get called back when
* the user selects a time.
*/
public class TimePickerDialogModule extends ReactContextBaseJavaModule {
@VisibleForTesting
public static final String FRAGMENT_TAG = "TimePickerAndroid";
private static final String ERROR_NO_ACTIVITY = "E_NO_ACTIVITY";
/* package */ static final String ARG_HOUR = "hour";
/* package */ static final String ARG_MINUTE = "minute";
/* package */ static final String ARG_IS24HOUR = "is24Hour";
/* package */ static final String ACTION_TIME_SET = "timeSetAction";
/* package */ static final String ACTION_DISMISSED = "dismissedAction";
public TimePickerDialogModule(ReactApplicationContext reactContext) {
super(reactContext);
}
@Override
public String getName() {
return "TimePickerAndroid";
}
private class TimePickerDialogListener implements OnTimeSetListener, OnDismissListener {
private final Promise mPromise;
private boolean mPromiseResolved = false;
public TimePickerDialogListener(Promise promise) {
mPromise = promise;
}
@Override
public void onTimeSet(TimePicker view, int hour, int minute) {
if (!mPromiseResolved && getReactApplicationContext().hasActiveCatalystInstance()) {
WritableMap result = new WritableNativeMap();
result.putString("action", ACTION_TIME_SET);
result.putInt("hour", hour);
result.putInt("minute", minute);
mPromise.resolve(result);
mPromiseResolved = true;
}
}
@Override
public void onDismiss(DialogInterface dialog) {
if (!mPromiseResolved && getReactApplicationContext().hasActiveCatalystInstance()) {
WritableMap result = new WritableNativeMap();
result.putString("action", ACTION_DISMISSED);
mPromise.resolve(result);
mPromiseResolved = true;
}
}
}
@ReactMethod
public void open(@Nullable final ReadableMap options, Promise promise) {
Activity activity = getCurrentActivity();
if (activity == null) {
promise.reject(
ERROR_NO_ACTIVITY,
"Tried to open a TimePicker dialog while not attached to an Activity");
return;
}
// We want to support both android.app.Activity and the pre-Honeycomb FragmentActivity
// (for apps that use it for legacy reasons). This unfortunately leads to some code duplication.
if (activity instanceof android.support.v4.app.FragmentActivity) {
android.support.v4.app.FragmentManager fragmentManager =
((android.support.v4.app.FragmentActivity) activity).getSupportFragmentManager();
android.support.v4.app.DialogFragment oldFragment =
(android.support.v4.app.DialogFragment)fragmentManager.findFragmentByTag(FRAGMENT_TAG);
if (oldFragment != null) {
oldFragment.dismiss();
}
SupportTimePickerDialogFragment fragment = new SupportTimePickerDialogFragment();
if (options != null) {
Bundle args = createFragmentArguments(options);
fragment.setArguments(args);
}
TimePickerDialogListener listener = new TimePickerDialogListener(promise);
fragment.setOnDismissListener(listener);
fragment.setOnTimeSetListener(listener);
fragment.show(fragmentManager, FRAGMENT_TAG);
} else {
FragmentManager fragmentManager = activity.getFragmentManager();
DialogFragment oldFragment = (DialogFragment) fragmentManager.findFragmentByTag(FRAGMENT_TAG);
if (oldFragment != null) {
oldFragment.dismiss();
}
TimePickerDialogFragment fragment = new TimePickerDialogFragment();
if (options != null) {
final Bundle args = createFragmentArguments(options);
fragment.setArguments(args);
}
TimePickerDialogListener listener = new TimePickerDialogListener(promise);
fragment.setOnDismissListener(listener);
fragment.setOnTimeSetListener(listener);
fragment.show(fragmentManager, FRAGMENT_TAG);
}
}
private Bundle createFragmentArguments(ReadableMap options) {
final Bundle args = new Bundle();
if (options.hasKey(ARG_HOUR) && !options.isNull(ARG_HOUR)) {
args.putInt(ARG_HOUR, options.getInt(ARG_HOUR));
}
if (options.hasKey(ARG_MINUTE) && !options.isNull(ARG_MINUTE)) {
args.putInt(ARG_MINUTE, options.getInt(ARG_MINUTE));
}
if (options.hasKey(ARG_IS24HOUR) && !options.isNull(ARG_IS24HOUR)) {
args.putBoolean(ARG_IS24HOUR, options.getBoolean(ARG_IS24HOUR));
}
return args;
}
}

View File

@@ -28,6 +28,7 @@ android_library(
react_native_target('java/com/facebook/react/modules/camera:camera'),
react_native_target('java/com/facebook/react/modules/clipboard:clipboard'),
react_native_target('java/com/facebook/react/modules/core:core'),
react_native_target('java/com/facebook/react/modules/datepicker:datepicker'),
react_native_target('java/com/facebook/react/modules/debug:debug'),
react_native_target('java/com/facebook/react/modules/dialog:dialog'),
react_native_target('java/com/facebook/react/modules/fresco:fresco'),
@@ -36,6 +37,7 @@ android_library(
react_native_target('java/com/facebook/react/modules/netinfo:netinfo'),
react_native_target('java/com/facebook/react/modules/network:network'),
react_native_target('java/com/facebook/react/modules/storage:storage'),
react_native_target('java/com/facebook/react/modules/timepicker:timepicker'),
react_native_target('java/com/facebook/react/modules/toast:toast'),
react_native_target('java/com/facebook/react/uimanager:uimanager'),
react_native_target('java/com/facebook/react/modules/websocket:websocket'),

View File

@@ -17,16 +17,19 @@ import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.JavaScriptModule;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.modules.appstate.AppStateModule;
import com.facebook.react.modules.camera.CameraRollManager;
import com.facebook.react.modules.clipboard.ClipboardModule;
import com.facebook.react.modules.dialog.DialogModule;
import com.facebook.react.modules.datepicker.DatePickerDialogModule;
import com.facebook.react.modules.fresco.FrescoModule;
import com.facebook.react.modules.intent.IntentModule;
import com.facebook.react.modules.location.LocationModule;
import com.facebook.react.modules.netinfo.NetInfoModule;
import com.facebook.react.modules.network.NetworkingModule;
import com.facebook.react.modules.storage.AsyncStorageModule;
import com.facebook.react.modules.timepicker.TimePickerDialogModule;
import com.facebook.react.modules.toast.ToastModule;
import com.facebook.react.modules.appstate.AppStateModule;
import com.facebook.react.modules.websocket.WebSocketModule;
import com.facebook.react.uimanager.ViewManager;
import com.facebook.react.views.art.ARTRenderableViewManager;
@@ -50,7 +53,6 @@ import com.facebook.react.views.view.ReactViewManager;
import com.facebook.react.views.viewpager.ReactViewPagerManager;
import com.facebook.react.views.swiperefresh.SwipeRefreshLayoutManager;
import com.facebook.react.views.webview.ReactWebViewManager;
import com.facebook.react.modules.clipboard.ClipboardModule;
/**
* Package defining basic modules and view managers.
@@ -64,12 +66,14 @@ public class MainReactPackage implements ReactPackage {
new AsyncStorageModule(reactContext),
new CameraRollManager(reactContext),
new ClipboardModule(reactContext),
new DatePickerDialogModule(reactContext),
new DialogModule(reactContext),
new FrescoModule(reactContext),
new IntentModule(reactContext),
new LocationModule(reactContext),
new NetworkingModule(reactContext),
new NetInfoModule(reactContext),
new TimePickerDialogModule(reactContext),
new ToastModule(reactContext),
new WebSocketModule(reactContext));
}