diff --git a/ui/android/BUILD.gn b/ui/android/BUILD.gn new file mode 100644 index 0000000000000..e0c6c56daa637 --- /dev/null +++ b/ui/android/BUILD.gn @@ -0,0 +1,79 @@ +import("//build/config/android/rules.gni") + +assert(is_android) + +java_cpp_enum("java_enums_srcjar") { + sources = [ + "../gfx/android/java_bitmap.h", + ] + outputs = [ + "org/chromium/ui/gfx/BitmapFormat.java", + ] +} + +java_strings_grd("ui_strings_grd") { + grd_file = "java/strings/android_ui_strings.grd" + outputs = [ + "values-am/android_ui_strings.xml", + "values-ar/android_ui_strings.xml", + "values-bg/android_ui_strings.xml", + "values-ca/android_ui_strings.xml", + "values-cs/android_ui_strings.xml", + "values-da/android_ui_strings.xml", + "values-de/android_ui_strings.xml", + "values-el/android_ui_strings.xml", + "values/android_ui_strings.xml", + "values-en-rGB/android_ui_strings.xml", + "values-es/android_ui_strings.xml", + "values-es-rUS/android_ui_strings.xml", + "values-fa/android_ui_strings.xml", + "values-fi/android_ui_strings.xml", + "values-tl/android_ui_strings.xml", + "values-fr/android_ui_strings.xml", + "values-hi/android_ui_strings.xml", + "values-hr/android_ui_strings.xml", + "values-hu/android_ui_strings.xml", + "values-in/android_ui_strings.xml", + "values-it/android_ui_strings.xml", + "values-iw/android_ui_strings.xml", + "values-ja/android_ui_strings.xml", + "values-ko/android_ui_strings.xml", + "values-lt/android_ui_strings.xml", + "values-lv/android_ui_strings.xml", + "values-nl/android_ui_strings.xml", + "values-nb/android_ui_strings.xml", + "values-pl/android_ui_strings.xml", + "values-pt-rBR/android_ui_strings.xml", + "values-pt-rPT/android_ui_strings.xml", + "values-ro/android_ui_strings.xml", + "values-ru/android_ui_strings.xml", + "values-sk/android_ui_strings.xml", + "values-sl/android_ui_strings.xml", + "values-sr/android_ui_strings.xml", + "values-sv/android_ui_strings.xml", + "values-sw/android_ui_strings.xml", + "values-th/android_ui_strings.xml", + "values-tr/android_ui_strings.xml", + "values-uk/android_ui_strings.xml", + "values-vi/android_ui_strings.xml", + "values-zh-rCN/android_ui_strings.xml", + "values-zh-rTW/android_ui_strings.xml", + ] +} + +android_resources("ui_java_resources") { + custom_package = "org.chromium.ui" + resource_dirs = [ "java/res" ] + deps = [ + ":ui_strings_grd", + ] +} + +android_library("ui_java") { + DEPRECATED_java_in_dir = "java/src" + deps = [ + ":ui_java_resources", + "//base:base_java", + ] + srcjar_deps = [ ":java_enums_srcjar" ] +} diff --git a/ui/android/OWNERS b/ui/android/OWNERS new file mode 100644 index 0000000000000..85d8f5a5e05f0 --- /dev/null +++ b/ui/android/OWNERS @@ -0,0 +1,4 @@ +jdduke@chromium.org +miguelg@chromium.org +newt@chromium.org +tedchoc@chromium.org diff --git a/ui/android/java/PageTransitionTypes.template b/ui/android/java/PageTransitionTypes.template new file mode 100644 index 0000000000000..d2f12f8a5d7a9 --- /dev/null +++ b/ui/android/java/PageTransitionTypes.template @@ -0,0 +1,12 @@ +// Copyright 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.ui.base; + +public class PageTransitionTypes { +#define PAGE_TRANSITION(label, value) public static final int \ + PAGE_TRANSITION_ ## label = value; +#include "ui/base/page_transition_types_list.h" +#undef PAGE_TRANSITION +} diff --git a/ui/android/java/res/drawable-hdpi/color_picker_advanced_select_handle.png b/ui/android/java/res/drawable-hdpi/color_picker_advanced_select_handle.png new file mode 100644 index 0000000000000..94f80cf618943 Binary files /dev/null and b/ui/android/java/res/drawable-hdpi/color_picker_advanced_select_handle.png differ diff --git a/ui/android/java/res/drawable-hdpi/dropdown_popup_background_down.9.png b/ui/android/java/res/drawable-hdpi/dropdown_popup_background_down.9.png new file mode 100644 index 0000000000000..acaa8a76839d1 Binary files /dev/null and b/ui/android/java/res/drawable-hdpi/dropdown_popup_background_down.9.png differ diff --git a/ui/android/java/res/drawable-hdpi/dropdown_popup_background_up.9.png b/ui/android/java/res/drawable-hdpi/dropdown_popup_background_up.9.png new file mode 100644 index 0000000000000..6d78622d7fd14 Binary files /dev/null and b/ui/android/java/res/drawable-hdpi/dropdown_popup_background_up.9.png differ diff --git a/ui/android/java/res/drawable-mdpi/color_picker_advanced_select_handle.png b/ui/android/java/res/drawable-mdpi/color_picker_advanced_select_handle.png new file mode 100644 index 0000000000000..0ba0946f91db4 Binary files /dev/null and b/ui/android/java/res/drawable-mdpi/color_picker_advanced_select_handle.png differ diff --git a/ui/android/java/res/drawable-mdpi/dropdown_popup_background_down.9.png b/ui/android/java/res/drawable-mdpi/dropdown_popup_background_down.9.png new file mode 100644 index 0000000000000..761e904212e14 Binary files /dev/null and b/ui/android/java/res/drawable-mdpi/dropdown_popup_background_down.9.png differ diff --git a/ui/android/java/res/drawable-mdpi/dropdown_popup_background_up.9.png b/ui/android/java/res/drawable-mdpi/dropdown_popup_background_up.9.png new file mode 100644 index 0000000000000..9c16993bcb812 Binary files /dev/null and b/ui/android/java/res/drawable-mdpi/dropdown_popup_background_up.9.png differ diff --git a/ui/android/java/res/drawable-xhdpi/color_picker_advanced_select_handle.png b/ui/android/java/res/drawable-xhdpi/color_picker_advanced_select_handle.png new file mode 100644 index 0000000000000..66ebf3ea177e8 Binary files /dev/null and b/ui/android/java/res/drawable-xhdpi/color_picker_advanced_select_handle.png differ diff --git a/ui/android/java/res/drawable-xhdpi/dropdown_popup_background_down.9.png b/ui/android/java/res/drawable-xhdpi/dropdown_popup_background_down.9.png new file mode 100644 index 0000000000000..ac01821e96299 Binary files /dev/null and b/ui/android/java/res/drawable-xhdpi/dropdown_popup_background_down.9.png differ diff --git a/ui/android/java/res/drawable-xhdpi/dropdown_popup_background_up.9.png b/ui/android/java/res/drawable-xhdpi/dropdown_popup_background_up.9.png new file mode 100644 index 0000000000000..22a58985510f0 Binary files /dev/null and b/ui/android/java/res/drawable-xhdpi/dropdown_popup_background_up.9.png differ diff --git a/ui/android/java/res/drawable/color_button_background.xml b/ui/android/java/res/drawable/color_button_background.xml new file mode 100644 index 0000000000000..66bcce2e4d2fc --- /dev/null +++ b/ui/android/java/res/drawable/color_button_background.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/ui/android/java/res/drawable/color_picker_border.xml b/ui/android/java/res/drawable/color_picker_border.xml new file mode 100644 index 0000000000000..6cd6bbfbd6810 --- /dev/null +++ b/ui/android/java/res/drawable/color_picker_border.xml @@ -0,0 +1,12 @@ + + + + + + + + \ No newline at end of file diff --git a/ui/android/java/res/drawable/dropdown_popup_background.xml b/ui/android/java/res/drawable/dropdown_popup_background.xml new file mode 100644 index 0000000000000..71b5271dd4d3b --- /dev/null +++ b/ui/android/java/res/drawable/dropdown_popup_background.xml @@ -0,0 +1,13 @@ + + + + + + + + \ No newline at end of file diff --git a/ui/android/java/res/layout-land/date_time_picker_dialog.xml b/ui/android/java/res/layout-land/date_time_picker_dialog.xml new file mode 100644 index 0000000000000..5cb2dd082c298 --- /dev/null +++ b/ui/android/java/res/layout-land/date_time_picker_dialog.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + diff --git a/ui/android/java/res/layout/color_picker_advanced_component.xml b/ui/android/java/res/layout/color_picker_advanced_component.xml new file mode 100644 index 0000000000000..a51c055b1b1c2 --- /dev/null +++ b/ui/android/java/res/layout/color_picker_advanced_component.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ui/android/java/res/layout/color_picker_dialog_content.xml b/ui/android/java/res/layout/color_picker_dialog_content.xml new file mode 100644 index 0000000000000..38a492d0b283e --- /dev/null +++ b/ui/android/java/res/layout/color_picker_dialog_content.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + diff --git a/ui/android/java/res/layout/color_picker_dialog_title.xml b/ui/android/java/res/layout/color_picker_dialog_title.xml new file mode 100644 index 0000000000000..d97cafb91972d --- /dev/null +++ b/ui/android/java/res/layout/color_picker_dialog_title.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/ui/android/java/res/layout/date_time_picker_dialog.xml b/ui/android/java/res/layout/date_time_picker_dialog.xml new file mode 100644 index 0000000000000..09b4bb1ea0713 --- /dev/null +++ b/ui/android/java/res/layout/date_time_picker_dialog.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + diff --git a/ui/android/java/res/layout/date_time_suggestion.xml b/ui/android/java/res/layout/date_time_suggestion.xml new file mode 100644 index 0000000000000..e47ac7682c3ca --- /dev/null +++ b/ui/android/java/res/layout/date_time_suggestion.xml @@ -0,0 +1,35 @@ + + + + + + + diff --git a/ui/android/java/res/layout/dropdown_item.xml b/ui/android/java/res/layout/dropdown_item.xml new file mode 100644 index 0000000000000..bded0ad992682 --- /dev/null +++ b/ui/android/java/res/layout/dropdown_item.xml @@ -0,0 +1,34 @@ + + + + + + + + diff --git a/ui/android/java/res/layout/multi_field_time_picker_dialog.xml b/ui/android/java/res/layout/multi_field_time_picker_dialog.xml new file mode 100644 index 0000000000000..e037a09739ae6 --- /dev/null +++ b/ui/android/java/res/layout/multi_field_time_picker_dialog.xml @@ -0,0 +1,122 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/ui/android/java/res/layout/two_field_date_picker.xml b/ui/android/java/res/layout/two_field_date_picker.xml new file mode 100644 index 0000000000000..0660251aae3a6 --- /dev/null +++ b/ui/android/java/res/layout/two_field_date_picker.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + diff --git a/ui/android/java/res/values-v17/styles.xml b/ui/android/java/res/values-v17/styles.xml new file mode 100644 index 0000000000000..48b87ff148c16 --- /dev/null +++ b/ui/android/java/res/values-v17/styles.xml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/ui/android/java/res/values/colors.xml b/ui/android/java/res/values/colors.xml new file mode 100644 index 0000000000000..8eaed12f7569a --- /dev/null +++ b/ui/android/java/res/values/colors.xml @@ -0,0 +1,13 @@ + + + + #B0B0B0 + #FFFFFF + #E5E5E5 + #C0C0C0 + + diff --git a/ui/android/java/res/values/dimens.xml b/ui/android/java/res/values/dimens.xml new file mode 100644 index 0000000000000..5a7a2274e263e --- /dev/null +++ b/ui/android/java/res/values/dimens.xml @@ -0,0 +1,26 @@ + + + + + + 14.5dp + 60dp + 44dp + 1px + + + 27.0mm + 48.0dp + + diff --git a/ui/android/java/res/values/strings.xml b/ui/android/java/res/values/strings.xml new file mode 100644 index 0000000000000..850bcc5c5e6b6 --- /dev/null +++ b/ui/android/java/res/values/strings.xml @@ -0,0 +1,16 @@ + + + + + + diff --git a/ui/android/java/res/values/values.xml b/ui/android/java/res/values/values.xml new file mode 100644 index 0000000000000..73f8c714431cd --- /dev/null +++ b/ui/android/java/res/values/values.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/ui/android/java/src/org/chromium/ui/ColorPickerAdvanced.java b/ui/android/java/src/org/chromium/ui/ColorPickerAdvanced.java new file mode 100644 index 0000000000000..29b4e1fd46660 --- /dev/null +++ b/ui/android/java/src/org/chromium/ui/ColorPickerAdvanced.java @@ -0,0 +1,250 @@ +// Copyright 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.ui; + +import android.content.Context; +import android.graphics.Color; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.LinearLayout; +import android.widget.SeekBar; +import android.widget.SeekBar.OnSeekBarChangeListener; + +/** + * Represents a more advanced way for the user to choose a color, based on selecting each of + * the Hue, Saturation and Value attributes. + */ +public class ColorPickerAdvanced extends LinearLayout implements OnSeekBarChangeListener { + private static final int HUE_SEEK_BAR_MAX = 360; + + private static final int HUE_COLOR_COUNT = 7; + + private static final int SATURATION_SEEK_BAR_MAX = 100; + + private static final int SATURATION_COLOR_COUNT = 2; + + private static final int VALUE_SEEK_BAR_MAX = 100; + + private static final int VALUE_COLOR_COUNT = 2; + + ColorPickerAdvancedComponent mHueDetails; + + ColorPickerAdvancedComponent mSaturationDetails; + + ColorPickerAdvancedComponent mValueDetails; + + private OnColorChangedListener mOnColorChangedListener; + + private int mCurrentColor; + + private final float[] mCurrentHsvValues = new float[3]; + + public ColorPickerAdvanced(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + public ColorPickerAdvanced(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + init(); + } + + public ColorPickerAdvanced(Context context) { + super(context); + init(); + } + + /** + * Initializes all the views and variables in the advanced view. + */ + private void init() { + setOrientation(LinearLayout.VERTICAL); + + mHueDetails = createAndAddNewGradient(R.string.color_picker_hue, + HUE_SEEK_BAR_MAX, this); + mSaturationDetails = createAndAddNewGradient(R.string.color_picker_saturation, + SATURATION_SEEK_BAR_MAX, this); + mValueDetails = createAndAddNewGradient(R.string.color_picker_value, + VALUE_SEEK_BAR_MAX, this); + refreshGradientComponents(); + } + + /** + * Creates a new GradientDetails object from the parameters provided, initializes it, + * and adds it to this advanced view. + * + * @param textResourceId The text to display for the label. + * @param seekBarMax The maximum value of the seek bar for the gradient. + * @param seekBarListener Object listening to when the user changes the seek bar. + * + * @return A new GradientDetails object initialized with the given parameters. + */ + public ColorPickerAdvancedComponent createAndAddNewGradient(int textResourceId, + int seekBarMax, + OnSeekBarChangeListener seekBarListener) { + LayoutInflater inflater = (LayoutInflater) getContext() + .getSystemService(Context.LAYOUT_INFLATER_SERVICE); + View newComponent = inflater.inflate(R.layout.color_picker_advanced_component, null); + addView(newComponent); + + return new ColorPickerAdvancedComponent(newComponent, + textResourceId, + seekBarMax, + seekBarListener); + } + + /** + * Sets the listener for when the user changes the color. + * + * @param onColorChangedListener The object listening for the change in color. + */ + public void setListener(OnColorChangedListener onColorChangedListener) { + mOnColorChangedListener = onColorChangedListener; + } + + /** + * @return The color the user has currently chosen. + */ + public int getColor() { + return mCurrentColor; + } + + /** + * Sets the color that the user has currently chosen. + * + * @param color The currently chosen color. + */ + public void setColor(int color) { + mCurrentColor = color; + Color.colorToHSV(mCurrentColor, mCurrentHsvValues); + refreshGradientComponents(); + } + + /** + * Notifies the listener, if there is one, of a change in the selected color. + */ + private void notifyColorChanged() { + if (mOnColorChangedListener != null) { + mOnColorChangedListener.onColorChanged(getColor()); + } + } + + /** + * Callback for when a slider is updated on the advanced view. + * + * @param seekBar The color slider that was updated. + * @param progress The new value of the color slider. + * @param fromUser Whether it was the user the changed the value, or whether + * we were setting it up. + */ + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + if (fromUser) { + mCurrentHsvValues[0] = mHueDetails.getValue(); + mCurrentHsvValues[1] = mSaturationDetails.getValue() / 100.0f; + mCurrentHsvValues[2] = mValueDetails.getValue() / 100.0f; + + mCurrentColor = Color.HSVToColor(mCurrentHsvValues); + + updateHueGradient(); + updateSaturationGradient(); + updateValueGradient(); + + notifyColorChanged(); + } + } + + /** + * Updates only the hue gradient display with the hue value for the + * currently selected color. + */ + private void updateHueGradient() { + float[] tempHsvValues = new float[3]; + tempHsvValues[1] = mCurrentHsvValues[1]; + tempHsvValues[2] = mCurrentHsvValues[2]; + + int[] newColors = new int[HUE_COLOR_COUNT]; + + for (int i = 0; i < HUE_COLOR_COUNT; ++i) { + tempHsvValues[0] = i * 60.0f; + newColors[i] = Color.HSVToColor(tempHsvValues); + } + mHueDetails.setGradientColors(newColors); + } + + /** + * Updates only the saturation gradient display with the saturation value + * for the currently selected color. + */ + private void updateSaturationGradient() { + float[] tempHsvValues = new float[3]; + tempHsvValues[0] = mCurrentHsvValues[0]; + tempHsvValues[1] = 0.0f; + tempHsvValues[2] = mCurrentHsvValues[2]; + + int[] newColors = new int[SATURATION_COLOR_COUNT]; + + newColors[0] = Color.HSVToColor(tempHsvValues); + + tempHsvValues[1] = 1.0f; + newColors[1] = Color.HSVToColor(tempHsvValues); + mSaturationDetails.setGradientColors(newColors); + } + + /** + * Updates only the Value gradient display with the Value amount for + * the currently selected color. + */ + private void updateValueGradient() { + float[] tempHsvValues = new float[3]; + tempHsvValues[0] = mCurrentHsvValues[0]; + tempHsvValues[1] = mCurrentHsvValues[1]; + tempHsvValues[2] = 0.0f; + + int[] newColors = new int[VALUE_COLOR_COUNT]; + + newColors[0] = Color.HSVToColor(tempHsvValues); + + tempHsvValues[2] = 1.0f; + newColors[1] = Color.HSVToColor(tempHsvValues); + mValueDetails.setGradientColors(newColors); + } + + /** + * Updates all the gradient displays to show the currently selected color. + */ + private void refreshGradientComponents() { + // Round and bound the saturation value. + int saturationValue = Math.round(mCurrentHsvValues[1] * 100.0f); + saturationValue = Math.min(saturationValue, SATURATION_SEEK_BAR_MAX); + saturationValue = Math.max(saturationValue, 0); + + // Round and bound the Value amount. + int valueValue = Math.round(mCurrentHsvValues[2] * 100.0f); + valueValue = Math.min(valueValue, VALUE_SEEK_BAR_MAX); + valueValue = Math.max(valueValue, 0); + + // Don't need to round the hue value since its possible values match the seek bar + // range directly. + mHueDetails.setValue(mCurrentHsvValues[0]); + mSaturationDetails.setValue(saturationValue); + mValueDetails.setValue(valueValue); + + updateHueGradient(); + updateSaturationGradient(); + updateValueGradient(); + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + // Do nothing. + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + // Do nothing. + } +} diff --git a/ui/android/java/src/org/chromium/ui/ColorPickerAdvancedComponent.java b/ui/android/java/src/org/chromium/ui/ColorPickerAdvancedComponent.java new file mode 100644 index 0000000000000..c40b0a9fa716d --- /dev/null +++ b/ui/android/java/src/org/chromium/ui/ColorPickerAdvancedComponent.java @@ -0,0 +1,93 @@ +// Copyright 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.ui; + +import android.content.Context; +import android.graphics.drawable.GradientDrawable; +import android.graphics.drawable.GradientDrawable.Orientation; +import android.os.Build; +import android.view.View; +import android.widget.SeekBar; +import android.widget.SeekBar.OnSeekBarChangeListener; +import android.widget.TextView; + +/** + * Encapsulates a single gradient view of the HSV color display, including its label, gradient + * view and seek bar. + * + * Mirrors a "color_picker_advanced_component" layout. + */ +public class ColorPickerAdvancedComponent { + // The view that displays the gradient. + private final View mGradientView; + // The seek bar that allows the user to change the value of this component. + private final SeekBar mSeekBar; + // The set of colors to interpolate the gradient through. + private int[] mGradientColors; + // The Drawable that represents the gradient. + private GradientDrawable mGradientDrawable; + // The text label for the component. + private final TextView mText; + + /** + * Initializes the views. + * + * @param rootView View that contains all the content, such as the label, gradient view, etc. + * @param textResourceId The resource ID of the text to show on the label. + * @param seekBarMax The range of the seek bar. + * @param seekBarListener The listener for when the seek bar value changes. + */ + ColorPickerAdvancedComponent(final View rootView, + final int textResourceId, + final int seekBarMax, + final OnSeekBarChangeListener seekBarListener) { + mGradientView = rootView.findViewById(R.id.gradient); + mText = (TextView) rootView.findViewById(R.id.text); + mText.setText(textResourceId); + mGradientDrawable = new GradientDrawable(GradientDrawable.Orientation.LEFT_RIGHT, null); + mSeekBar = (SeekBar) rootView.findViewById(R.id.seek_bar); + mSeekBar.setOnSeekBarChangeListener(seekBarListener); + mSeekBar.setMax(seekBarMax); + // Setting the thumb offset means the seek bar thumb can move all the way to each end + // of the gradient view. + Context context = rootView.getContext(); + int offset = context.getResources() + .getDrawable(R.drawable.color_picker_advanced_select_handle) + .getIntrinsicWidth(); + mSeekBar.setThumbOffset(offset / 2); + } + + /** + * @return The value represented by this component, maintained by the seek bar progress. + */ + public float getValue() { + return mSeekBar.getProgress(); + } + + /** + * Sets the value of the component (by setting the seek bar value). + * + * @param newValue The value to give the component. + */ + public void setValue(float newValue) { + mSeekBar.setProgress((int) newValue); + } + + /** + * Sets the colors for the gradient view to interpolate through. + * + * @param newColors The set of colors representing the interpolation points for the gradient. + */ + public void setGradientColors(int[] newColors) { + mGradientColors = newColors.clone(); + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) { + Orientation currentOrientation = Orientation.LEFT_RIGHT; + mGradientDrawable = new GradientDrawable(currentOrientation, mGradientColors); + } else { + mGradientDrawable.setColors(mGradientColors); + } + mGradientView.setBackground(mGradientDrawable); + } +} diff --git a/ui/android/java/src/org/chromium/ui/ColorPickerDialog.java b/ui/android/java/src/org/chromium/ui/ColorPickerDialog.java new file mode 100644 index 0000000000000..f6d3ace5cb464 --- /dev/null +++ b/ui/android/java/src/org/chromium/ui/ColorPickerDialog.java @@ -0,0 +1,165 @@ +// Copyright 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.ui; + +import android.app.AlertDialog; +import android.app.Dialog; +import android.content.Context; +import android.content.DialogInterface; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.Button; +import android.widget.TextView; + +/** + * UI for the color chooser that shows on the Android platform as a result of + * <input type=color > form element. + */ +public class ColorPickerDialog extends AlertDialog implements OnColorChangedListener { + private final ColorPickerAdvanced mAdvancedColorPicker; + + private final ColorPickerSimple mSimpleColorPicker; + + private final Button mMoreButton; + + // The view up in the corner that shows the user the color they've currently selected. + private final View mCurrentColorView; + + private final OnColorChangedListener mListener; + + private final int mInitialColor; + + private int mCurrentColor; + + /** + * @param context The context the dialog is to run in. + * @param listener The object to notify when the color is set. + * @param color The initial color to set. + * @param suggestions The list of suggestions. + */ + public ColorPickerDialog(Context context, + OnColorChangedListener listener, + int color, + ColorSuggestion[] suggestions) { + super(context, 0); + + mListener = listener; + mInitialColor = color; + mCurrentColor = mInitialColor; + + // Initialize title + LayoutInflater inflater = (LayoutInflater) context + .getSystemService(Context.LAYOUT_INFLATER_SERVICE); + View title = inflater.inflate(R.layout.color_picker_dialog_title, null); + setCustomTitle(title); + + mCurrentColorView = title.findViewById(R.id.selected_color_view); + + TextView titleText = (TextView) title.findViewById(R.id.title); + titleText.setText(R.string.color_picker_dialog_title); + + // Initialize Set/Cancel buttons + String positiveButtonText = context.getString(R.string.color_picker_button_set); + setButton(BUTTON_POSITIVE, positiveButtonText, + new Dialog.OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) { + tryNotifyColorSet(mCurrentColor); + } + }); + + // Note that with the color picker there's not really any such thing as + // "cancelled". + // The color picker flow only finishes when we return a color, so we + // have to always + // return something. The concept of "cancelled" in this case just means + // returning + // the color that we were initialized with. + String negativeButtonText = context.getString(R.string.color_picker_button_cancel); + setButton(BUTTON_NEGATIVE, negativeButtonText, + new Dialog.OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) { + tryNotifyColorSet(mInitialColor); + } + }); + + setOnCancelListener(new DialogInterface.OnCancelListener() { + @Override + public void onCancel(DialogInterface arg0) { + tryNotifyColorSet(mInitialColor); + } + }); + + // Initialize main content view + View content = inflater.inflate(R.layout.color_picker_dialog_content, null); + setView(content); + + // Initialize More button. + mMoreButton = (Button) content.findViewById(R.id.more_colors_button); + mMoreButton.setOnClickListener(new Button.OnClickListener() { + @Override + public void onClick(View v) { + showAdvancedView(); + } + }); + + // Initialize advanced color view (hidden initially). + mAdvancedColorPicker = + (ColorPickerAdvanced) content.findViewById(R.id.color_picker_advanced); + mAdvancedColorPicker.setVisibility(View.GONE); + + // Initialize simple color view (default view). + mSimpleColorPicker = (ColorPickerSimple) content.findViewById(R.id.color_picker_simple); + mSimpleColorPicker.init(suggestions, this); + + updateCurrentColor(mInitialColor); + } + + /** + * Listens to the ColorPicker for when the user has changed the selected color, and + * updates the current color (the color shown in the title) accordingly. + * + * @param color The new color chosen by the user. + */ + @Override + public void onColorChanged(int color) { + updateCurrentColor(color); + } + + /** + * Hides the simple view (the default) and shows the advanced one instead, hiding the + * "More" button at the same time. + */ + private void showAdvancedView() { + // Only need to hide the borders, not the Views themselves, since the Views are + // contained within the borders. + View buttonBorder = findViewById(R.id.more_colors_button_border); + buttonBorder.setVisibility(View.GONE); + + View simpleView = findViewById(R.id.color_picker_simple); + simpleView.setVisibility(View.GONE); + + mAdvancedColorPicker.setVisibility(View.VISIBLE); + mAdvancedColorPicker.setListener(this); + mAdvancedColorPicker.setColor(mCurrentColor); + } + + /** + * Tries to notify any listeners that the color has been set. + */ + private void tryNotifyColorSet(int color) { + if (mListener != null) mListener.onColorChanged(color); + } + + /** + * Updates the internal cache of the currently selected color, updating the colorful little + * box in the title at the same time. + */ + private void updateCurrentColor(int color) { + mCurrentColor = color; + if (mCurrentColorView != null) mCurrentColorView.setBackgroundColor(color); + } +} diff --git a/ui/android/java/src/org/chromium/ui/ColorPickerMoreButton.java b/ui/android/java/src/org/chromium/ui/ColorPickerMoreButton.java new file mode 100644 index 0000000000000..849c4d8182b33 --- /dev/null +++ b/ui/android/java/src/org/chromium/ui/ColorPickerMoreButton.java @@ -0,0 +1,55 @@ +// Copyright 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.ui; +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.util.AttributeSet; +import android.widget.Button; + +/** + * Simple class that draws a white border around a button, purely for a UI change. + */ +public class ColorPickerMoreButton extends Button { + + // A cache for the paint used to draw the border, so it doesn't have to be created in + // every onDraw() call. + private Paint mBorderPaint; + + public ColorPickerMoreButton(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + public ColorPickerMoreButton(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + init(); + } + + /** + * Sets up the paint to use for drawing the border. + */ + public void init() { + mBorderPaint = new Paint(); + mBorderPaint.setStyle(Paint.Style.STROKE); + mBorderPaint.setColor(Color.WHITE); + // Set the width to one pixel. + mBorderPaint.setStrokeWidth(1.0f); + // And make sure the border doesn't bleed into the outside. + mBorderPaint.setAntiAlias(false); + } + + /** + * Draws the border around the edge of the button. + * + * @param canvas The canvas to draw on. + */ + @Override + protected void onDraw(Canvas canvas) { + canvas.drawRect(0.5f, 0.5f, getWidth() - 1.5f, getHeight() - 1.5f, mBorderPaint); + super.onDraw(canvas); + } +} diff --git a/ui/android/java/src/org/chromium/ui/ColorPickerSimple.java b/ui/android/java/src/org/chromium/ui/ColorPickerSimple.java new file mode 100644 index 0000000000000..69b880ea33b19 --- /dev/null +++ b/ui/android/java/src/org/chromium/ui/ColorPickerSimple.java @@ -0,0 +1,86 @@ +// Copyright 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.ui; + +import android.content.Context; +import android.graphics.Color; +import android.util.AttributeSet; +import android.widget.ListView; + +import org.chromium.ui.ColorSuggestionListAdapter.OnColorSuggestionClickListener; + +/** + * Draws a grid of (predefined) colors and allows the user to choose one of + * those colors. + */ +public class ColorPickerSimple extends ListView implements OnColorSuggestionClickListener { + + private OnColorChangedListener mOnColorChangedListener; + + private static final int[] DEFAULT_COLORS = { + Color.RED, + Color.CYAN, + Color.BLUE, + Color.GREEN, + Color.MAGENTA, + Color.YELLOW, + Color.BLACK, + Color.WHITE + }; + + private static final int[] DEFAULT_COLOR_LABEL_IDS = { + R.string.color_picker_button_red, + R.string.color_picker_button_cyan, + R.string.color_picker_button_blue, + R.string.color_picker_button_green, + R.string.color_picker_button_magenta, + R.string.color_picker_button_yellow, + R.string.color_picker_button_black, + R.string.color_picker_button_white + }; + + public ColorPickerSimple(Context context) { + super(context); + } + + public ColorPickerSimple(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public ColorPickerSimple(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + /** + * Initializes the listener and sets the adapter for the given list of suggestions. If the + * suggestions is null a default set of colors will be used. + * + * @param suggestions The list of suggestions that should be displayed. + * @param onColorChangedListener The listener that gets notified when the user touches + * a color. + */ + public void init(ColorSuggestion[] suggestions, + OnColorChangedListener onColorChangedListener) { + mOnColorChangedListener = onColorChangedListener; + + if (suggestions == null) { + suggestions = new ColorSuggestion[DEFAULT_COLORS.length]; + for (int i = 0; i < suggestions.length; ++i) { + suggestions[i] = new ColorSuggestion(DEFAULT_COLORS[i], + getContext().getString(DEFAULT_COLOR_LABEL_IDS[i])); + } + } + + ColorSuggestionListAdapter adapter = new ColorSuggestionListAdapter( + getContext(), suggestions); + adapter.setOnColorSuggestionClickListener(this); + setAdapter(adapter); + } + + @Override + public void onColorSuggestionClick(ColorSuggestion suggestion) { + mOnColorChangedListener.onColorChanged(suggestion.mColor); + } +} diff --git a/ui/android/java/src/org/chromium/ui/ColorSuggestion.java b/ui/android/java/src/org/chromium/ui/ColorSuggestion.java new file mode 100644 index 0000000000000..4e964a767d247 --- /dev/null +++ b/ui/android/java/src/org/chromium/ui/ColorSuggestion.java @@ -0,0 +1,24 @@ +// Copyright 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.ui; + +/** + * Color suggestion container used to store information for each color button that will be shown in + * the simple color picker. + */ +public class ColorSuggestion { + final int mColor; + final String mLabel; + + /** + * Constructs a color suggestion container. + * @param color The suggested color. + * @param label The label for the suggestion. + */ + public ColorSuggestion(int color, String label) { + mColor = color; + mLabel = label; + } +} diff --git a/ui/android/java/src/org/chromium/ui/ColorSuggestionListAdapter.java b/ui/android/java/src/org/chromium/ui/ColorSuggestionListAdapter.java new file mode 100644 index 0000000000000..bdf255cdb5f86 --- /dev/null +++ b/ui/android/java/src/org/chromium/ui/ColorSuggestionListAdapter.java @@ -0,0 +1,142 @@ +// Copyright 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.ui; + +import android.content.Context; +import android.graphics.Color; +import android.graphics.drawable.GradientDrawable; +import android.graphics.drawable.LayerDrawable; +import android.text.TextUtils; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AbsListView; +import android.widget.BaseAdapter; +import android.widget.LinearLayout; + +import org.chromium.base.ApiCompatibilityUtils; + +/** + * The adapter used to populate ColorPickerSimple. + */ +public class ColorSuggestionListAdapter extends BaseAdapter implements View.OnClickListener { + private Context mContext; + private ColorSuggestion[] mSuggestions; + private OnColorSuggestionClickListener mListener; + + /** + * The callback used to indicate the user has clicked on a suggestion. + */ + public interface OnColorSuggestionClickListener { + + /** + * Called upon a click on a suggestion. + * + * @param suggestion The suggestion that was clicked. + */ + void onColorSuggestionClick(ColorSuggestion suggestion); + } + + private static final int COLORS_PER_ROW = 4; + + ColorSuggestionListAdapter(Context context, ColorSuggestion[] suggestions) { + mContext = context; + mSuggestions = suggestions; + } + + /** + * Sets the listener that will be notified upon a click on a suggestion. + */ + public void setOnColorSuggestionClickListener(OnColorSuggestionClickListener listener) { + mListener = listener; + } + + /** + * Sets up the color button to represent a color suggestion. + * + * @param button The button view to set up. + * @param index The index of the suggestion in mSuggestions. + */ + private void setUpColorButton(View button, int index) { + if (index >= mSuggestions.length) { + button.setTag(null); + button.setContentDescription(null); + button.setVisibility(View.INVISIBLE); + return; + } + button.setTag(mSuggestions[index]); + button.setVisibility(View.VISIBLE); + ColorSuggestion suggestion = mSuggestions[index]; + LayerDrawable layers = (LayerDrawable) button.getBackground(); + GradientDrawable swatch = + (GradientDrawable) layers.findDrawableByLayerId(R.id.color_button_swatch); + swatch.setColor(suggestion.mColor); + String description = suggestion.mLabel; + if (TextUtils.isEmpty(description)) { + description = String.format("#%06X", (0xFFFFFF & suggestion.mColor)); + } + button.setContentDescription(description); + button.setOnClickListener(this); + } + + @Override + public void onClick(View v) { + if (mListener == null) { + return; + } + ColorSuggestion suggestion = (ColorSuggestion) v.getTag(); + if (suggestion == null) { + return; + } + mListener.onColorSuggestionClick(suggestion); + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + LinearLayout layout; + if (convertView != null && convertView instanceof LinearLayout) { + layout = (LinearLayout) convertView; + } else { + layout = new LinearLayout(mContext); + layout.setLayoutParams(new AbsListView.LayoutParams( + AbsListView.LayoutParams.MATCH_PARENT, + AbsListView.LayoutParams.WRAP_CONTENT)); + layout.setOrientation(LinearLayout.HORIZONTAL); + layout.setBackgroundColor(Color.WHITE); + int buttonHeight = + mContext.getResources().getDimensionPixelOffset(R.dimen.color_button_height); + for (int i = 0; i < COLORS_PER_ROW; ++i) { + View button = new View(mContext); + LinearLayout.LayoutParams layoutParams = + new LinearLayout.LayoutParams(0, buttonHeight, 1f); + ApiCompatibilityUtils.setMarginStart(layoutParams, -1); + if (i == COLORS_PER_ROW - 1) { + ApiCompatibilityUtils.setMarginEnd(layoutParams, -1); + } + button.setLayoutParams(layoutParams); + button.setBackgroundResource(R.drawable.color_button_background); + layout.addView(button); + } + } + for (int i = 0; i < COLORS_PER_ROW; ++i) { + setUpColorButton(layout.getChildAt(i), position * COLORS_PER_ROW + i); + } + return layout; + } + + @Override + public long getItemId(int position) { + return position; + } + + @Override + public Object getItem(int position) { + return null; + } + + @Override + public int getCount() { + return (mSuggestions.length + COLORS_PER_ROW - 1) / COLORS_PER_ROW; + } +} diff --git a/ui/android/java/src/org/chromium/ui/DropdownAdapter.java b/ui/android/java/src/org/chromium/ui/DropdownAdapter.java new file mode 100644 index 0000000000000..51bb6b65e9bb3 --- /dev/null +++ b/ui/android/java/src/org/chromium/ui/DropdownAdapter.java @@ -0,0 +1,117 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.ui; + +import android.content.Context; +import android.graphics.Color; +import android.graphics.Typeface; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AbsListView.LayoutParams; +import android.widget.ArrayAdapter; +import android.widget.TextView; + +import java.util.List; +import java.util.Set; + +/** + * Dropdown item adapter for DropdownPopupWindow. + */ +public class DropdownAdapter extends ArrayAdapter { + private Context mContext; + private Set mSeparators; + private boolean mAreAllItemsEnabled; + + public DropdownAdapter(Context context, List items, Set separators) { + super(context, R.layout.dropdown_item, items); + mSeparators = separators; + mContext = context; + mAreAllItemsEnabled = checkAreAllItemsEnabled(); + } + + public DropdownAdapter(Context context, DropdownItem[] items, Set separators) { + super(context, R.layout.dropdown_item, items); + mSeparators = separators; + mContext = context; + mAreAllItemsEnabled = checkAreAllItemsEnabled(); + } + + private boolean checkAreAllItemsEnabled() { + for (int i = 0; i < getCount(); i++) { + DropdownItem item = getItem(i); + if (item.isEnabled() && !item.isGroupHeader()) { + return false; + } + } + return true; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + View layout = convertView; + if (convertView == null) { + LayoutInflater inflater = + (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + layout = inflater.inflate(R.layout.dropdown_item, null); + layout.setBackground(new DropdownDividerDrawable()); + } + + DropdownItem item = getItem(position); + + TextView labelView = (TextView) layout.findViewById(R.id.dropdown_label); + labelView.setText(item.getLabel()); + + labelView.setEnabled(item.isEnabled()); + if (item.isGroupHeader()) { + labelView.setTypeface(null, Typeface.BOLD); + } else { + labelView.setTypeface(null, Typeface.NORMAL); + } + + DropdownDividerDrawable divider = (DropdownDividerDrawable) layout.getBackground(); + int height = mContext.getResources().getDimensionPixelSize(R.dimen.dropdown_item_height); + if (position == 0) { + divider.setColor(Color.TRANSPARENT); + } else { + int dividerHeight = mContext.getResources().getDimensionPixelSize( + R.dimen.dropdown_item_divider_height); + height += dividerHeight; + divider.setHeight(dividerHeight); + if (mSeparators != null && mSeparators.contains(position)) { + divider.setColor(mContext.getResources().getColor( + R.color.dropdown_dark_divider_color)); + } else { + divider.setColor(mContext.getResources().getColor( + R.color.dropdown_divider_color)); + } + } + layout.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, height)); + + TextView sublabelView = (TextView) layout.findViewById(R.id.dropdown_sublabel); + CharSequence sublabel = item.getSublabel(); + if (TextUtils.isEmpty(sublabel)) { + sublabelView.setVisibility(View.GONE); + } else { + sublabelView.setText(sublabel); + sublabelView.setVisibility(View.VISIBLE); + } + + return layout; + } + + @Override + public boolean areAllItemsEnabled() { + return mAreAllItemsEnabled; + } + + @Override + public boolean isEnabled(int position) { + if (position < 0 || position >= getCount()) return false; + DropdownItem item = getItem(position); + return item.isEnabled() && !item.isGroupHeader(); + } +} diff --git a/ui/android/java/src/org/chromium/ui/DropdownDividerDrawable.java b/ui/android/java/src/org/chromium/ui/DropdownDividerDrawable.java new file mode 100644 index 0000000000000..da5009b45198a --- /dev/null +++ b/ui/android/java/src/org/chromium/ui/DropdownDividerDrawable.java @@ -0,0 +1,54 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.ui; + +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.Paint; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; + +class DropdownDividerDrawable extends Drawable { + + private Paint mPaint; + private Rect mDividerRect; + + public DropdownDividerDrawable() { + mPaint = new Paint(); + mDividerRect = new Rect(); + } + + @Override + public void draw(Canvas canvas) { + canvas.drawRect(mDividerRect, mPaint); + } + + @Override + public void onBoundsChange(Rect bounds) { + mDividerRect.set(0, 0, bounds.width(), mDividerRect.height()); + } + + public void setHeight(int height) { + mDividerRect.set(0, 0, mDividerRect.right, height); + } + + public void setColor(int color) { + mPaint.setColor(color); + } + + @Override + public void setAlpha(int alpha) { + } + + @Override + public void setColorFilter(ColorFilter cf) { + } + + @Override + public int getOpacity() { + return PixelFormat.OPAQUE; + } +} diff --git a/ui/android/java/src/org/chromium/ui/DropdownItem.java b/ui/android/java/src/org/chromium/ui/DropdownItem.java new file mode 100644 index 0000000000000..46d8d0e215318 --- /dev/null +++ b/ui/android/java/src/org/chromium/ui/DropdownItem.java @@ -0,0 +1,27 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.ui; + +/** + * Dropdown item interface used to access all the information needed to show the item. + */ +public interface DropdownItem { + /** + * Returns the label that should be shown in the dropdown. + */ + String getLabel(); + /** + * Returns the sublabel that should be shown in the dropdown. + */ + String getSublabel(); + /** + * Returns true if the item should be enabled in the dropdown. + */ + boolean isEnabled(); + /** + * Returns true if the item should be a group header in the dropdown. + */ + boolean isGroupHeader(); +} diff --git a/ui/android/java/src/org/chromium/ui/DropdownPopupWindow.java b/ui/android/java/src/org/chromium/ui/DropdownPopupWindow.java new file mode 100644 index 0000000000000..a1b8ee6c4e04d --- /dev/null +++ b/ui/android/java/src/org/chromium/ui/DropdownPopupWindow.java @@ -0,0 +1,177 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.ui; + +import android.content.Context; +import android.graphics.Rect; +import android.util.Log; +import android.view.View; +import android.view.View.MeasureSpec; +import android.view.View.OnLayoutChangeListener; +import android.view.ViewGroup; +import android.widget.LinearLayout; +import android.widget.ListAdapter; +import android.widget.ListPopupWindow; +import android.widget.PopupWindow; + +import org.chromium.base.ApiCompatibilityUtils; +import org.chromium.ui.base.ViewAndroidDelegate; + +import java.lang.reflect.Method; + +/** + * The dropdown list popup window. + */ +public class DropdownPopupWindow extends ListPopupWindow { + + private final Context mContext; + private final ViewAndroidDelegate mViewAndroidDelegate; + private final View mAnchorView; + private float mAnchorWidth; + private float mAnchorHeight; + private float mAnchorX; + private float mAnchorY; + private boolean mRtl; + private OnLayoutChangeListener mLayoutChangeListener; + private PopupWindow.OnDismissListener mOnDismissListener; + ListAdapter mAdapter; + + /** + * Creates an DropdownPopupWindow with specified parameters. + * @param context Application context. + * @param viewAndroidDelegate View delegate used to add and remove views. + */ + public DropdownPopupWindow(Context context, ViewAndroidDelegate viewAndroidDelegate) { + super(context, null, 0, R.style.DropdownPopupWindow); + mContext = context; + mViewAndroidDelegate = viewAndroidDelegate; + + mAnchorView = mViewAndroidDelegate.acquireAnchorView(); + mAnchorView.setId(R.id.dropdown_popup_window); + mAnchorView.setTag(this); + + mLayoutChangeListener = new OnLayoutChangeListener() { + @Override + public void onLayoutChange(View v, int left, int top, int right, int bottom, + int oldLeft, int oldTop, int oldRight, int oldBottom) { + if (v == mAnchorView) DropdownPopupWindow.this.show(); + } + }; + mAnchorView.addOnLayoutChangeListener(mLayoutChangeListener); + + super.setOnDismissListener(new PopupWindow.OnDismissListener() { + @Override + public void onDismiss() { + if (mOnDismissListener != null) { + mOnDismissListener.onDismiss(); + } + mAnchorView.removeOnLayoutChangeListener(mLayoutChangeListener); + mAnchorView.setTag(null); + mViewAndroidDelegate.releaseAnchorView(mAnchorView); + } + }); + + setAnchorView(mAnchorView); + } + + /** + * Sets the location and the size of the anchor view that the DropdownPopupWindow will use to + * attach itself. + * @param x X coordinate of the top left corner of the anchor view. + * @param y Y coordinate of the top left corner of the anchor view. + * @param width The width of the anchor view. + * @param height The height of the anchor view. + */ + public void setAnchorRect(float x, float y, float width, float height) { + mAnchorWidth = width; + mAnchorHeight = height; + mAnchorX = x; + mAnchorY = y; + if (mAnchorView != null) { + mViewAndroidDelegate.setAnchorViewPosition(mAnchorView, mAnchorX, mAnchorY, + mAnchorWidth, mAnchorHeight); + } + } + + @Override + public void setAdapter(ListAdapter adapter) { + mAdapter = adapter; + super.setAdapter(adapter); + } + + @Override + public void show() { + // An ugly hack to keep the popup from expanding on top of the keyboard. + setInputMethodMode(INPUT_METHOD_NEEDED); + int contentWidth = measureContentWidth(); + float contentWidthInDip = contentWidth / + mContext.getResources().getDisplayMetrics().density; + if (contentWidthInDip > mAnchorWidth) { + setContentWidth(contentWidth); + final Rect displayFrame = new Rect(); + mAnchorView.getWindowVisibleDisplayFrame(displayFrame); + if (getWidth() > displayFrame.width()) { + setWidth(displayFrame.width()); + } + } else { + setWidth(ViewGroup.LayoutParams.WRAP_CONTENT); + } + mViewAndroidDelegate.setAnchorViewPosition(mAnchorView, mAnchorX, mAnchorY, mAnchorWidth, + mAnchorHeight); + super.show(); + getListView().setDividerHeight(0); + ApiCompatibilityUtils.setLayoutDirection(getListView(), + mRtl ? View.LAYOUT_DIRECTION_RTL : View.LAYOUT_DIRECTION_LTR); + + // HACK: The ListPopupWindow's mPopup automatically dismisses on an outside tap. There's + // no way to override it or prevent it, except reaching into ListPopupWindow's hidden + // API. This allows the C++ controller to completely control showing/hiding the popup. + // See http://crbug.com/400601 + try { + Method setForceIgnoreOutsideTouch = ListPopupWindow.class.getMethod( + "setForceIgnoreOutsideTouch", new Class[] { boolean.class }); + setForceIgnoreOutsideTouch.invoke(this, new Object[] { true }); + } catch (Exception e) { + Log.e("AutofillPopup", + "ListPopupWindow.setForceIgnoreOutsideTouch not found", + e); + } + } + + @Override + public void setOnDismissListener(PopupWindow.OnDismissListener listener) { + mOnDismissListener = listener; + } + + /** + * Sets the text direction in the dropdown. Should be called before show(). + * @param isRtl If true, then dropdown text direciton is right to left. + */ + protected void setRtl(boolean isRtl) { + mRtl = isRtl; + } + + /** + * Measures the width of the list content. + * @return The popup window width in pixels. + */ + private int measureContentWidth() { + int maxWidth = 0; + View itemView = null; + if (mAdapter == null) return 0; + final int widthMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); + final int heightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); + for (int i = 0; i < mAdapter.getCount(); i++) { + itemView = mAdapter.getView(i, itemView, null); + LinearLayout.LayoutParams params = + new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, + LinearLayout.LayoutParams.WRAP_CONTENT); + itemView.setLayoutParams(params); + itemView.measure(widthMeasureSpec, heightMeasureSpec); + maxWidth = Math.max(maxWidth, itemView.getMeasuredWidth()); + } + return maxWidth; + } +} diff --git a/ui/android/java/src/org/chromium/ui/OnColorChangedListener.java b/ui/android/java/src/org/chromium/ui/OnColorChangedListener.java new file mode 100644 index 0000000000000..c4847fa7f866b --- /dev/null +++ b/ui/android/java/src/org/chromium/ui/OnColorChangedListener.java @@ -0,0 +1,18 @@ +// Copyright 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.ui; + +/** + * The callback used to indicate the user changed the color. + */ +public interface OnColorChangedListener { + + /** + * Called upon a color change. + * + * @param color The color that was set. + */ + void onColorChanged(int color); +} \ No newline at end of file diff --git a/ui/android/java/src/org/chromium/ui/UiUtils.java b/ui/android/java/src/org/chromium/ui/UiUtils.java new file mode 100644 index 0000000000000..10f33162d47aa --- /dev/null +++ b/ui/android/java/src/org/chromium/ui/UiUtils.java @@ -0,0 +1,242 @@ +// Copyright 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.ui; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.os.Handler; +import android.util.Log; +import android.view.SurfaceView; +import android.view.View; +import android.view.ViewGroup; +import android.view.inputmethod.InputMethodManager; + +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Utility functions for common Android UI tasks. + * This class is not supposed to be instantiated. + */ +public class UiUtils { + private static final String TAG = "UiUtils"; + + private static final int KEYBOARD_RETRY_ATTEMPTS = 10; + private static final long KEYBOARD_RETRY_DELAY_MS = 100; + + /** + * Guards this class from being instantiated. + */ + private UiUtils() { + } + + /** The minimum size of the bottom margin below the app to detect a keyboard. */ + private static final float KEYBOARD_DETECT_BOTTOM_THRESHOLD_DP = 100; + + /** A delegate that allows disabling keyboard visibility detection. */ + private static KeyboardShowingDelegate sKeyboardShowingDelegate; + + /** + * A delegate that can be implemented to override whether or not keyboard detection will be + * used. + */ + public interface KeyboardShowingDelegate { + /** + * Will be called to determine whether or not to detect if the keyboard is visible. + * @param context A {@link Context} instance. + * @param view A {@link View}. + * @return Whether or not the keyboard check should be disabled. + */ + boolean disableKeyboardCheck(Context context, View view); + } + + /** + * Allows setting a delegate to override the default software keyboard visibility detection. + * @param delegate A {@link KeyboardShowingDelegate} instance. + */ + public static void setKeyboardShowingDelegate(KeyboardShowingDelegate delegate) { + sKeyboardShowingDelegate = delegate; + } + + /** + * Shows the software keyboard if necessary. + * @param view The currently focused {@link View}, which would receive soft keyboard input. + */ + public static void showKeyboard(final View view) { + final Handler handler = new Handler(); + final AtomicInteger attempt = new AtomicInteger(); + Runnable openRunnable = new Runnable() { + @Override + public void run() { + // Not passing InputMethodManager.SHOW_IMPLICIT as it does not trigger the + // keyboard in landscape mode. + InputMethodManager imm = + (InputMethodManager) view.getContext().getSystemService( + Context.INPUT_METHOD_SERVICE); + try { + imm.showSoftInput(view, 0); + } catch (IllegalArgumentException e) { + if (attempt.incrementAndGet() <= KEYBOARD_RETRY_ATTEMPTS) { + handler.postDelayed(this, KEYBOARD_RETRY_DELAY_MS); + } else { + Log.e(TAG, "Unable to open keyboard. Giving up.", e); + } + } + } + }; + openRunnable.run(); + } + + /** + * Hides the keyboard. + * @param view The {@link View} that is currently accepting input. + * @return Whether the keyboard was visible before. + */ + public static boolean hideKeyboard(View view) { + InputMethodManager imm = + (InputMethodManager) view.getContext().getSystemService( + Context.INPUT_METHOD_SERVICE); + return imm.hideSoftInputFromWindow(view.getWindowToken(), 0); + } + + /** + * Detects whether or not the keyboard is showing. This is a best guess as there is no + * standardized/foolproof way to do this. + * @param context A {@link Context} instance. + * @param view A {@link View}. + * @return Whether or not the software keyboard is visible and taking up screen space. + */ + public static boolean isKeyboardShowing(Context context, View view) { + if (sKeyboardShowingDelegate != null + && sKeyboardShowingDelegate.disableKeyboardCheck(context, view)) { + return false; + } + + View rootView = view.getRootView(); + if (rootView == null) return false; + Rect appRect = new Rect(); + rootView.getWindowVisibleDisplayFrame(appRect); + + final float density = context.getResources().getDisplayMetrics().density; + final float bottomMarginDp = Math.abs(rootView.getHeight() - appRect.height()) / density; + return bottomMarginDp > KEYBOARD_DETECT_BOTTOM_THRESHOLD_DP; + } + + /** + * Inserts a {@link View} into a {@link ViewGroup} after directly before a given {@View}. + * @param container The {@link View} to add newView to. + * @param newView The new {@link View} to add. + * @param existingView The {@link View} to insert the newView before. + * @return The index where newView was inserted, or -1 if it was not inserted. + */ + public static int insertBefore(ViewGroup container, View newView, View existingView) { + return insertView(container, newView, existingView, false); + } + + /** + * Inserts a {@link View} into a {@link ViewGroup} after directly after a given {@View}. + * @param container The {@link View} to add newView to. + * @param newView The new {@link View} to add. + * @param existingView The {@link View} to insert the newView after. + * @return The index where newView was inserted, or -1 if it was not inserted. + */ + public static int insertAfter(ViewGroup container, View newView, View existingView) { + return insertView(container, newView, existingView, true); + } + + private static int insertView( + ViewGroup container, View newView, View existingView, boolean after) { + // See if the view has already been added. + int index = container.indexOfChild(newView); + if (index >= 0) return index; + + // Find the location of the existing view. + index = container.indexOfChild(existingView); + if (index < 0) return -1; + + // Add the view. + if (after) index++; + container.addView(newView, index); + return index; + } + + /** + * Generates a scaled screenshot of the given view. The maximum size of the screenshot is + * determined by maximumDimension. + * + * @param currentView The view to generate a screenshot of. + * @param maximumDimension The maximum width or height of the generated screenshot. The bitmap + * will be scaled to ensure the maximum width or height is equal to or + * less than this. Any value <= 0, will result in no scaling. + * @param bitmapConfig Bitmap config for the generated screenshot (ARGB_8888 or RGB_565). + * @return The screen bitmap of the view or null if a problem was encountered. + */ + public static Bitmap generateScaledScreenshot( + View currentView, int maximumDimension, Bitmap.Config bitmapConfig) { + Bitmap screenshot = null; + boolean drawingCacheEnabled = currentView.isDrawingCacheEnabled(); + try { + prepareViewHierarchyForScreenshot(currentView, true); + if (!drawingCacheEnabled) currentView.setDrawingCacheEnabled(true); + // Android has a maximum drawing cache size and if the drawing cache is bigger + // than that, getDrawingCache() returns null. + Bitmap originalBitmap = currentView.getDrawingCache(); + if (originalBitmap != null) { + double originalHeight = originalBitmap.getHeight(); + double originalWidth = originalBitmap.getWidth(); + int newWidth = (int) originalWidth; + int newHeight = (int) originalHeight; + if (maximumDimension > 0) { + double scale = maximumDimension / Math.max(originalWidth, originalHeight); + newWidth = (int) Math.round(originalWidth * scale); + newHeight = (int) Math.round(originalHeight * scale); + } + Bitmap scaledScreenshot = + Bitmap.createScaledBitmap(originalBitmap, newWidth, newHeight, true); + if (scaledScreenshot.getConfig() != bitmapConfig) { + screenshot = scaledScreenshot.copy(bitmapConfig, false); + scaledScreenshot.recycle(); + scaledScreenshot = null; + } else { + screenshot = scaledScreenshot; + } + } else if (currentView.getMeasuredHeight() > 0 && currentView.getMeasuredWidth() > 0) { + double originalHeight = currentView.getMeasuredHeight(); + double originalWidth = currentView.getMeasuredWidth(); + int newWidth = (int) originalWidth; + int newHeight = (int) originalHeight; + if (maximumDimension > 0) { + double scale = maximumDimension / Math.max(originalWidth, originalHeight); + newWidth = (int) Math.round(originalWidth * scale); + newHeight = (int) Math.round(originalHeight * scale); + } + Bitmap bitmap = Bitmap.createBitmap(newWidth, newHeight, bitmapConfig); + Canvas canvas = new Canvas(bitmap); + canvas.scale((float) (newWidth / originalWidth), + (float) (newHeight / originalHeight)); + currentView.draw(canvas); + screenshot = bitmap; + } + } catch (OutOfMemoryError e) { + Log.d(TAG, "Unable to capture screenshot and scale it down." + e.getMessage()); + } finally { + if (!drawingCacheEnabled) currentView.setDrawingCacheEnabled(false); + prepareViewHierarchyForScreenshot(currentView, false); + } + return screenshot; + } + + private static void prepareViewHierarchyForScreenshot(View view, boolean takingScreenshot) { + if (view instanceof ViewGroup) { + ViewGroup viewGroup = (ViewGroup) view; + for (int i = 0; i < viewGroup.getChildCount(); i++) { + prepareViewHierarchyForScreenshot(viewGroup.getChildAt(i), takingScreenshot); + } + } else if (view instanceof SurfaceView) { + view.setWillNotDraw(!takingScreenshot); + } + } +} diff --git a/ui/android/java/src/org/chromium/ui/VSyncMonitor.java b/ui/android/java/src/org/chromium/ui/VSyncMonitor.java new file mode 100644 index 0000000000000..e717851aa23d8 --- /dev/null +++ b/ui/android/java/src/org/chromium/ui/VSyncMonitor.java @@ -0,0 +1,242 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.ui; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.os.Build; +import android.os.Handler; +import android.view.Choreographer; +import android.view.WindowManager; + +import org.chromium.base.TraceEvent; + +/** + * Notifies clients of the default displays's vertical sync pulses. + * On ICS, VSyncMonitor relies on setVSyncPointForICS() being called to set a reasonable + * approximation of a vertical sync starting point; see also http://crbug.com/156397. + */ +@SuppressLint("NewApi") +public class VSyncMonitor { + private static final long NANOSECONDS_PER_SECOND = 1000000000; + private static final long NANOSECONDS_PER_MILLISECOND = 1000000; + private static final long NANOSECONDS_PER_MICROSECOND = 1000; + + private boolean mInsideVSync = false; + + // Conservative guess about vsync's consecutivity. + // If true, next tick is guaranteed to be consecutive. + private boolean mConsecutiveVSync = false; + + /** + * VSync listener class + */ + public interface Listener { + /** + * Called very soon after the start of the display's vertical sync period. + * @param monitor The VSyncMonitor that triggered the signal. + * @param vsyncTimeMicros Absolute frame time in microseconds. + */ + public void onVSync(VSyncMonitor monitor, long vsyncTimeMicros); + } + + private Listener mListener; + + // Display refresh rate as reported by the system. + private long mRefreshPeriodNano; + + private boolean mHaveRequestInFlight; + + // Choreographer is used to detect vsync on >= JB. + private final Choreographer mChoreographer; + private final Choreographer.FrameCallback mVSyncFrameCallback; + + // On ICS we just post a task through the handler (http://crbug.com/156397) + private final Runnable mVSyncRunnableCallback; + private long mGoodStartingPointNano; + private long mLastPostedNano; + + // If the monitor is activated after having been idle, we synthesize the first vsync to reduce + // latency. + private final Handler mHandler = new Handler(); + private final Runnable mSyntheticVSyncRunnable; + private long mLastVSyncCpuTimeNano; + + /** + * Constructs a VSyncMonitor + * @param context The application context. + * @param listener The listener receiving VSync notifications. + */ + public VSyncMonitor(Context context, VSyncMonitor.Listener listener) { + this(context, listener, true); + } + + /** + * Constructs a VSyncMonitor + * @param context The application context. + * @param listener The listener receiving VSync notifications. + * @param enableJBVsync Whether to allow Choreographer-based notifications on JB and up. + */ + public VSyncMonitor(Context context, VSyncMonitor.Listener listener, boolean enableJBVSync) { + mListener = listener; + float refreshRate = ((WindowManager) context.getSystemService(Context.WINDOW_SERVICE)) + .getDefaultDisplay().getRefreshRate(); + final boolean useEstimatedRefreshPeriod = refreshRate < 30; + + if (refreshRate <= 0) refreshRate = 60; + mRefreshPeriodNano = (long) (NANOSECONDS_PER_SECOND / refreshRate); + + if (enableJBVSync && Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + // Use Choreographer on JB+ to get notified of vsync. + mChoreographer = Choreographer.getInstance(); + mVSyncFrameCallback = new Choreographer.FrameCallback() { + @Override + public void doFrame(long frameTimeNanos) { + TraceEvent.begin("VSync"); + if (useEstimatedRefreshPeriod && mConsecutiveVSync) { + // Display.getRefreshRate() is unreliable on some platforms. + // Adjust refresh period- initial value is based on Display.getRefreshRate() + // after that it asymptotically approaches the real value. + long lastRefreshDurationNano = frameTimeNanos - mGoodStartingPointNano; + float lastRefreshDurationWeight = 0.1f; + mRefreshPeriodNano += (long) (lastRefreshDurationWeight * + (lastRefreshDurationNano - mRefreshPeriodNano)); + } + mGoodStartingPointNano = frameTimeNanos; + onVSyncCallback(frameTimeNanos, getCurrentNanoTime()); + TraceEvent.end("VSync"); + } + }; + mVSyncRunnableCallback = null; + } else { + // On ICS we just hope that running tasks is relatively predictable. + mChoreographer = null; + mVSyncFrameCallback = null; + mVSyncRunnableCallback = new Runnable() { + @Override + public void run() { + TraceEvent.begin("VSyncTimer"); + final long currentTime = getCurrentNanoTime(); + onVSyncCallback(currentTime, currentTime); + TraceEvent.end("VSyncTimer"); + } + }; + mLastPostedNano = 0; + } + mSyntheticVSyncRunnable = new Runnable() { + @Override + public void run() { + TraceEvent.begin("VSyncSynthetic"); + final long currentTime = getCurrentNanoTime(); + onVSyncCallback(estimateLastVSyncTime(currentTime), currentTime); + TraceEvent.end("VSyncSynthetic"); + } + }; + mGoodStartingPointNano = getCurrentNanoTime(); + } + + /** + * Returns the time interval between two consecutive vsync pulses in microseconds. + */ + public long getVSyncPeriodInMicroseconds() { + return mRefreshPeriodNano / NANOSECONDS_PER_MICROSECOND; + } + + /** + * Determine whether a true vsync signal is available on this platform. + */ + private boolean isVSyncSignalAvailable() { + return mChoreographer != null; + } + + /** + * Request to be notified of the closest display vsync events. + * Listener.onVSync() will be called soon after the upcoming vsync pulses. + */ + public void requestUpdate() { + postCallback(); + } + + /** + * Set the best guess of the point in the past when the vsync has happened. + * @param goodStartingPointNano Known vsync point in the past. + */ + public void setVSyncPointForICS(long goodStartingPointNano) { + mGoodStartingPointNano = goodStartingPointNano; + } + + /** + * @return true if onVSync handler is executing. If onVSync handler + * introduces invalidations, View#invalidate() should be called. If + * View#postInvalidateOnAnimation is called instead, the corresponding onDraw + * will be delayed by one frame. The embedder of VSyncMonitor should check + * this value if it wants to post an invalidation. + */ + public boolean isInsideVSync() { + return mInsideVSync; + } + + private long getCurrentNanoTime() { + return System.nanoTime(); + } + + private void onVSyncCallback(long frameTimeNanos, long currentTimeNanos) { + assert mHaveRequestInFlight; + mInsideVSync = true; + mHaveRequestInFlight = false; + mLastVSyncCpuTimeNano = currentTimeNanos; + try { + if (mListener != null) { + mListener.onVSync(this, frameTimeNanos / NANOSECONDS_PER_MICROSECOND); + } + } finally { + mInsideVSync = false; + } + } + + private void postCallback() { + if (mHaveRequestInFlight) return; + mHaveRequestInFlight = true; + if (postSyntheticVSync()) return; + if (isVSyncSignalAvailable()) { + mConsecutiveVSync = mInsideVSync; + mChoreographer.postFrameCallback(mVSyncFrameCallback); + } else { + postRunnableCallback(); + } + } + + private boolean postSyntheticVSync() { + final long currentTime = getCurrentNanoTime(); + // Only trigger a synthetic vsync if we've been idle for long enough and the upcoming real + // vsync is more than half a frame away. + if (currentTime - mLastVSyncCpuTimeNano < 2 * mRefreshPeriodNano) return false; + if (currentTime - estimateLastVSyncTime(currentTime) > mRefreshPeriodNano / 2) return false; + mHandler.post(mSyntheticVSyncRunnable); + return true; + } + + private long estimateLastVSyncTime(long currentTime) { + final long lastRefreshTime = mGoodStartingPointNano + + ((currentTime - mGoodStartingPointNano) / mRefreshPeriodNano) * mRefreshPeriodNano; + return lastRefreshTime; + } + + private void postRunnableCallback() { + assert !isVSyncSignalAvailable(); + final long currentTime = getCurrentNanoTime(); + final long lastRefreshTime = estimateLastVSyncTime(currentTime); + long delay = (lastRefreshTime + mRefreshPeriodNano) - currentTime; + assert delay > 0 && delay <= mRefreshPeriodNano; + + if (currentTime + delay <= mLastPostedNano + mRefreshPeriodNano / 2) { + delay += mRefreshPeriodNano; + } + + mLastPostedNano = currentTime + delay; + if (delay == 0) mHandler.post(mVSyncRunnableCallback); + else mHandler.postDelayed(mVSyncRunnableCallback, delay / NANOSECONDS_PER_MILLISECOND); + } +} diff --git a/ui/android/java/src/org/chromium/ui/autofill/AutofillPopup.java b/ui/android/java/src/org/chromium/ui/autofill/AutofillPopup.java new file mode 100644 index 0000000000000..7b9f3fb07007c --- /dev/null +++ b/ui/android/java/src/org/chromium/ui/autofill/AutofillPopup.java @@ -0,0 +1,115 @@ +// Copyright 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.ui.autofill; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.view.View; +import android.widget.AdapterView; +import android.widget.PopupWindow; + +import org.chromium.ui.DropdownAdapter; +import org.chromium.ui.DropdownItem; +import org.chromium.ui.DropdownPopupWindow; +import org.chromium.ui.base.ViewAndroidDelegate; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; + +/** + * The Autofill suggestion popup that lists relevant suggestions. + */ +public class AutofillPopup extends DropdownPopupWindow implements AdapterView.OnItemClickListener, + PopupWindow.OnDismissListener { + + /** + * The constant used to specify a separator in a list of Autofill suggestions. + * Has to be kept in sync with enum in WebAutofillClient.h + */ + private static final int ITEM_ID_SEPARATOR_ENTRY = -3; + + private final Context mContext; + private final AutofillPopupDelegate mAutofillCallback; + private List mSuggestions; + + + /** + * An interface to handle the touch interaction with an AutofillPopup object. + */ + public interface AutofillPopupDelegate { + /** + * Informs the controller the AutofillPopup was hidden. + */ + public void dismissed(); + + /** + * Handles the selection of an Autofill suggestion from an AutofillPopup. + * @param listIndex The index of the selected Autofill suggestion. + */ + public void suggestionSelected(int listIndex); + } + + /** + * Creates an AutofillWindow with specified parameters. + * @param context Application context. + * @param viewAndroidDelegate View delegate used to add and remove views. + * @param autofillCallback A object that handles the calls to the native AutofillPopupView. + */ + public AutofillPopup(Context context, ViewAndroidDelegate viewAndroidDelegate, + AutofillPopupDelegate autofillCallback) { + super(context, viewAndroidDelegate); + mContext = context; + mAutofillCallback = autofillCallback; + + setOnItemClickListener(this); + setOnDismissListener(this); + } + + /** + * Filters the Autofill suggestions to the ones that we support and shows the popup. + * @param suggestions Autofill suggestion data. + */ + @SuppressLint("InlinedApi") + public void filterAndShow(AutofillSuggestion[] suggestions, boolean isRtl) { + mSuggestions = new ArrayList(Arrays.asList(suggestions)); + // Remove the AutofillSuggestions with IDs that are not supported by Android + ArrayList cleanedData = new ArrayList(); + HashSet separators = new HashSet(); + for (int i = 0; i < suggestions.length; i++) { + int itemId = suggestions[i].getSuggestionId(); + if (itemId == ITEM_ID_SEPARATOR_ENTRY) { + separators.add(cleanedData.size()); + } else { + cleanedData.add(suggestions[i]); + } + } + + setAdapter(new DropdownAdapter(mContext, cleanedData, separators)); + setRtl(isRtl); + show(); + } + + /** + * Hides the popup. + */ + public void hide() { + dismiss(); + } + + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + DropdownAdapter adapter = (DropdownAdapter) parent.getAdapter(); + int listIndex = mSuggestions.indexOf(adapter.getItem(position)); + assert listIndex > -1; + mAutofillCallback.suggestionSelected(listIndex); + } + + @Override + public void onDismiss() { + mAutofillCallback.dismissed(); + } +} diff --git a/ui/android/java/src/org/chromium/ui/autofill/AutofillSuggestion.java b/ui/android/java/src/org/chromium/ui/autofill/AutofillSuggestion.java new file mode 100644 index 0000000000000..a1fb2b02c5399 --- /dev/null +++ b/ui/android/java/src/org/chromium/ui/autofill/AutofillSuggestion.java @@ -0,0 +1,52 @@ +// Copyright 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.ui.autofill; + +import org.chromium.ui.DropdownItem; + +/** + * Autofill suggestion container used to store information needed for each Autofill popup entry. + */ +public class AutofillSuggestion implements DropdownItem { + private final String mLabel; + private final String mSublabel; + private final int mSuggestionId; + + /** + * Constructs a Autofill suggestion container. + * @param name The name of the Autofill suggestion. + * @param label The describing label of the Autofill suggestion. + * @param suggestionId The type of suggestion. + */ + public AutofillSuggestion(String name, String label, int suggestionId) { + mLabel = name; + mSublabel = label; + mSuggestionId = suggestionId; + } + + @Override + public String getLabel() { + return mLabel; + } + + @Override + public String getSublabel() { + return mSublabel; + } + + @Override + public boolean isEnabled() { + return true; + } + + @Override + public boolean isGroupHeader() { + return false; + } + + public int getSuggestionId() { + return mSuggestionId; + } +} diff --git a/ui/android/java/src/org/chromium/ui/autofill/OWNERS b/ui/android/java/src/org/chromium/ui/autofill/OWNERS new file mode 100644 index 0000000000000..812e8d9572e95 --- /dev/null +++ b/ui/android/java/src/org/chromium/ui/autofill/OWNERS @@ -0,0 +1 @@ +aurimas@chromium.org diff --git a/ui/android/java/src/org/chromium/ui/base/ActivityWindowAndroid.java b/ui/android/java/src/org/chromium/ui/base/ActivityWindowAndroid.java new file mode 100644 index 0000000000000..07f48dcc13f32 --- /dev/null +++ b/ui/android/java/src/org/chromium/ui/base/ActivityWindowAndroid.java @@ -0,0 +1,111 @@ +// Copyright 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.ui.base; + +import android.app.Activity; +import android.app.PendingIntent; +import android.content.ActivityNotFoundException; +import android.content.Intent; +import android.content.IntentSender.SendIntentException; + +import java.lang.ref.WeakReference; + +/** + * The class provides the WindowAndroid's implementation which requires + * Activity Instance. + * Only instantiate this class when you need the implemented features. + */ +public class ActivityWindowAndroid extends WindowAndroid { + // Constants used for intent request code bounding. + private static final int REQUEST_CODE_PREFIX = 1000; + private static final int REQUEST_CODE_RANGE_SIZE = 100; + private static final String TAG = "ActivityWindowAndroid"; + + private final WeakReference mActivityRef; + private int mNextRequestCode = 0; + + public ActivityWindowAndroid(Activity activity) { + super(activity.getApplicationContext()); + mActivityRef = new WeakReference(activity); + } + + @Override + public int showCancelableIntent(PendingIntent intent, IntentCallback callback, int errorId) { + Activity activity = mActivityRef.get(); + if (activity == null) return START_INTENT_FAILURE; + + int requestCode = generateNextRequestCode(); + + try { + activity.startIntentSenderForResult( + intent.getIntentSender(), requestCode, new Intent(), 0, 0, 0); + } catch (SendIntentException e) { + return START_INTENT_FAILURE; + } + + storeCallbackData(requestCode, callback, errorId); + return requestCode; + } + + @Override + public int showCancelableIntent(Intent intent, IntentCallback callback, int errorId) { + Activity activity = mActivityRef.get(); + if (activity == null) return START_INTENT_FAILURE; + + int requestCode = generateNextRequestCode(); + + try { + activity.startActivityForResult(intent, requestCode); + } catch (ActivityNotFoundException e) { + return START_INTENT_FAILURE; + } + + storeCallbackData(requestCode, callback, errorId); + return requestCode; + } + + @Override + public void cancelIntent(int requestCode) { + Activity activity = mActivityRef.get(); + if (activity == null) return; + activity.finishActivity(requestCode); + } + + @Override + public boolean onActivityResult(int requestCode, int resultCode, Intent data) { + IntentCallback callback = mOutstandingIntents.get(requestCode); + mOutstandingIntents.delete(requestCode); + String errorMessage = mIntentErrors.remove(requestCode); + + if (callback != null) { + callback.onIntentCompleted(this, resultCode, + mApplicationContext.getContentResolver(), data); + return true; + } else { + if (errorMessage != null) { + showCallbackNonExistentError(errorMessage); + return true; + } + } + return false; + } + + @Override + public WeakReference getActivity() { + // Return a new WeakReference to prevent clients from releasing our internal WeakReference. + return new WeakReference(mActivityRef.get()); + } + + private int generateNextRequestCode() { + int requestCode = REQUEST_CODE_PREFIX + mNextRequestCode; + mNextRequestCode = (mNextRequestCode + 1) % REQUEST_CODE_RANGE_SIZE; + return requestCode; + } + + private void storeCallbackData(int requestCode, IntentCallback callback, int errorId) { + mOutstandingIntents.put(requestCode, callback); + mIntentErrors.put(requestCode, mApplicationContext.getString(errorId)); + } +} diff --git a/ui/android/java/src/org/chromium/ui/base/Clipboard.java b/ui/android/java/src/org/chromium/ui/base/Clipboard.java new file mode 100644 index 0000000000000..67aaca273fe95 --- /dev/null +++ b/ui/android/java/src/org/chromium/ui/base/Clipboard.java @@ -0,0 +1,172 @@ +// Copyright 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.ui.base; + +import android.annotation.TargetApi; +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.Context; +import android.os.Build; +import android.widget.Toast; + +import org.chromium.base.CalledByNative; +import org.chromium.base.JNINamespace; +import org.chromium.ui.R; + +/** + * Simple proxy that provides C++ code with an access pathway to the Android + * clipboard. + */ +@JNINamespace("ui") +public class Clipboard { + + private static final boolean IS_HTML_CLIPBOARD_SUPPORTED = + Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN; + + // Necessary for coercing clipboard contents to text if they require + // access to network resources, etceteras (e.g., URI in clipboard) + private final Context mContext; + + private final ClipboardManager mClipboardManager; + + /** + * Use the factory constructor instead. + * + * @param context for accessing the clipboard + */ + public Clipboard(final Context context) { + mContext = context; + mClipboardManager = (ClipboardManager) + context.getSystemService(Context.CLIPBOARD_SERVICE); + } + + /** + * Returns a new Clipboard object bound to the specified context. + * + * @param context for accessing the clipboard + * @return the new object + */ + @CalledByNative + private static Clipboard create(final Context context) { + return new Clipboard(context); + } + + /** + * Emulates the behavior of the now-deprecated + * {@link android.text.ClipboardManager#getText()} by invoking + * {@link android.content.ClipData.Item#coerceToText(Context)} on the first + * item in the clipboard (if any) and returning the result as a string. + *

+ * This is quite different than simply calling {@link Object#toString()} on + * the clip; consumers of this API should familiarize themselves with the + * process described in + * {@link android.content.ClipData.Item#coerceToText(Context)} before using + * this method. + * + * @return a string representation of the first item on the clipboard, if + * the clipboard currently has an item and coercion of the item into + * a string is possible; otherwise, null + */ + @SuppressWarnings("javadoc") + @CalledByNative + private String getCoercedText() { + final ClipData clip = mClipboardManager.getPrimaryClip(); + if (clip != null && clip.getItemCount() > 0) { + final CharSequence sequence = clip.getItemAt(0).coerceToText(mContext); + if (sequence != null) { + return sequence.toString(); + } + } + return null; + } + + /** + * Gets the HTML text of top item on the primary clip on the Android clipboard. + * + * @return a Java string with the html text if any, or null if there is no html + * text or no entries on the primary clip. + */ + @TargetApi(Build.VERSION_CODES.JELLY_BEAN) + @CalledByNative + private String getHTMLText() { + if (IS_HTML_CLIPBOARD_SUPPORTED) { + final ClipData clip = mClipboardManager.getPrimaryClip(); + if (clip != null && clip.getItemCount() > 0) { + return clip.getItemAt(0).getHtmlText(); + } + } + return null; + } + + /** + * Emulates the behavior of the now-deprecated + * {@link android.text.ClipboardManager#setText(CharSequence)}, setting the + * clipboard's current primary clip to a plain-text clip that consists of + * the specified string. + * + * @param label will become the label of the clipboard's primary clip + * @param text will become the content of the clipboard's primary clip + */ + public void setText(final String label, final String text) { + setPrimaryClipNoException(ClipData.newPlainText(label, text)); + } + + /** + * Emulates the behavior of the now-deprecated + * {@link android.text.ClipboardManager#setText(CharSequence)}, setting the + * clipboard's current primary clip to a plain-text clip that consists of + * the specified string. + * + * @param text will become the content of the clipboard's primary clip + */ + @CalledByNative + public void setText(final String text) { + setText(null, text); + } + + /** + * Writes HTML to the clipboard, together with a plain-text representation + * of that very data. This API is only available in Android JellyBean+ and + * will be a no-operation in older versions. + * + * @param html The HTML content to be pasted to the clipboard. + * @param label The Plain-text label for the HTML content. + * @param text Plain-text representation of the HTML content. + */ + @TargetApi(Build.VERSION_CODES.JELLY_BEAN) + public void setHTMLText(final String html, final String label, final String text) { + if (IS_HTML_CLIPBOARD_SUPPORTED) { + setPrimaryClipNoException(ClipData.newHtmlText(label, text, html)); + } + } + + /** + * Writes HTML to the clipboard, together with a plain-text representation + * of that very data. This API is only available in Android JellyBean+ and + * will be a no-operation in older versions. + * + * @param html The HTML content to be pasted to the clipboard. + * @param text Plain-text representation of the HTML content. + */ + @CalledByNative + public void setHTMLText(final String html, final String text) { + setHTMLText(html, null, text); + } + + @CalledByNative + private static boolean isHTMLClipboardSupported() { + return IS_HTML_CLIPBOARD_SUPPORTED; + } + + private void setPrimaryClipNoException(ClipData clip) { + try { + mClipboardManager.setPrimaryClip(clip); + } catch (Exception ex) { + // Ignore any exceptions here as certain devices have bugs and will fail. + String text = mContext.getString(R.string.copy_to_clipboard_failure_message); + Toast.makeText(mContext, text, Toast.LENGTH_SHORT).show(); + } + } +} diff --git a/ui/android/java/src/org/chromium/ui/base/DeviceFormFactor.java b/ui/android/java/src/org/chromium/ui/base/DeviceFormFactor.java new file mode 100644 index 0000000000000..78f86c9f9deb6 --- /dev/null +++ b/ui/android/java/src/org/chromium/ui/base/DeviceFormFactor.java @@ -0,0 +1,36 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.ui.base; + +import android.content.Context; + +import org.chromium.base.CalledByNative; + +/** + * UI utilities for accessing form factor information. + */ +public class DeviceFormFactor { + + /** + * The minimum width that would classify the device as a tablet. + */ + private static final int MINIMUM_TABLET_WIDTH_DP = 600; + + private static Boolean sIsTablet = null; + + /** + * @param context Android's context + * @return Whether the app is should treat the device as a tablet for layout. + */ + @CalledByNative + public static boolean isTablet(Context context) { + if (sIsTablet == null) { + int minimumScreenWidthDp = context.getResources().getConfiguration(). + smallestScreenWidthDp; + sIsTablet = minimumScreenWidthDp >= MINIMUM_TABLET_WIDTH_DP; + } + return sIsTablet; + } +} diff --git a/ui/android/java/src/org/chromium/ui/base/SelectFileDialog.java b/ui/android/java/src/org/chromium/ui/base/SelectFileDialog.java new file mode 100644 index 0000000000000..bc6efb78f9c1f --- /dev/null +++ b/ui/android/java/src/org/chromium/ui/base/SelectFileDialog.java @@ -0,0 +1,371 @@ +// Copyright 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.ui.base; + +import android.annotation.TargetApi; +import android.app.Activity; +import android.content.ClipData; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Build; +import android.os.Environment; +import android.provider.MediaStore; +import android.text.TextUtils; +import android.util.Log; + +import org.chromium.base.CalledByNative; +import org.chromium.base.ContentUriUtils; +import org.chromium.base.JNINamespace; +import org.chromium.ui.R; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * A dialog that is triggered from a file input field that allows a user to select a file based on + * a set of accepted file types. The path of the selected file is passed to the native dialog. + */ +@JNINamespace("ui") +class SelectFileDialog implements WindowAndroid.IntentCallback { + private static final String TAG = "SelectFileDialog"; + private static final String IMAGE_TYPE = "image/"; + private static final String VIDEO_TYPE = "video/"; + private static final String AUDIO_TYPE = "audio/"; + private static final String ALL_IMAGE_TYPES = IMAGE_TYPE + "*"; + private static final String ALL_VIDEO_TYPES = VIDEO_TYPE + "*"; + private static final String ALL_AUDIO_TYPES = AUDIO_TYPE + "*"; + private static final String ANY_TYPES = "*/*"; + private static final String CAPTURE_IMAGE_DIRECTORY = "browser-photos"; + // Keep this variable in sync with the value defined in file_paths.xml. + private static final String IMAGE_FILE_PATH = "images"; + + private final long mNativeSelectFileDialog; + private List mFileTypes; + private boolean mCapture; + private Uri mCameraOutputUri; + + private SelectFileDialog(long nativeSelectFileDialog) { + mNativeSelectFileDialog = nativeSelectFileDialog; + } + + /** + * Creates and starts an intent based on the passed fileTypes and capture value. + * @param fileTypes MIME types requested (i.e. "image/*") + * @param capture The capture value as described in http://www.w3.org/TR/html-media-capture/ + * @param multiple Whether it should be possible to select multiple files. + * @param window The WindowAndroid that can show intents + */ + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2) + @CalledByNative + private void selectFile( + String[] fileTypes, boolean capture, boolean multiple, WindowAndroid window) { + mFileTypes = new ArrayList(Arrays.asList(fileTypes)); + mCapture = capture; + + Intent chooser = new Intent(Intent.ACTION_CHOOSER); + Intent camera = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); + Context context = window.getApplicationContext(); + camera.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | + Intent.FLAG_GRANT_WRITE_URI_PERMISSION); + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { + mCameraOutputUri = ContentUriUtils.getContentUriFromFile( + context, getFileForImageCapture(context)); + } else { + mCameraOutputUri = Uri.fromFile(getFileForImageCapture(context)); + } + } catch (IOException e) { + Log.e(TAG, "Cannot retrieve content uri from file", e); + } + + if (mCameraOutputUri == null) { + onFileNotSelected(); + return; + } + + camera.putExtra(MediaStore.EXTRA_OUTPUT, mCameraOutputUri); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { + camera.setClipData( + ClipData.newUri(context.getContentResolver(), + IMAGE_FILE_PATH, mCameraOutputUri)); + } + Intent camcorder = new Intent(MediaStore.ACTION_VIDEO_CAPTURE); + Intent soundRecorder = new Intent( + MediaStore.Audio.Media.RECORD_SOUND_ACTION); + + // Quick check - if the |capture| parameter is set and |fileTypes| has the appropriate MIME + // type, we should just launch the appropriate intent. Otherwise build up a chooser based on + // the accept type and then display that to the user. + if (captureCamera()) { + if (window.showIntent(camera, this, R.string.low_memory_error)) return; + } else if (captureCamcorder()) { + if (window.showIntent(camcorder, this, R.string.low_memory_error)) return; + } else if (captureMicrophone()) { + if (window.showIntent(soundRecorder, this, R.string.low_memory_error)) return; + } + + Intent getContentIntent = new Intent(Intent.ACTION_GET_CONTENT); + getContentIntent.addCategory(Intent.CATEGORY_OPENABLE); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2 && multiple) + getContentIntent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true); + + ArrayList extraIntents = new ArrayList(); + if (!noSpecificType()) { + // Create a chooser based on the accept type that was specified in the webpage. Note + // that if the web page specified multiple accept types, we will have built a generic + // chooser above. + if (shouldShowImageTypes()) { + extraIntents.add(camera); + getContentIntent.setType(ALL_IMAGE_TYPES); + } else if (shouldShowVideoTypes()) { + extraIntents.add(camcorder); + getContentIntent.setType(ALL_VIDEO_TYPES); + } else if (shouldShowAudioTypes()) { + extraIntents.add(soundRecorder); + getContentIntent.setType(ALL_AUDIO_TYPES); + } + } + + if (extraIntents.isEmpty()) { + // We couldn't resolve an accept type, so fallback to a generic chooser. + getContentIntent.setType(ANY_TYPES); + extraIntents.add(camera); + extraIntents.add(camcorder); + extraIntents.add(soundRecorder); + } + + chooser.putExtra(Intent.EXTRA_INITIAL_INTENTS, + extraIntents.toArray(new Intent[] { })); + + chooser.putExtra(Intent.EXTRA_INTENT, getContentIntent); + + if (!window.showIntent(chooser, this, R.string.low_memory_error)) { + onFileNotSelected(); + } + } + + /** + * Get a file for the image capture operation. For devices with JB MR2 or + * latter android versions, the file is put under IMAGE_FILE_PATH directory. + * For ICS devices, the file is put under CAPTURE_IMAGE_DIRECTORY. + * + * @param context The application context. + * @return file path for the captured image to be stored. + */ + private File getFileForImageCapture(Context context) throws IOException { + File path; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { + path = new File(context.getFilesDir(), IMAGE_FILE_PATH); + if (!path.exists() && !path.mkdir()) { + throw new IOException("Folder cannot be created."); + } + } else { + File externalDataDir = Environment.getExternalStoragePublicDirectory( + Environment.DIRECTORY_DCIM); + path = new File(externalDataDir.getAbsolutePath() + + File.separator + CAPTURE_IMAGE_DIRECTORY); + if (!path.exists() && !path.mkdirs()) { + path = externalDataDir; + } + } + File photoFile = File.createTempFile( + String.valueOf(System.currentTimeMillis()), ".jpg", path); + return photoFile; + } + + /** + * Callback method to handle the intent results and pass on the path to the native + * SelectFileDialog. + * @param window The window that has access to the application activity. + * @param resultCode The result code whether the intent returned successfully. + * @param contentResolver The content resolver used to extract the path of the selected file. + * @param results The results of the requested intent. + */ + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2) + @Override + public void onIntentCompleted(WindowAndroid window, int resultCode, + ContentResolver contentResolver, Intent results) { + if (resultCode != Activity.RESULT_OK) { + onFileNotSelected(); + return; + } + + if (results == null) { + // If we have a successful return but no data, then assume this is the camera returning + // the photo that we requested. + // If the uri is a file, we need to convert it to the absolute path or otherwise + // android cannot handle it correctly on some earlier versions. + // http://crbug.com/423338. + String path = ContentResolver.SCHEME_FILE.equals(mCameraOutputUri.getScheme()) ? + mCameraOutputUri.getPath() : mCameraOutputUri.toString(); + nativeOnFileSelected(mNativeSelectFileDialog, path, + mCameraOutputUri.getLastPathSegment()); + // Broadcast to the media scanner that there's a new photo on the device so it will + // show up right away in the gallery (rather than waiting until the next time the media + // scanner runs). + window.sendBroadcast(new Intent( + Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, mCameraOutputUri)); + return; + } + + // Path for when EXTRA_ALLOW_MULTIPLE Intent extra has been defined. Each of the selected + // files will be shared as an entry on the Intent's ClipData. This functionality is only + // available in Android JellyBean MR2 and higher. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2 && + results.getData() == null && + results.getClipData() != null) { + ClipData clipData = results.getClipData(); + + int itemCount = clipData.getItemCount(); + if (itemCount == 0) { + onFileNotSelected(); + return; + } + + Uri[] filePathArray = new Uri[itemCount]; + for (int i = 0; i < itemCount; ++i) { + filePathArray[i] = clipData.getItemAt(i).getUri(); + } + GetDisplayNameTask task = new GetDisplayNameTask(contentResolver, true); + task.execute(filePathArray); + return; + } + + if (ContentResolver.SCHEME_FILE.equals(results.getData().getScheme())) { + nativeOnFileSelected(mNativeSelectFileDialog, + results.getData().getSchemeSpecificPart(), ""); + return; + } + + if (ContentResolver.SCHEME_CONTENT.equals(results.getScheme())) { + GetDisplayNameTask task = new GetDisplayNameTask(contentResolver, false); + task.execute(results.getData()); + return; + } + + onFileNotSelected(); + window.showError(R.string.opening_file_error); + } + + private void onFileNotSelected() { + nativeOnFileNotSelected(mNativeSelectFileDialog); + } + + private boolean noSpecificType() { + // We use a single Intent to decide the type of the file chooser we display to the user, + // which means we can only give it a single type. If there are multiple accept types + // specified, we will fallback to a generic chooser (unless a capture parameter has been + // specified, in which case we'll try to satisfy that first. + return mFileTypes.size() != 1 || mFileTypes.contains(ANY_TYPES); + } + + private boolean shouldShowTypes(String allTypes, String specificType) { + if (noSpecificType() || mFileTypes.contains(allTypes)) return true; + return acceptSpecificType(specificType); + } + + private boolean shouldShowImageTypes() { + return shouldShowTypes(ALL_IMAGE_TYPES, IMAGE_TYPE); + } + + private boolean shouldShowVideoTypes() { + return shouldShowTypes(ALL_VIDEO_TYPES, VIDEO_TYPE); + } + + private boolean shouldShowAudioTypes() { + return shouldShowTypes(ALL_AUDIO_TYPES, AUDIO_TYPE); + } + + private boolean acceptsSpecificType(String type) { + return mFileTypes.size() == 1 && TextUtils.equals(mFileTypes.get(0), type); + } + + private boolean captureCamera() { + return mCapture && acceptsSpecificType(ALL_IMAGE_TYPES); + } + + private boolean captureCamcorder() { + return mCapture && acceptsSpecificType(ALL_VIDEO_TYPES); + } + + private boolean captureMicrophone() { + return mCapture && acceptsSpecificType(ALL_AUDIO_TYPES); + } + + private boolean acceptSpecificType(String accept) { + for (String type : mFileTypes) { + if (type.startsWith(accept)) { + return true; + } + } + return false; + } + + private class GetDisplayNameTask extends AsyncTask { + String[] mFilePaths; + final ContentResolver mContentResolver; + final boolean mIsMultiple; + + public GetDisplayNameTask(ContentResolver contentResolver, boolean isMultiple) { + mContentResolver = contentResolver; + mIsMultiple = isMultiple; + } + + @Override + protected String[] doInBackground(Uri...uris) { + mFilePaths = new String[uris.length]; + String[] displayNames = new String[uris.length]; + try { + for (int i = 0; i < uris.length; i++) { + mFilePaths[i] = uris[i].toString(); + displayNames[i] = ContentUriUtils.getDisplayName( + uris[i], mContentResolver, MediaStore.MediaColumns.DISPLAY_NAME); + } + } catch (SecurityException e) { + // Some third party apps will present themselves as being able + // to handle the ACTION_GET_CONTENT intent but then declare themselves + // as exported=false (or more often omit the exported keyword in + // the manifest which defaults to false after JB). + // In those cases trying to access the contents raises a security exception + // which we should not crash on. See crbug.com/382367 for details. + Log.w(TAG, "Unable to extract results from the content provider"); + return null; + } + + return displayNames; + } + + @Override + protected void onPostExecute(String[] result) { + if (result == null) { + onFileNotSelected(); + return; + } + if (mIsMultiple) { + nativeOnMultipleFilesSelected(mNativeSelectFileDialog, mFilePaths, result); + } else { + nativeOnFileSelected(mNativeSelectFileDialog, mFilePaths[0], result[0]); + } + } + } + + @CalledByNative + private static SelectFileDialog create(long nativeSelectFileDialog) { + return new SelectFileDialog(nativeSelectFileDialog); + } + + private native void nativeOnFileSelected(long nativeSelectFileDialogImpl, + String filePath, String displayName); + private native void nativeOnMultipleFilesSelected(long nativeSelectFileDialogImpl, + String[] filePathArray, String[] displayNameArray); + private native void nativeOnFileNotSelected(long nativeSelectFileDialogImpl); +} diff --git a/ui/android/java/src/org/chromium/ui/base/ViewAndroid.java b/ui/android/java/src/org/chromium/ui/base/ViewAndroid.java new file mode 100644 index 0000000000000..f33971369a54c --- /dev/null +++ b/ui/android/java/src/org/chromium/ui/base/ViewAndroid.java @@ -0,0 +1,86 @@ +// Copyright 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.ui.base; + +import android.view.View; + +import org.chromium.base.JNINamespace; + +/** + * From the Chromium architecture point of view, ViewAndroid and its native counterpart + * serve purpose of representing Android view where Chrome expects to have a cross platform + * handle to the system view type. As Views are Java object on Android, this ViewAndroid + * and its native counterpart provide the expected abstractions on the C++ side and allow + * it to be flexibly glued to an actual Android Java View at runtime. + * + * It should only be used where access to Android Views is needed from the C++ code. + */ +@JNINamespace("ui") +public class ViewAndroid { + // Native pointer to the c++ ViewAndroid object. + private long mNativeViewAndroid = 0; + private final ViewAndroidDelegate mViewAndroidDelegate; + private final WindowAndroid mWindowAndroid; + private int mKeepScreenOnCount; + private View mKeepScreenOnView; + + /** + * Constructs a View object. + */ + public ViewAndroid(WindowAndroid nativeWindow, ViewAndroidDelegate viewAndroidDelegate) { + mWindowAndroid = nativeWindow; + mViewAndroidDelegate = viewAndroidDelegate; + mNativeViewAndroid = nativeInit(mWindowAndroid.getNativePointer()); + } + + public ViewAndroidDelegate getViewAndroidDelegate() { + return mViewAndroidDelegate; + } + + /** + * Destroys the c++ ViewAndroid object if one has been created. + */ + public void destroy() { + if (mNativeViewAndroid != 0) { + nativeDestroy(mNativeViewAndroid); + mNativeViewAndroid = 0; + } + } + + /** + * Returns a pointer to the c++ AndroidWindow object. + * @return A pointer to the c++ AndroidWindow. + */ + public long getNativePointer() { + return mNativeViewAndroid; + } + + /** + * Set KeepScreenOn flag. If the flag already set, increase mKeepScreenOnCount. + */ + public void incrementKeepScreenOnCount() { + mKeepScreenOnCount++; + if (mKeepScreenOnCount == 1) { + mKeepScreenOnView = mViewAndroidDelegate.acquireAnchorView(); + mViewAndroidDelegate.setAnchorViewPosition(mKeepScreenOnView, 0, 0, 0, 0); + mKeepScreenOnView.setKeepScreenOn(true); + } + } + + /** + * Decrease mKeepScreenOnCount, if it is decreased to 0, remove the flag. + */ + public void decrementKeepScreenOnCount() { + assert mKeepScreenOnCount > 0; + mKeepScreenOnCount--; + if (mKeepScreenOnCount == 0) { + mViewAndroidDelegate.releaseAnchorView(mKeepScreenOnView); + mKeepScreenOnView = null; + } + } + + private native long nativeInit(long windowPtr); + private native void nativeDestroy(long nativeViewAndroid); +} diff --git a/ui/android/java/src/org/chromium/ui/base/ViewAndroidDelegate.java b/ui/android/java/src/org/chromium/ui/base/ViewAndroidDelegate.java new file mode 100644 index 0000000000000..6d3694cc9bfe1 --- /dev/null +++ b/ui/android/java/src/org/chromium/ui/base/ViewAndroidDelegate.java @@ -0,0 +1,34 @@ +// Copyright 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.ui.base; + +import android.view.View; + +/** + * Interface to acquire and release anchor views from the implementing View. + */ +public interface ViewAndroidDelegate { + + /** + * @return An anchor view that can be used to anchor decoration views like Autofill popup. + */ + View acquireAnchorView(); + + /** + * Set the anchor view to specified position and width (all units in dp). + * @param view The anchor view that needs to be positioned. + * @param x X coordinate of the top left corner of the anchor view. + * @param y Y coordinate of the top left corner of the anchor view. + * @param width The width of the anchor view. + * @param height The height of the anchor view. + */ + void setAnchorViewPosition(View view, float x, float y, float width, float height); + + /** + * Release given anchor view. + * @param anchorView The anchor view that needs to be released. + */ + void releaseAnchorView(View anchorView); +} diff --git a/ui/android/java/src/org/chromium/ui/base/WindowAndroid.java b/ui/android/java/src/org/chromium/ui/base/WindowAndroid.java new file mode 100644 index 0000000000000..8f1c58f67ffcf --- /dev/null +++ b/ui/android/java/src/org/chromium/ui/base/WindowAndroid.java @@ -0,0 +1,298 @@ +// Copyright 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.ui.base; + +import android.annotation.SuppressLint; +import android.app.Activity; +import android.app.PendingIntent; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.util.Log; +import android.util.SparseArray; +import android.widget.Toast; + +import org.chromium.base.CalledByNative; +import org.chromium.base.JNINamespace; +import org.chromium.ui.VSyncMonitor; + +import java.lang.ref.WeakReference; +import java.util.HashMap; + +/** + * The window base class that has the minimum functionality. + */ +@JNINamespace("ui") +public class WindowAndroid { + private static final String TAG = "WindowAndroid"; + + // Native pointer to the c++ WindowAndroid object. + private long mNativeWindowAndroid = 0; + private final VSyncMonitor mVSyncMonitor; + + // A string used as a key to store intent errors in a bundle + static final String WINDOW_CALLBACK_ERRORS = "window_callback_errors"; + + // Error code returned when an Intent fails to start an Activity. + public static final int START_INTENT_FAILURE = -1; + + protected Context mApplicationContext; + protected SparseArray mOutstandingIntents; + + // Ideally, this would be a SparseArray, but there's no easy way to store a + // SparseArray in a bundle during saveInstanceState(). So we use a HashMap and suppress + // the Android lint warning "UseSparseArrays". + protected HashMap mIntentErrors; + + private final VSyncMonitor.Listener mVSyncListener = new VSyncMonitor.Listener() { + @Override + public void onVSync(VSyncMonitor monitor, long vsyncTimeMicros) { + if (mNativeWindowAndroid != 0) { + nativeOnVSync(mNativeWindowAndroid, + vsyncTimeMicros, + mVSyncMonitor.getVSyncPeriodInMicroseconds()); + } + } + }; + + /** + * @return true if onVSync handler is executing. + * @see org.chromium.ui.VSyncMonitor#isInsideVSync(). + */ + public boolean isInsideVSync() { + return mVSyncMonitor.isInsideVSync(); + } + + /** + * @param context The application context. + */ + @SuppressLint("UseSparseArrays") + public WindowAndroid(Context context) { + assert context == context.getApplicationContext(); + mApplicationContext = context; + mOutstandingIntents = new SparseArray(); + mIntentErrors = new HashMap(); + mVSyncMonitor = new VSyncMonitor(context, mVSyncListener); + } + + /** + * Shows an intent and returns the results to the callback object. + * @param intent The PendingIntent that needs to be shown. + * @param callback The object that will receive the results for the intent. + * @param errorId The ID of error string to be show if activity is paused before intent + * results. + * @return Whether the intent was shown. + */ + public boolean showIntent(PendingIntent intent, IntentCallback callback, int errorId) { + return showCancelableIntent(intent, callback, errorId) >= 0; + } + + /** + * Shows an intent and returns the results to the callback object. + * @param intent The intent that needs to be shown. + * @param callback The object that will receive the results for the intent. + * @param errorId The ID of error string to be show if activity is paused before intent + * results. + * @return Whether the intent was shown. + */ + public boolean showIntent(Intent intent, IntentCallback callback, int errorId) { + return showCancelableIntent(intent, callback, errorId) >= 0; + } + + /** + * Shows an intent that could be canceled and returns the results to the callback object. + * @param intent The PendingIntent that needs to be shown. + * @param callback The object that will receive the results for the intent. + * @param errorId The ID of error string to be show if activity is paused before intent + * results. + * @return A non-negative request code that could be used for finishActivity, or + * START_INTENT_FAILURE if failed. + */ + public int showCancelableIntent(PendingIntent intent, IntentCallback callback, int errorId) { + Log.d(TAG, "Can't show intent as context is not an Activity: " + intent); + return START_INTENT_FAILURE; + } + + /** + * Shows an intent that could be canceled and returns the results to the callback object. + * @param intent The intent that needs to be showed. + * @param callback The object that will receive the results for the intent. + * @param errorId The ID of error string to be show if activity is paused before intent + * results. + * @return A non-negative request code that could be used for finishActivity, or + * START_INTENT_FAILURE if failed. + */ + public int showCancelableIntent(Intent intent, IntentCallback callback, int errorId) { + Log.d(TAG, "Can't show intent as context is not an Activity: " + intent); + return START_INTENT_FAILURE; + } + + /** + * Force finish another activity that you had previously started with showCancelableIntent. + * @param requestCode The request code returned from showCancelableIntent. + */ + public void cancelIntent(int requestCode) { + Log.d(TAG, "Can't cancel intent as context is not an Activity: " + requestCode); + } + + /** + * Removes a callback from the list of pending intents, so that nothing happens if/when the + * result for that intent is received. + * @param callback The object that should have received the results + * @return True if the callback was removed, false if it was not found. + */ + public boolean removeIntentCallback(IntentCallback callback) { + int requestCode = mOutstandingIntents.indexOfValue(callback); + if (requestCode < 0) return false; + mOutstandingIntents.remove(requestCode); + mIntentErrors.remove(requestCode); + return true; + } + + /** + * Displays an error message with a provided error message string. + * @param error The error message string to be displayed. + */ + public void showError(String error) { + if (error != null) { + Toast.makeText(mApplicationContext, error, Toast.LENGTH_SHORT).show(); + } + } + + /** + * Displays an error message from the given resource id. + * @param resId The error message string's resource id. + */ + public void showError(int resId) { + showError(mApplicationContext.getString(resId)); + } + + /** + * Displays an error message for a nonexistent callback. + * @param error The error message string to be displayed. + */ + protected void showCallbackNonExistentError(String error) { + showError(error); + } + + /** + * Broadcasts the given intent to all interested BroadcastReceivers. + */ + public void sendBroadcast(Intent intent) { + mApplicationContext.sendBroadcast(intent); + } + + /** + * @return A reference to owning Activity. The returned WeakReference will never be null, but + * the contained Activity can be null (either if it has been garbage collected or if + * this is in the context of a WebView that was not created using an Activity). + */ + public WeakReference getActivity() { + return new WeakReference(null); + } + + /** + * @return The application context for this activity. + */ + public Context getApplicationContext() { + return mApplicationContext; + } + + /** + * Saves the error messages that should be shown if any pending intents would return + * after the application has been put onPause. + * @param bundle The bundle to save the information in onPause + */ + public void saveInstanceState(Bundle bundle) { + bundle.putSerializable(WINDOW_CALLBACK_ERRORS, mIntentErrors); + } + + /** + * Restores the error messages that should be shown if any pending intents would return + * after the application has been put onPause. + * @param bundle The bundle to restore the information from onResume + */ + public void restoreInstanceState(Bundle bundle) { + if (bundle == null) return; + + Object errors = bundle.getSerializable(WINDOW_CALLBACK_ERRORS); + if (errors instanceof HashMap) { + @SuppressWarnings("unchecked") + HashMap intentErrors = (HashMap) errors; + mIntentErrors = intentErrors; + } + } + + /** + * Responds to the intent result if the intent was created by the native window. + * @param requestCode Request code of the requested intent. + * @param resultCode Result code of the requested intent. + * @param data The data returned by the intent. + * @return Boolean value of whether the intent was started by the native window. + */ + public boolean onActivityResult(int requestCode, int resultCode, Intent data) { + return false; + } + + @CalledByNative + private void requestVSyncUpdate() { + mVSyncMonitor.requestUpdate(); + } + + /** + * An interface that intent callback objects have to implement. + */ + public interface IntentCallback { + /** + * Handles the data returned by the requested intent. + * @param window A window reference. + * @param resultCode Result code of the requested intent. + * @param contentResolver An instance of ContentResolver class for accessing returned data. + * @param data The data returned by the intent. + */ + void onIntentCompleted(WindowAndroid window, int resultCode, + ContentResolver contentResolver, Intent data); + } + + /** + * Tests that an activity is available to handle the passed in intent. + * @param intent The intent to check. + * @return True if an activity is available to process this intent when started, meaning that + * Context.startActivity will not throw ActivityNotFoundException. + */ + public boolean canResolveActivity(Intent intent) { + return mApplicationContext.getPackageManager().resolveActivity(intent, 0) != null; + } + + /** + * Destroys the c++ WindowAndroid object if one has been created. + */ + public void destroy() { + if (mNativeWindowAndroid != 0) { + nativeDestroy(mNativeWindowAndroid); + mNativeWindowAndroid = 0; + } + } + + /** + * Returns a pointer to the c++ AndroidWindow object and calls the initializer if + * the object has not been previously initialized. + * @return A pointer to the c++ AndroidWindow. + */ + public long getNativePointer() { + if (mNativeWindowAndroid == 0) { + mNativeWindowAndroid = nativeInit(); + } + return mNativeWindowAndroid; + } + + private native long nativeInit(); + private native void nativeOnVSync(long nativeWindowAndroid, + long vsyncTimeMicros, + long vsyncPeriodMicros); + private native void nativeDestroy(long nativeWindowAndroid); + +} diff --git a/ui/android/java/src/org/chromium/ui/gfx/BitmapHelper.java b/ui/android/java/src/org/chromium/ui/gfx/BitmapHelper.java new file mode 100644 index 0000000000000..8ffaae4b09371 --- /dev/null +++ b/ui/android/java/src/org/chromium/ui/gfx/BitmapHelper.java @@ -0,0 +1,120 @@ +// Copyright 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.ui.gfx; + +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; + +import org.chromium.base.CalledByNative; +import org.chromium.base.JNINamespace; + +/** + * Helper class to decode and sample down bitmap resources. + */ +@JNINamespace("gfx") +public class BitmapHelper { + @CalledByNative + private static Bitmap createBitmap(int width, + int height, + int bitmapFormatValue) { + Bitmap.Config bitmapConfig = getBitmapConfigForFormat(bitmapFormatValue); + return Bitmap.createBitmap(width, height, bitmapConfig); + } + + /** + * Decode and sample down a bitmap resource to the requested width and height. + * + * @param name The resource name of the bitmap to decode. + * @param reqWidth The requested width of the resulting bitmap. + * @param reqHeight The requested height of the resulting bitmap. + * @return A bitmap sampled down from the original with the same aspect ratio and dimensions. + * that are equal to or greater than the requested width and height. + */ + @CalledByNative + private static Bitmap decodeDrawableResource(String name, + int reqWidth, + int reqHeight) { + Resources res = Resources.getSystem(); + int resId = res.getIdentifier(name, null, null); + if (resId == 0) return null; + + final BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + BitmapFactory.decodeResource(res, resId, options); + + options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight); + options.inJustDecodeBounds = false; + options.inPreferredConfig = Bitmap.Config.ARGB_8888; + return BitmapFactory.decodeResource(res, resId, options); + } + + // http://developer.android.com/training/displaying-bitmaps/load-bitmap.html + private static 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) { + + // Calculate ratios of height and width to requested height and width + final int heightRatio = Math.round((float) height / (float) reqHeight); + final int widthRatio = Math.round((float) width / (float) reqWidth); + + // Choose the smallest ratio as inSampleSize value, this will guarantee + // a final image with both dimensions larger than or equal to the + // requested height and width. + inSampleSize = heightRatio < widthRatio ? heightRatio : widthRatio; + } + + return inSampleSize; + } + + /** + * Provides a matching integer constant for the Bitmap.Config value passed. + * + * @param bitmapConfig The Bitmap Configuration value. + * @return Matching integer constant for the Bitmap.Config value passed. + */ + @CalledByNative + private static int getBitmapFormatForConfig(Bitmap.Config bitmapConfig) { + switch (bitmapConfig) { + case ALPHA_8: + return BitmapFormat.ALPHA_8; + case ARGB_4444: + return BitmapFormat.ARGB_4444; + case ARGB_8888: + return BitmapFormat.ARGB_8888; + case RGB_565: + return BitmapFormat.RGB_565; + default: + return BitmapFormat.NO_CONFIG; + } + } + + /** + * Provides a matching Bitmap.Config for the enum config value passed. + * + * @param bitmapFormatValue The Bitmap Configuration enum value. + * @return Matching Bitmap.Config for the enum value passed. + */ + private static Bitmap.Config getBitmapConfigForFormat(int bitmapFormatValue) { + switch (bitmapFormatValue) { + case BitmapFormat.ALPHA_8: + return Bitmap.Config.ALPHA_8; + case BitmapFormat.ARGB_4444: + return Bitmap.Config.ARGB_4444; + case BitmapFormat.RGB_565: + return Bitmap.Config.RGB_565; + case BitmapFormat.ARGB_8888: + default: + return Bitmap.Config.ARGB_8888; + } + } + +} diff --git a/ui/android/java/src/org/chromium/ui/gfx/DeviceDisplayInfo.java b/ui/android/java/src/org/chromium/ui/gfx/DeviceDisplayInfo.java new file mode 100644 index 0000000000000..38569513d08dc --- /dev/null +++ b/ui/android/java/src/org/chromium/ui/gfx/DeviceDisplayInfo.java @@ -0,0 +1,214 @@ +// Copyright 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.ui.gfx; + +import android.content.Context; +import android.graphics.PixelFormat; +import android.graphics.Point; +import android.os.Build; +import android.util.DisplayMetrics; +import android.view.Display; +import android.view.Surface; +import android.view.WindowManager; + +import org.chromium.base.CalledByNative; +import org.chromium.base.JNINamespace; + +/** + * This class facilitates access to android information typically only + * available using the Java SDK, including {@link Display} properties. + * + * Currently the information consists of very raw display information (height, width, DPI scale) + * regarding the main display. + */ +@JNINamespace("gfx") +public class DeviceDisplayInfo { + + private final Context mAppContext; + private final WindowManager mWinManager; + private Point mTempPoint = new Point(); + private DisplayMetrics mTempMetrics = new DisplayMetrics(); + + private DeviceDisplayInfo(Context context) { + mAppContext = context.getApplicationContext(); + mWinManager = (WindowManager) mAppContext.getSystemService(Context.WINDOW_SERVICE); + } + + /** + * @return Display height in physical pixels. + */ + @CalledByNative + public int getDisplayHeight() { + getDisplay().getSize(mTempPoint); + return mTempPoint.y; + } + + /** + * @return Display width in physical pixels. + */ + @CalledByNative + public int getDisplayWidth() { + getDisplay().getSize(mTempPoint); + return mTempPoint.x; + } + + /** + * @return Real physical display height in physical pixels. + */ + @CalledByNative + public int getPhysicalDisplayHeight() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1) { + return 0; + } + getDisplay().getRealSize(mTempPoint); + return mTempPoint.y; + } + + /** + * @return Real physical display width in physical pixels. + */ + @CalledByNative + public int getPhysicalDisplayWidth() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1) { + return 0; + } + getDisplay().getRealSize(mTempPoint); + return mTempPoint.x; + } + + @SuppressWarnings("deprecation") + private int getPixelFormat() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1) { + return getDisplay().getPixelFormat(); + } + // JellyBean MR1 and later always uses RGBA_8888. + return PixelFormat.RGBA_8888; + } + + /** + * @return Bits per pixel. + */ + @CalledByNative + public int getBitsPerPixel() { + int format = getPixelFormat(); + PixelFormat info = new PixelFormat(); + PixelFormat.getPixelFormatInfo(format, info); + return info.bitsPerPixel; + } + + /** + * @return Bits per component. + */ + @SuppressWarnings("deprecation") + @CalledByNative + public int getBitsPerComponent() { + int format = getPixelFormat(); + switch (format) { + case PixelFormat.RGBA_4444: + return 4; + + case PixelFormat.RGBA_5551: + return 5; + + case PixelFormat.RGBA_8888: + case PixelFormat.RGBX_8888: + case PixelFormat.RGB_888: + return 8; + + case PixelFormat.RGB_332: + return 2; + + case PixelFormat.RGB_565: + return 5; + + // Non-RGB formats. + case PixelFormat.A_8: + case PixelFormat.LA_88: + case PixelFormat.L_8: + return 0; + + // Unknown format. Use 8 as a sensible default. + default: + return 8; + } + } + + /** + * @return A scaling factor for the Density Independent Pixel unit. 1.0 is + * 160dpi, 0.75 is 120dpi, 2.0 is 320dpi. + */ + @CalledByNative + public double getDIPScale() { + getDisplay().getMetrics(mTempMetrics); + return mTempMetrics.density; + } + + /** + * @return Smallest screen size in density-independent pixels that the + * application will see, regardless of orientation. + */ + @CalledByNative + private int getSmallestDIPWidth() { + return mAppContext.getResources().getConfiguration().smallestScreenWidthDp; + } + + /** + * @return the screen's rotation angle from its 'natural' orientation. + * Expected values are one of { 0, 90, 180, 270 }. + * See http://developer.android.com/reference/android/view/Display.html#getRotation() + * for more information about Display.getRotation() behavior. + */ + @CalledByNative + public int getRotationDegrees() { + switch (getDisplay().getRotation()) { + case Surface.ROTATION_0: + return 0; + case Surface.ROTATION_90: + return 90; + case Surface.ROTATION_180: + return 180; + case Surface.ROTATION_270: + return 270; + } + + // This should not happen. + assert false; + return 0; + } + + /** + * Inform the native implementation to update its cached representation of + * the DeviceDisplayInfo values. + */ + public void updateNativeSharedDisplayInfo() { + nativeUpdateSharedDeviceDisplayInfo( + getDisplayHeight(), getDisplayWidth(), + getPhysicalDisplayHeight(), getPhysicalDisplayWidth(), + getBitsPerPixel(), getBitsPerComponent(), + getDIPScale(), getSmallestDIPWidth(), getRotationDegrees()); + } + + private Display getDisplay() { + return mWinManager.getDefaultDisplay(); + } + + /** + * Creates DeviceDisplayInfo for a given Context. + * + * @param context A context to use. + * @return DeviceDisplayInfo associated with a given Context. + */ + @CalledByNative + public static DeviceDisplayInfo create(Context context) { + return new DeviceDisplayInfo(context); + } + + private native void nativeUpdateSharedDeviceDisplayInfo( + int displayHeight, int displayWidth, + int physicalDisplayHeight, int physicalDisplayWidth, + int bitsPerPixel, int bitsPerComponent, double dipScale, + int smallestDIPWidth, int rotationDegrees); + +} diff --git a/ui/android/java/src/org/chromium/ui/gfx/OWNERS b/ui/android/java/src/org/chromium/ui/gfx/OWNERS new file mode 100644 index 0000000000000..316565b6ed378 --- /dev/null +++ b/ui/android/java/src/org/chromium/ui/gfx/OWNERS @@ -0,0 +1,3 @@ +aelias@chromium.org +jdduke@chromium.org +skyostil@chromium.org diff --git a/ui/android/java/src/org/chromium/ui/gfx/ViewConfigurationHelper.java b/ui/android/java/src/org/chromium/ui/gfx/ViewConfigurationHelper.java new file mode 100644 index 0000000000000..e8fc5b996d8de --- /dev/null +++ b/ui/android/java/src/org/chromium/ui/gfx/ViewConfigurationHelper.java @@ -0,0 +1,149 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.ui.gfx; + +import android.content.ComponentCallbacks; +import android.content.Context; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.util.TypedValue; +import android.view.ViewConfiguration; + +import org.chromium.base.CalledByNative; +import org.chromium.base.JNINamespace; +import org.chromium.ui.R; + +/** + * This class facilitates access to ViewConfiguration-related properties, also + * providing native-code notifications when such properties have changed. + * + */ +@JNINamespace("gfx") +public class ViewConfigurationHelper { + + // Fallback constants when resource lookup fails, see + // ui/android/java/res/values/dimens.xml. + private static final float MIN_SCALING_SPAN_MM = 27.0f; + private static final float MIN_SCALING_TOUCH_MAJOR_DIP = 48.0f; + + private final Context mAppContext; + private ViewConfiguration mViewConfiguration; + + private ViewConfigurationHelper(Context context) { + mAppContext = context.getApplicationContext(); + mViewConfiguration = ViewConfiguration.get(mAppContext); + } + + private void registerListener() { + mAppContext.registerComponentCallbacks( + new ComponentCallbacks() { + @Override + public void onConfigurationChanged(Configuration configuration) { + updateNativeViewConfigurationIfNecessary(); + } + + @Override + public void onLowMemory() { + } + }); + } + + private void updateNativeViewConfigurationIfNecessary() { + // The ViewConfiguration will differ only if the density has changed. + ViewConfiguration configuration = ViewConfiguration.get(mAppContext); + if (mViewConfiguration == configuration) return; + + mViewConfiguration = configuration; + nativeUpdateSharedViewConfiguration( + getScaledMaximumFlingVelocity(), + getScaledMinimumFlingVelocity(), + getScaledTouchSlop(), + getScaledDoubleTapSlop(), + getScaledMinScalingSpan(), + getScaledMinScalingTouchMajor()); + } + + @CalledByNative + private static int getDoubleTapTimeout() { + return ViewConfiguration.getDoubleTapTimeout(); + } + + @CalledByNative + private static int getLongPressTimeout() { + return ViewConfiguration.getLongPressTimeout(); + } + + @CalledByNative + private static int getTapTimeout() { + return ViewConfiguration.getTapTimeout(); + } + + @CalledByNative + private static float getScrollFriction() { + return ViewConfiguration.getScrollFriction(); + } + + @CalledByNative + private int getScaledMaximumFlingVelocity() { + return mViewConfiguration.getScaledMaximumFlingVelocity(); + } + + @CalledByNative + private int getScaledMinimumFlingVelocity() { + return mViewConfiguration.getScaledMinimumFlingVelocity(); + } + + @CalledByNative + private int getScaledTouchSlop() { + return mViewConfiguration.getScaledTouchSlop(); + } + + @CalledByNative + private int getScaledDoubleTapSlop() { + return mViewConfiguration.getScaledDoubleTapSlop(); + } + + @CalledByNative + private int getScaledMinScalingSpan() { + final Resources res = mAppContext.getResources(); + int id = res.getIdentifier("config_minScalingSpan", "dimen", "android"); + // Fall back to a sensible default if the internal identifier does not exist. + if (id == 0) id = R.dimen.config_min_scaling_span; + try { + return res.getDimensionPixelSize(id); + } catch (Resources.NotFoundException e) { + assert false : "MinScalingSpan resource lookup failed."; + return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_MM, MIN_SCALING_SPAN_MM, + res.getDisplayMetrics()); + } + } + + @CalledByNative + private int getScaledMinScalingTouchMajor() { + final Resources res = mAppContext.getResources(); + int id = res.getIdentifier("config_minScalingTouchMajor", "dimen", "android"); + // Fall back to a sensible default if the internal identifier does not exist. + if (id == 0) id = R.dimen.config_min_scaling_touch_major; + try { + return res.getDimensionPixelSize(id); + } catch (Resources.NotFoundException e) { + assert false : "MinScalingTouchMajor resource lookup failed."; + return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, + MIN_SCALING_TOUCH_MAJOR_DIP, res.getDisplayMetrics()); + } + } + + @CalledByNative + private static ViewConfigurationHelper createWithListener(Context context) { + ViewConfigurationHelper viewConfigurationHelper = new ViewConfigurationHelper(context); + viewConfigurationHelper.registerListener(); + return viewConfigurationHelper; + } + + private native void nativeUpdateSharedViewConfiguration( + int scaledMaximumFlingVelocity, int scaledMinimumFlingVelocity, + int scaledTouchSlop, int scaledDoubleTapSlop, + int scaledMinScalingSpan, int scaledMinScalingTouchMajor); +} diff --git a/ui/android/java/src/org/chromium/ui/gl/OWNERS b/ui/android/java/src/org/chromium/ui/gl/OWNERS new file mode 100644 index 0000000000000..3a065315bc09a --- /dev/null +++ b/ui/android/java/src/org/chromium/ui/gl/OWNERS @@ -0,0 +1,2 @@ +epenner@chromium.org +sievers@chromium.org diff --git a/ui/android/java/src/org/chromium/ui/gl/SurfaceTextureListener.java b/ui/android/java/src/org/chromium/ui/gl/SurfaceTextureListener.java new file mode 100644 index 0000000000000..bc57e1faca2e4 --- /dev/null +++ b/ui/android/java/src/org/chromium/ui/gl/SurfaceTextureListener.java @@ -0,0 +1,40 @@ +// Copyright 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.ui.gl; + +import android.graphics.SurfaceTexture; + +import org.chromium.base.JNINamespace; + +/** + * Listener to an android SurfaceTexture object for frame availability. + */ +@JNINamespace("gfx") +class SurfaceTextureListener implements SurfaceTexture.OnFrameAvailableListener { + // Used to determine the class instance to dispatch the native call to. + private final long mNativeSurfaceTextureListener; + + SurfaceTextureListener(long nativeSurfaceTextureListener) { + assert nativeSurfaceTextureListener != 0; + mNativeSurfaceTextureListener = nativeSurfaceTextureListener; + } + + @Override + public void onFrameAvailable(SurfaceTexture surfaceTexture) { + nativeFrameAvailable(mNativeSurfaceTextureListener); + } + + @Override + protected void finalize() throws Throwable { + try { + nativeDestroy(mNativeSurfaceTextureListener); + } finally { + super.finalize(); + } + } + + private native void nativeFrameAvailable(long nativeSurfaceTextureListener); + private native void nativeDestroy(long nativeSurfaceTextureListener); +} diff --git a/ui/android/java/src/org/chromium/ui/gl/SurfaceTexturePlatformWrapper.java b/ui/android/java/src/org/chromium/ui/gl/SurfaceTexturePlatformWrapper.java new file mode 100644 index 0000000000000..e785af6a108c3 --- /dev/null +++ b/ui/android/java/src/org/chromium/ui/gl/SurfaceTexturePlatformWrapper.java @@ -0,0 +1,78 @@ +// Copyright 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.ui.gl; + +import android.graphics.SurfaceTexture; +import android.os.Build; +import android.util.Log; + +import org.chromium.base.CalledByNative; +import org.chromium.base.JNINamespace; + +/** + * Wrapper class for the underlying platform's SurfaceTexture in order to + * provide a stable JNI API. + */ +@JNINamespace("gfx") +class SurfaceTexturePlatformWrapper { + + private static final String TAG = "SurfaceTexturePlatformWrapper"; + + @CalledByNative + private static SurfaceTexture create(int textureId) { + return new SurfaceTexture(textureId); + } + + @CalledByNative + private static SurfaceTexture createSingleBuffered(int textureId) { + assert Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT; + return new SurfaceTexture(textureId, true); + } + + @CalledByNative + private static void destroy(SurfaceTexture surfaceTexture) { + surfaceTexture.setOnFrameAvailableListener(null); + surfaceTexture.release(); + } + + @CalledByNative + private static void setFrameAvailableCallback(SurfaceTexture surfaceTexture, + long nativeSurfaceTextureListener) { + surfaceTexture.setOnFrameAvailableListener( + new SurfaceTextureListener(nativeSurfaceTextureListener)); + } + + @CalledByNative + private static void updateTexImage(SurfaceTexture surfaceTexture) { + try { + surfaceTexture.updateTexImage(); + } catch (RuntimeException e) { + Log.e(TAG, "Error calling updateTexImage", e); + } + } + + @CalledByNative + private static void releaseTexImage(SurfaceTexture surfaceTexture) { + assert Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT; + surfaceTexture.releaseTexImage(); + } + + @CalledByNative + private static void getTransformMatrix(SurfaceTexture surfaceTexture, float[] matrix) { + surfaceTexture.getTransformMatrix(matrix); + } + + @CalledByNative + private static void attachToGLContext(SurfaceTexture surfaceTexture, int texName) { + assert Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN; + surfaceTexture.attachToGLContext(texName); + } + + @CalledByNative + private static void detachFromGLContext(SurfaceTexture surfaceTexture) { + assert Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN; + surfaceTexture.detachFromGLContext(); + } +} diff --git a/ui/android/java/src/org/chromium/ui/interpolators/BakedBezierInterpolator.java b/ui/android/java/src/org/chromium/ui/interpolators/BakedBezierInterpolator.java new file mode 100644 index 0000000000000..bffcb4890f3ad --- /dev/null +++ b/ui/android/java/src/org/chromium/ui/interpolators/BakedBezierInterpolator.java @@ -0,0 +1,163 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.ui.interpolators; + +import android.view.animation.Interpolator; + +/** + * A pre-baked bezier-curved interpolator for quantum-paper transitions. + * TODO(dtrainor): Move to the API Compatability version iff that supports the curves we need and + * once we move to that SDK. + */ +public class BakedBezierInterpolator implements Interpolator { + /** + * Lookup table values. + * Generated using a Bezier curve from (0,0) to (1,1) with control points: + * P0 (0.0, 0.0) + * P1 (0.4, 0.0) + * P2 (0.2, 1.0) + * P3 (1.0, 1.0) + * + * Values sampled with x at regular intervals between 0 and 1. + */ + private static final float[] TRANSFORM_VALUES = new float[] { + 0.0f, 0.0002f, 0.0009f, 0.0019f, 0.0036f, 0.0059f, 0.0086f, 0.0119f, 0.0157f, 0.0209f, + 0.0257f, 0.0321f, 0.0392f, 0.0469f, 0.0566f, 0.0656f, 0.0768f, 0.0887f, 0.1033f, 0.1186f, + 0.1349f, 0.1519f, 0.1696f, 0.1928f, 0.2121f, 0.237f, 0.2627f, 0.2892f, 0.3109f, 0.3386f, + 0.3667f, 0.3952f, 0.4241f, 0.4474f, 0.4766f, 0.5f, 0.5234f, 0.5468f, 0.5701f, 0.5933f, + 0.6134f, 0.6333f, 0.6531f, 0.6698f, 0.6891f, 0.7054f, 0.7214f, 0.7346f, 0.7502f, 0.763f, + 0.7756f, 0.7879f, 0.8f, 0.8107f, 0.8212f, 0.8326f, 0.8415f, 0.8503f, 0.8588f, 0.8672f, + 0.8754f, 0.8833f, 0.8911f, 0.8977f, 0.9041f, 0.9113f, 0.9165f, 0.9232f, 0.9281f, 0.9328f, + 0.9382f, 0.9434f, 0.9476f, 0.9518f, 0.9557f, 0.9596f, 0.9632f, 0.9662f, 0.9695f, 0.9722f, + 0.9753f, 0.9777f, 0.9805f, 0.9826f, 0.9847f, 0.9866f, 0.9884f, 0.9901f, 0.9917f, 0.9931f, + 0.9944f, 0.9955f, 0.9964f, 0.9973f, 0.9981f, 0.9986f, 0.9992f, 0.9995f, 0.9998f, 1.0f, 1.0f + }; + + /** + * Lookup table values. + * Generated using a Bezier curve from (0,0) to (1,1) with control points: + * P0 (0.0, 0.0) + * P1 (0.4, 0.0) + * P2 (1.0, 1.0) + * P3 (1.0, 1.0) + * + * Values sampled with x at regular intervals between 0 and 1. + */ + private static final float[] FADE_OUT_VALUES = new float[] { + 0.0f, 0.0002f, 0.0008f, 0.0019f, 0.0032f, 0.0049f, 0.0069f, 0.0093f, 0.0119f, 0.0149f, + 0.0182f, 0.0218f, 0.0257f, 0.0299f, 0.0344f, 0.0392f, 0.0443f, 0.0496f, 0.0552f, 0.0603f, + 0.0656f, 0.0719f, 0.0785f, 0.0853f, 0.0923f, 0.0986f, 0.1051f, 0.1128f, 0.1206f, 0.1287f, + 0.1359f, 0.1433f, 0.1519f, 0.1607f, 0.1696f, 0.1776f, 0.1857f, 0.1952f, 0.2048f, 0.2145f, + 0.2232f, 0.2319f, 0.2421f, 0.2523f, 0.2627f, 0.2733f, 0.2826f, 0.2919f, 0.3027f, 0.3137f, + 0.3247f, 0.3358f, 0.3469f, 0.3582f, 0.3695f, 0.3809f, 0.3924f, 0.4039f, 0.4154f, 0.427f, + 0.4386f, 0.4503f, 0.4619f, 0.4751f, 0.4883f, 0.5f, 0.5117f, 0.5264f, 0.5381f, 0.5497f, + 0.5643f, 0.5759f, 0.5904f, 0.6033f, 0.6162f, 0.6305f, 0.6446f, 0.6587f, 0.6698f, 0.6836f, + 0.7f, 0.7134f, 0.7267f, 0.7425f, 0.7554f, 0.7706f, 0.7855f, 0.8f, 0.8143f, 0.8281f, 0.8438f, + 0.8588f, 0.8733f, 0.8892f, 0.9041f, 0.9215f, 0.9344f, 0.9518f, 0.9667f, 0.9826f, 0.9993f + }; + + /** + * Lookup table values. + * Generated using a Bezier curve from (0,0) to (1,1) with control points: + * P0 (0.0, 0.0) + * P1 (0.0, 0.0) + * P2 (0.2, 1.0) + * P3 (1.0, 1.0) + * + * Values sampled with x at regular intervals between 0 and 1. + */ + private static final float[] FADE_IN_VALUES = new float[] { + 0.0029f, 0.043f, 0.0785f, 0.1147f, 0.1476f, 0.1742f, 0.2024f, 0.2319f, 0.2575f, 0.2786f, + 0.3055f, 0.3274f, 0.3498f, 0.3695f, 0.3895f, 0.4096f, 0.4299f, 0.4474f, 0.4649f, 0.4824f, + 0.5f, 0.5176f, 0.5322f, 0.5468f, 0.5643f, 0.5788f, 0.5918f, 0.6048f, 0.6191f, 0.6333f, + 0.6446f, 0.6573f, 0.6698f, 0.6808f, 0.6918f, 0.704f, 0.7148f, 0.7254f, 0.7346f, 0.7451f, + 0.7554f, 0.7655f, 0.7731f, 0.783f, 0.7916f, 0.8f, 0.8084f, 0.8166f, 0.8235f, 0.8315f, + 0.8393f, 0.8459f, 0.8535f, 0.8599f, 0.8672f, 0.8733f, 0.8794f, 0.8853f, 0.8911f, 0.8967f, + 0.9023f, 0.9077f, 0.9121f, 0.9173f, 0.9224f, 0.9265f, 0.9313f, 0.9352f, 0.9397f, 0.9434f, + 0.9476f, 0.9511f, 0.9544f, 0.9577f, 0.9614f, 0.9644f, 0.9673f, 0.9701f, 0.9727f, 0.9753f, + 0.9777f, 0.98f, 0.9818f, 0.9839f, 0.9859f, 0.9877f, 0.9891f, 0.9907f, 0.9922f, 0.9933f, + 0.9946f, 0.9957f, 0.9966f, 0.9974f, 0.9981f, 0.9986f, 0.9992f, 0.9995f, 0.9998f, 1.0f, 1.0f + }; + + /** + * Lookup table values. + * Generated using a Bezier curve from (0,0) to (1,1) with control points: + * P0 (0.0, 0.0) + * P1 (0.0, 0.84) + * P2 (0.13, 0.99) + * P3 (1.0, 1.0) + */ + private static final float[] TRANSFORM_FOLLOW_THROUGH_VALUES = new float[] { + 0.0767f, 0.315f, 0.4173f, 0.484f, 0.5396f, 0.5801f, 0.6129f, 0.644f, 0.6687f, 0.6876f, + 0.7102f, 0.7276f, 0.7443f, 0.7583f, 0.7718f, 0.7849f, 0.7975f, 0.8079f, 0.8179f, 0.8276f, + 0.8355f, 0.8446f, 0.8519f, 0.859f, 0.8659f, 0.8726f, 0.8791f, 0.8841f, 0.8902f, 0.8949f, + 0.9001f, 0.9051f, 0.9094f, 0.9136f, 0.9177f, 0.9217f, 0.925f, 0.9283f, 0.9319f, 0.9355f, + 0.938f, 0.9413f, 0.9437f, 0.9469f, 0.9491f, 0.9517f, 0.9539f, 0.9563f, 0.9583f, 0.9603f, + 0.9622f, 0.9643f, 0.9661f, 0.9679f, 0.9693f, 0.9709f, 0.9725f, 0.974f, 0.9753f, 0.9767f, + 0.9779f, 0.9792f, 0.9803f, 0.9816f, 0.9826f, 0.9835f, 0.9845f, 0.9854f, 0.9863f, 0.9872f, + 0.988f, 0.9888f, 0.9895f, 0.9903f, 0.991f, 0.9917f, 0.9922f, 0.9928f, 0.9934f, 0.9939f, + 0.9944f, 0.9948f, 0.9953f, 0.9957f, 0.9962f, 0.9965f, 0.9969f, 0.9972f, 0.9975f, 0.9978f, + 0.9981f, 0.9984f, 0.9986f, 0.9989f, 0.9991f, 0.9992f, 0.9994f, 0.9996f, 0.9997f, 0.9999f, + 1.0f + }; + + /** + * 0.4 to 0.2 bezier curve. Should be used for general movement. + */ + public static final BakedBezierInterpolator TRANSFORM_CURVE = + new BakedBezierInterpolator(TRANSFORM_VALUES); + + /** + * 0.4 to 1.0 bezier curve. Should be used for fading out. + */ + public static final BakedBezierInterpolator FADE_OUT_CURVE = + new BakedBezierInterpolator(FADE_OUT_VALUES); + + /** + * 0.0 to 0.2 bezier curve. Should be used for fading in. + */ + public static final BakedBezierInterpolator FADE_IN_CURVE = + new BakedBezierInterpolator(FADE_IN_VALUES); + + /** + * 0.0 to 0.13 by 0.84 to 0.99 bezier curve. Should be used for very quick transforms. + */ + public static final BakedBezierInterpolator TRANSFORM_FOLLOW_THROUGH_CURVE = + new BakedBezierInterpolator(TRANSFORM_FOLLOW_THROUGH_VALUES); + + private final float[] mValues; + private final float mStepSize; + + /** + * Use the INSTANCE variable instead of instantiating. + */ + private BakedBezierInterpolator(float[] values) { + super(); + mValues = values; + mStepSize = 1.f / (mValues.length - 1); + } + + @Override + public float getInterpolation(float input) { + if (input >= 1.0f) { + return 1.0f; + } + + if (input <= 0f) { + return 0f; + } + + int position = Math.min( + (int) (input * (mValues.length - 1)), + mValues.length - 2); + + float quantized = position * mStepSize; + float difference = input - quantized; + float weight = difference / mStepSize; + + return mValues[position] + weight * (mValues[position + 1] - mValues[position]); + } + +} \ No newline at end of file diff --git a/ui/android/java/src/org/chromium/ui/picker/ChromeDatePickerDialog.java b/ui/android/java/src/org/chromium/ui/picker/ChromeDatePickerDialog.java new file mode 100644 index 0000000000000..f236ee161758c --- /dev/null +++ b/ui/android/java/src/org/chromium/ui/picker/ChromeDatePickerDialog.java @@ -0,0 +1,42 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.ui.picker; + +import android.content.Context; +import android.content.DialogInterface; +import android.widget.DatePicker; + +/** + * The behavior of the DatePickerDialog changed after JellyBean so it now calls + * OndateSetListener.onDateSet() even when the dialog is dismissed (e.g. back button, tap + * outside). This class will call the listener instead of the DatePickerDialog only when the + * BUTTON_POSITIVE has been clicked. + */ +class ChromeDatePickerDialog extends android.app.DatePickerDialog { + private final OnDateSetListener mCallBack; + + public ChromeDatePickerDialog(Context context, + OnDateSetListener callBack, + int year, + int monthOfYear, + int dayOfMonth) { + super(context, 0, callBack, year, monthOfYear, dayOfMonth); + mCallBack = callBack; + } + + /** + * The superclass DatePickerDialog has null for OnDateSetListener so we need to call the + * listener manually. + */ + @Override + public void onClick(DialogInterface dialog, int which) { + if (which == BUTTON_POSITIVE && mCallBack != null) { + DatePicker datePicker = getDatePicker(); + datePicker.clearFocus(); + mCallBack.onDateSet(datePicker, datePicker.getYear(), + datePicker.getMonth(), datePicker.getDayOfMonth()); + } + } +} diff --git a/ui/android/java/src/org/chromium/ui/picker/DateDialogNormalizer.java b/ui/android/java/src/org/chromium/ui/picker/DateDialogNormalizer.java new file mode 100644 index 0000000000000..25a7495638717 --- /dev/null +++ b/ui/android/java/src/org/chromium/ui/picker/DateDialogNormalizer.java @@ -0,0 +1,77 @@ +// Copyright 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.ui.picker; + +import android.widget.DatePicker; +import android.widget.DatePicker.OnDateChangedListener; + +import java.util.Calendar; +import java.util.TimeZone; + +/** + * Normalize a date dialog so that it respect min and max. + */ +public class DateDialogNormalizer { + + private static void setLimits(DatePicker picker, long minMillis, long maxMillis) { + // DatePicker intervals are non inclusive, the DatePicker will throw an + // exception when setting the min/max attribute to the current date + // so make sure this never happens + if (maxMillis <= minMillis) { + return; + } + Calendar minCal = trimToDate(minMillis); + Calendar maxCal = trimToDate(maxMillis); + int currentYear = picker.getYear(); + int currentMonth = picker.getMonth(); + int currentDayOfMonth = picker.getDayOfMonth(); + picker.updateDate(maxCal.get(Calendar.YEAR), + maxCal.get(Calendar.MONTH), + maxCal.get(Calendar.DAY_OF_MONTH)); + picker.setMinDate(minCal.getTimeInMillis()); + picker.updateDate(minCal.get(Calendar.YEAR), + minCal.get(Calendar.MONTH), + minCal.get(Calendar.DAY_OF_MONTH)); + picker.setMaxDate(maxCal.getTimeInMillis()); + + // Restore the current date, this will keep the min/max settings + // previously set into account. + picker.updateDate(currentYear, currentMonth, currentDayOfMonth); + } + + private static Calendar trimToDate(long time) { + Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("GMT")); + cal.clear(); + cal.setTimeInMillis(time); + Calendar result = Calendar.getInstance(TimeZone.getTimeZone("GMT")); + result.clear(); + result.set(cal.get(Calendar.YEAR), cal.get(Calendar.MONTH), cal.get(Calendar.DAY_OF_MONTH), + 0, 0, 0); + return result; + } + + /** + * Normalizes an existing DateDialogPicker changing the default date if + * needed to comply with the {@code min} and {@code max} attributes. + */ + public static void normalize(DatePicker picker, OnDateChangedListener listener, + int year, int month, int day, int hour, int minute, long minMillis, long maxMillis) { + Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("GMT")); + calendar.clear(); + calendar.set(year, month, day, hour, minute, 0); + if (calendar.getTimeInMillis() < minMillis) { + calendar.clear(); + calendar.setTimeInMillis(minMillis); + } else if (calendar.getTimeInMillis() > maxMillis) { + calendar.clear(); + calendar.setTimeInMillis(maxMillis); + } + picker.init( + calendar.get(Calendar.YEAR), calendar.get(Calendar.MONTH), + calendar.get(Calendar.DAY_OF_MONTH), listener); + + setLimits(picker, minMillis, maxMillis); + } +} diff --git a/ui/android/java/src/org/chromium/ui/picker/DateTimePickerDialog.java b/ui/android/java/src/org/chromium/ui/picker/DateTimePickerDialog.java new file mode 100644 index 0000000000000..c9dde077ecf8c --- /dev/null +++ b/ui/android/java/src/org/chromium/ui/picker/DateTimePickerDialog.java @@ -0,0 +1,147 @@ +// Copyright 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.ui.picker; + +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.DialogInterface.OnClickListener; +import android.text.format.Time; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.DatePicker; +import android.widget.DatePicker.OnDateChangedListener; +import android.widget.TimePicker; +import android.widget.TimePicker.OnTimeChangedListener; + +import org.chromium.ui.R; + +public class DateTimePickerDialog extends AlertDialog implements OnClickListener, + OnDateChangedListener, OnTimeChangedListener { + private final DatePicker mDatePicker; + private final TimePicker mTimePicker; + private final OnDateTimeSetListener mCallBack; + + private final long mMinTimeMillis; + private final long mMaxTimeMillis; + + /** + * The callback used to indicate the user is done filling in the date. + */ + public interface OnDateTimeSetListener { + + /** + * @param dateView The DatePicker view associated with this listener. + * @param timeView The TimePicker view associated with this listener. + * @param year The year that was set. + * @param monthOfYear The month that was set (0-11) for compatibility + * with {@link java.util.Calendar}. + * @param dayOfMonth The day of the month that was set. + * @param hourOfDay The hour that was set. + * @param minute The minute that was set. + */ + void onDateTimeSet(DatePicker dateView, TimePicker timeView, int year, int monthOfYear, + int dayOfMonth, int hourOfDay, int minute); + } + + /** + * @param context The context the dialog is to run in. + * @param callBack How the parent is notified that the date is set. + * @param year The initial year of the dialog. + * @param monthOfYear The initial month of the dialog. + * @param dayOfMonth The initial day of the dialog. + */ + public DateTimePickerDialog(Context context, + OnDateTimeSetListener callBack, + int year, + int monthOfYear, + int dayOfMonth, + int hourOfDay, int minute, boolean is24HourView, + double min, double max) { + super(context, 0); + + mMinTimeMillis = (long) min; + mMaxTimeMillis = (long) max; + + mCallBack = callBack; + + setButton(BUTTON_POSITIVE, context.getText( + R.string.date_picker_dialog_set), this); + setButton(BUTTON_NEGATIVE, context.getText(android.R.string.cancel), + (OnClickListener) null); + setIcon(0); + setTitle(context.getText(R.string.date_time_picker_dialog_title)); + + LayoutInflater inflater = + (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + View view = inflater.inflate(R.layout.date_time_picker_dialog, null); + setView(view); + mDatePicker = (DatePicker) view.findViewById(R.id.date_picker); + DateDialogNormalizer.normalize(mDatePicker, this, + year, monthOfYear, dayOfMonth, hourOfDay, minute, mMinTimeMillis, mMaxTimeMillis); + + mTimePicker = (TimePicker) view.findViewById(R.id.time_picker); + mTimePicker.setIs24HourView(is24HourView); + mTimePicker.setCurrentHour(hourOfDay); + mTimePicker.setCurrentMinute(minute); + mTimePicker.setOnTimeChangedListener(this); + onTimeChanged(mTimePicker, mTimePicker.getCurrentHour(), + mTimePicker.getCurrentMinute()); + } + + @Override + public void onClick(DialogInterface dialog, int which) { + tryNotifyDateTimeSet(); + } + + private void tryNotifyDateTimeSet() { + if (mCallBack != null) { + mDatePicker.clearFocus(); + mCallBack.onDateTimeSet(mDatePicker, mTimePicker, mDatePicker.getYear(), + mDatePicker.getMonth(), mDatePicker.getDayOfMonth(), + mTimePicker.getCurrentHour(), mTimePicker.getCurrentMinute()); + } + } + + @Override + public void onDateChanged(DatePicker view, int year, + int month, int day) { + // Signal a time change so the max/min checks can be applied. + if (mTimePicker != null) { + onTimeChanged(mTimePicker, mTimePicker.getCurrentHour(), + mTimePicker.getCurrentMinute()); + } + } + + @Override + public void onTimeChanged(TimePicker view, int hourOfDay, int minute) { + Time time = new Time(); + time.set(0, mTimePicker.getCurrentMinute(), + mTimePicker.getCurrentHour(), mDatePicker.getDayOfMonth(), + mDatePicker.getMonth(), mDatePicker.getYear()); + + if (time.toMillis(true) < mMinTimeMillis) { + time.set(mMinTimeMillis); + } else if (time.toMillis(true) > mMaxTimeMillis) { + time.set(mMaxTimeMillis); + } + mTimePicker.setCurrentHour(time.hour); + mTimePicker.setCurrentMinute(time.minute); + } + + /** + * Sets the current date. + * + * @param year The date year. + * @param monthOfYear The date month. + * @param dayOfMonth The date day of month. + */ + public void updateDateTime(int year, int monthOfYear, int dayOfMonth, + int hourOfDay, int minutOfHour) { + mDatePicker.updateDate(year, monthOfYear, dayOfMonth); + mTimePicker.setCurrentHour(hourOfDay); + mTimePicker.setCurrentMinute(minutOfHour); + } +} diff --git a/ui/android/java/src/org/chromium/ui/picker/DateTimeSuggestion.java b/ui/android/java/src/org/chromium/ui/picker/DateTimeSuggestion.java new file mode 100644 index 0000000000000..4c814681ed373 --- /dev/null +++ b/ui/android/java/src/org/chromium/ui/picker/DateTimeSuggestion.java @@ -0,0 +1,59 @@ +// Copyright 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.ui.picker; + +/** + * Date/time suggestion container used to store information for each suggestion that will be shown + * in the suggestion list dialog. Keep in sync with date_time_suggestion.h. + */ +public class DateTimeSuggestion { + private final double mValue; + private final String mLocalizedValue; + private final String mLabel; + + /** + * Constructs a color suggestion container. + * @param value The suggested date/time value. + * @param localizedValue The suggested value localized. + * @param label The label for the suggestion. + */ + public DateTimeSuggestion(double value, String localizedValue, String label) { + mValue = value; + mLocalizedValue = localizedValue; + mLabel = label; + } + + double value() { + return mValue; + } + + String localizedValue() { + return mLocalizedValue; + } + + String label() { + return mLabel; + } + + @Override + public boolean equals(Object object) { + if (!(object instanceof DateTimeSuggestion)) { + return false; + } + final DateTimeSuggestion other = (DateTimeSuggestion) object; + return mValue == other.mValue && + mLocalizedValue == other.mLocalizedValue && + mLabel == other.mLabel; + } + + @Override + public int hashCode() { + int hash = 31; + hash = 37 * hash + (int) mValue; + hash = 37 * hash + mLocalizedValue.hashCode(); + hash = 37 * hash + mLabel.hashCode(); + return hash; + } +} diff --git a/ui/android/java/src/org/chromium/ui/picker/DateTimeSuggestionListAdapter.java b/ui/android/java/src/org/chromium/ui/picker/DateTimeSuggestionListAdapter.java new file mode 100644 index 0000000000000..c7a166c9d626b --- /dev/null +++ b/ui/android/java/src/org/chromium/ui/picker/DateTimeSuggestionListAdapter.java @@ -0,0 +1,54 @@ +// Copyright 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.ui.picker; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.TextView; + +import org.chromium.ui.R; + +import java.util.List; + +/** + * Date/time suggestion adapter for the suggestion dialog. + */ +class DateTimeSuggestionListAdapter extends ArrayAdapter { + private final Context mContext; + + DateTimeSuggestionListAdapter(Context context, List objects) { + super(context, R.layout.date_time_suggestion, objects); + mContext = context; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + View layout = convertView; + if (convertView == null) { + LayoutInflater inflater = LayoutInflater.from(mContext); + layout = inflater.inflate(R.layout.date_time_suggestion, parent, false); + } + TextView labelView = (TextView) layout.findViewById(R.id.date_time_suggestion_value); + TextView sublabelView = (TextView) layout.findViewById(R.id.date_time_suggestion_label); + + if (position == getCount() - 1) { + labelView.setText(mContext.getText(R.string.date_picker_dialog_other_button_label)); + sublabelView.setText(""); + } else { + labelView.setText(getItem(position).localizedValue()); + sublabelView.setText(getItem(position).label()); + } + + return layout; + } + + @Override + public int getCount() { + return super.getCount() + 1; + } +} diff --git a/ui/android/java/src/org/chromium/ui/picker/InputDialogContainer.java b/ui/android/java/src/org/chromium/ui/picker/InputDialogContainer.java new file mode 100644 index 0000000000000..48304d546fab0 --- /dev/null +++ b/ui/android/java/src/org/chromium/ui/picker/InputDialogContainer.java @@ -0,0 +1,383 @@ +// Copyright 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.ui.picker; + +import android.app.AlertDialog; +import android.app.DatePickerDialog.OnDateSetListener; +import android.content.Context; +import android.content.DialogInterface; +import android.content.DialogInterface.OnDismissListener; +import android.text.format.DateFormat; +import android.view.View; +import android.widget.AdapterView; +import android.widget.DatePicker; +import android.widget.ListView; +import android.widget.TimePicker; + +import org.chromium.ui.R; +import org.chromium.ui.picker.DateTimePickerDialog.OnDateTimeSetListener; +import org.chromium.ui.picker.MultiFieldTimePickerDialog.OnMultiFieldTimeSetListener; + +import java.util.Arrays; +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.TimeZone; +import java.util.concurrent.TimeUnit; + +/** + * Opens the appropriate date/time picker dialog for the given dialog type. + */ +public class InputDialogContainer { + + public interface InputActionDelegate { + void cancelDateTimeDialog(); + void replaceDateTime(double value); + } + + private static int sTextInputTypeDate; + private static int sTextInputTypeDateTime; + private static int sTextInputTypeDateTimeLocal; + private static int sTextInputTypeMonth; + private static int sTextInputTypeTime; + private static int sTextInputTypeWeek; + + private final Context mContext; + + // Prevents sending two notifications (from onClick and from onDismiss) + private boolean mDialogAlreadyDismissed; + + private AlertDialog mDialog; + private final InputActionDelegate mInputActionDelegate; + + public static void initializeInputTypes(int textInputTypeDate, + int textInputTypeDateTime, int textInputTypeDateTimeLocal, + int textInputTypeMonth, int textInputTypeTime, + int textInputTypeWeek) { + sTextInputTypeDate = textInputTypeDate; + sTextInputTypeDateTime = textInputTypeDateTime; + sTextInputTypeDateTimeLocal = textInputTypeDateTimeLocal; + sTextInputTypeMonth = textInputTypeMonth; + sTextInputTypeTime = textInputTypeTime; + sTextInputTypeWeek = textInputTypeWeek; + } + + public static boolean isDialogInputType(int type) { + return type == sTextInputTypeDate || type == sTextInputTypeTime + || type == sTextInputTypeDateTime || type == sTextInputTypeDateTimeLocal + || type == sTextInputTypeMonth || type == sTextInputTypeWeek; + } + + public InputDialogContainer(Context context, InputActionDelegate inputActionDelegate) { + mContext = context; + mInputActionDelegate = inputActionDelegate; + } + + public void showPickerDialog(final int dialogType, double dialogValue, + double min, double max, double step) { + Calendar cal; + // |dialogValue|, |min|, |max| mean different things depending on the |dialogType|. + // For input type=month is the number of months since 1970. + // For input type=time it is milliseconds since midnight. + // For other types they are just milliseconds since 1970. + // If |dialogValue| is NaN it means an empty value. We will show the current time. + if (Double.isNaN(dialogValue)) { + cal = Calendar.getInstance(); + cal.set(Calendar.MILLISECOND, 0); + } else { + if (dialogType == sTextInputTypeMonth) { + cal = MonthPicker.createDateFromValue(dialogValue); + } else if (dialogType == sTextInputTypeWeek) { + cal = WeekPicker.createDateFromValue(dialogValue); + } else { + GregorianCalendar gregorianCalendar = + new GregorianCalendar(TimeZone.getTimeZone("UTC")); + // According to the HTML spec we only use the Gregorian calendar + // so we ignore the Julian/Gregorian transition. + gregorianCalendar.setGregorianChange(new Date(Long.MIN_VALUE)); + gregorianCalendar.setTimeInMillis((long) dialogValue); + cal = gregorianCalendar; + } + } + if (dialogType == sTextInputTypeDate) { + showPickerDialog(dialogType, + cal.get(Calendar.YEAR), + cal.get(Calendar.MONTH), + cal.get(Calendar.DAY_OF_MONTH), + 0, 0, 0, 0, 0, min, max, step); + } else if (dialogType == sTextInputTypeTime) { + showPickerDialog(dialogType, 0, 0, 0, + cal.get(Calendar.HOUR_OF_DAY), + cal.get(Calendar.MINUTE), + 0, 0, 0, min, max, step); + } else if (dialogType == sTextInputTypeDateTime || + dialogType == sTextInputTypeDateTimeLocal) { + showPickerDialog(dialogType, + cal.get(Calendar.YEAR), + cal.get(Calendar.MONTH), + cal.get(Calendar.DAY_OF_MONTH), + cal.get(Calendar.HOUR_OF_DAY), + cal.get(Calendar.MINUTE), + cal.get(Calendar.SECOND), + cal.get(Calendar.MILLISECOND), + 0, min, max, step); + } else if (dialogType == sTextInputTypeMonth) { + showPickerDialog(dialogType, cal.get(Calendar.YEAR), cal.get(Calendar.MONTH), 0, + 0, 0, 0, 0, 0, min, max, step); + } else if (dialogType == sTextInputTypeWeek) { + int year = WeekPicker.getISOWeekYearForDate(cal); + int week = WeekPicker.getWeekForDate(cal); + showPickerDialog(dialogType, year, 0, 0, 0, 0, 0, 0, week, min, max, step); + } + } + + void showSuggestionDialog(final int dialogType, + final double dialogValue, + final double min, final double max, final double step, + DateTimeSuggestion[] suggestions) { + ListView suggestionListView = new ListView(mContext); + final DateTimeSuggestionListAdapter adapter = + new DateTimeSuggestionListAdapter(mContext, Arrays.asList(suggestions)); + suggestionListView.setAdapter(adapter); + suggestionListView.setOnItemClickListener(new AdapterView.OnItemClickListener() { + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + if (position == adapter.getCount() - 1) { + dismissDialog(); + showPickerDialog(dialogType, dialogValue, min, max, step); + } else { + double suggestionValue = adapter.getItem(position).value(); + mInputActionDelegate.replaceDateTime(suggestionValue); + dismissDialog(); + mDialogAlreadyDismissed = true; + } + } + }); + + int dialogTitleId = R.string.date_picker_dialog_title; + if (dialogType == sTextInputTypeTime) { + dialogTitleId = R.string.time_picker_dialog_title; + } else if (dialogType == sTextInputTypeDateTime || + dialogType == sTextInputTypeDateTimeLocal) { + dialogTitleId = R.string.date_time_picker_dialog_title; + } else if (dialogType == sTextInputTypeMonth) { + dialogTitleId = R.string.month_picker_dialog_title; + } else if (dialogType == sTextInputTypeWeek) { + dialogTitleId = R.string.week_picker_dialog_title; + } + + mDialog = new AlertDialog.Builder(mContext) + .setTitle(dialogTitleId) + .setView(suggestionListView) + .setNegativeButton(mContext.getText(android.R.string.cancel), + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + dismissDialog(); + } + }) + .create(); + + mDialog.setOnDismissListener(new DialogInterface.OnDismissListener() { + @Override + public void onDismiss(DialogInterface dialog) { + if (mDialog == dialog && !mDialogAlreadyDismissed) { + mDialogAlreadyDismissed = true; + mInputActionDelegate.cancelDateTimeDialog(); + } + } + }); + mDialogAlreadyDismissed = false; + mDialog.show(); + } + + public void showDialog(final int type, final double value, + double min, double max, double step, + DateTimeSuggestion[] suggestions) { + // When the web page asks to show a dialog while there is one already open, + // dismiss the old one. + dismissDialog(); + if (suggestions == null) { + showPickerDialog(type, value, min, max, step); + } else { + showSuggestionDialog(type, value, min, max, step, suggestions); + } + } + + protected void showPickerDialog(final int dialogType, + int year, int month, int monthDay, + int hourOfDay, int minute, int second, int millis, int week, + double min, double max, double step) { + if (isDialogShowing()) mDialog.dismiss(); + + int stepTime = (int) step; + + if (dialogType == sTextInputTypeDate) { + ChromeDatePickerDialog dialog = new ChromeDatePickerDialog(mContext, + new DateListener(dialogType), + year, month, monthDay); + DateDialogNormalizer.normalize(dialog.getDatePicker(), dialog, + year, month, monthDay, + 0, 0, + (long) min, (long) max); + + dialog.setTitle(mContext.getText(R.string.date_picker_dialog_title)); + mDialog = dialog; + } else if (dialogType == sTextInputTypeTime) { + mDialog = new MultiFieldTimePickerDialog( + mContext, 0 /* theme */ , + hourOfDay, minute, second, millis, + (int) min, (int) max, stepTime, + DateFormat.is24HourFormat(mContext), + new FullTimeListener(dialogType)); + } else if (dialogType == sTextInputTypeDateTime || + dialogType == sTextInputTypeDateTimeLocal) { + mDialog = new DateTimePickerDialog(mContext, + new DateTimeListener(dialogType), + year, month, monthDay, + hourOfDay, minute, + DateFormat.is24HourFormat(mContext), min, max); + } else if (dialogType == sTextInputTypeMonth) { + mDialog = new MonthPickerDialog(mContext, new MonthOrWeekListener(dialogType), + year, month, min, max); + } else if (dialogType == sTextInputTypeWeek) { + mDialog = new WeekPickerDialog(mContext, new MonthOrWeekListener(dialogType), + year, week, min, max); + } + + mDialog.setButton(DialogInterface.BUTTON_POSITIVE, + mContext.getText(R.string.date_picker_dialog_set), + (DialogInterface.OnClickListener) mDialog); + + mDialog.setButton(DialogInterface.BUTTON_NEGATIVE, + mContext.getText(android.R.string.cancel), + (DialogInterface.OnClickListener) null); + + mDialog.setButton(DialogInterface.BUTTON_NEUTRAL, + mContext.getText(R.string.date_picker_dialog_clear), + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + mDialogAlreadyDismissed = true; + mInputActionDelegate.replaceDateTime(Double.NaN); + } + }); + + mDialog.setOnDismissListener( + new OnDismissListener() { + @Override + public void onDismiss(final DialogInterface dialog) { + if (!mDialogAlreadyDismissed) { + mDialogAlreadyDismissed = true; + mInputActionDelegate.cancelDateTimeDialog(); + } + } + }); + + mDialogAlreadyDismissed = false; + mDialog.show(); + } + + boolean isDialogShowing() { + return mDialog != null && mDialog.isShowing(); + } + + void dismissDialog() { + if (isDialogShowing()) mDialog.dismiss(); + } + + private class DateListener implements OnDateSetListener { + private final int mDialogType; + + DateListener(int dialogType) { + mDialogType = dialogType; + } + + @Override + public void onDateSet(DatePicker view, int year, int month, int monthDay) { + setFieldDateTimeValue(mDialogType, year, month, monthDay, 0, 0, 0, 0, 0); + } + } + + private class FullTimeListener implements OnMultiFieldTimeSetListener { + private final int mDialogType; + FullTimeListener(int dialogType) { + mDialogType = dialogType; + } + + @Override + public void onTimeSet(int hourOfDay, int minute, int second, int milli) { + setFieldDateTimeValue(mDialogType, 0, 0, 0, hourOfDay, minute, second, milli, 0); + } + } + + private class DateTimeListener implements OnDateTimeSetListener { + private final boolean mLocal; + private final int mDialogType; + + public DateTimeListener(int dialogType) { + mLocal = dialogType == sTextInputTypeDateTimeLocal; + mDialogType = dialogType; + } + + @Override + public void onDateTimeSet(DatePicker dateView, TimePicker timeView, + int year, int month, int monthDay, + int hourOfDay, int minute) { + setFieldDateTimeValue(mDialogType, year, month, monthDay, hourOfDay, minute, 0, 0, 0); + } + } + + private class MonthOrWeekListener implements TwoFieldDatePickerDialog.OnValueSetListener { + private final int mDialogType; + + MonthOrWeekListener(int dialogType) { + mDialogType = dialogType; + } + + @Override + public void onValueSet(int year, int positionInYear) { + if (mDialogType == sTextInputTypeMonth) { + setFieldDateTimeValue(mDialogType, year, positionInYear, 0, 0, 0, 0, 0, 0); + } else { + setFieldDateTimeValue(mDialogType, year, 0, 0, 0, 0, 0, 0, positionInYear); + } + } + } + + protected void setFieldDateTimeValue(int dialogType, + int year, int month, int monthDay, + int hourOfDay, int minute, int second, int millis, + int week) { + // Prevents more than one callback being sent to the native + // side when the dialog triggers multiple events. + if (mDialogAlreadyDismissed) + return; + mDialogAlreadyDismissed = true; + + if (dialogType == sTextInputTypeMonth) { + mInputActionDelegate.replaceDateTime((year - 1970) * 12 + month); + } else if (dialogType == sTextInputTypeWeek) { + mInputActionDelegate.replaceDateTime( + WeekPicker.createDateFromWeek(year, week).getTimeInMillis()); + } else if (dialogType == sTextInputTypeTime) { + mInputActionDelegate.replaceDateTime(TimeUnit.HOURS.toMillis(hourOfDay) + + TimeUnit.MINUTES.toMillis(minute) + + TimeUnit.SECONDS.toMillis(second) + + millis); + } else { + Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("UTC")); + cal.clear(); + cal.set(Calendar.YEAR, year); + cal.set(Calendar.MONTH, month); + cal.set(Calendar.DAY_OF_MONTH, monthDay); + cal.set(Calendar.HOUR_OF_DAY, hourOfDay); + cal.set(Calendar.MINUTE, minute); + cal.set(Calendar.SECOND, second); + cal.set(Calendar.MILLISECOND, millis); + mInputActionDelegate.replaceDateTime(cal.getTimeInMillis()); + } + } +} diff --git a/ui/android/java/src/org/chromium/ui/picker/MonthPicker.java b/ui/android/java/src/org/chromium/ui/picker/MonthPicker.java new file mode 100644 index 0000000000000..416217c0dc287 --- /dev/null +++ b/ui/android/java/src/org/chromium/ui/picker/MonthPicker.java @@ -0,0 +1,117 @@ +// Copyright 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.ui.picker; + +import android.content.Context; + +import org.chromium.ui.R; + +import java.text.DateFormatSymbols; +import java.util.Arrays; +import java.util.Calendar; +import java.util.Locale; +import java.util.TimeZone; + +public class MonthPicker extends TwoFieldDatePicker { + private static final int MONTHS_NUMBER = 12; + + private final String[] mShortMonths; + + public MonthPicker(Context context, double minValue, double maxValue) { + super(context, minValue, maxValue); + + getPositionInYearSpinner().setContentDescription( + getResources().getString(R.string.accessibility_date_picker_month)); + + // initialization based on locale + mShortMonths = + DateFormatSymbols.getInstance(Locale.getDefault()).getShortMonths(); + + // initialize to current date + Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("UTC")); + init(cal.get(Calendar.YEAR), cal.get(Calendar.MONTH), null); + } + + /** + * Creates a date object from the |value| which is months since epoch. + */ + public static Calendar createDateFromValue(double value) { + int year = (int) Math.min(value / 12 + 1970, Integer.MAX_VALUE); + int month = (int) (value % 12); + Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("UTC")); + cal.clear(); + cal.set(year, month, 1); + return cal; + } + + @Override + protected Calendar getDateForValue(double value) { + return MonthPicker.createDateFromValue(value); + } + + @Override + protected void setCurrentDate(int year, int month) { + Calendar date = Calendar.getInstance(TimeZone.getTimeZone("UTC")); + date.set(year, month, 1); + if (date.before(getMinDate())) { + setCurrentDate(getMinDate()); + } else if (date.after(getMaxDate())) { + setCurrentDate(getMaxDate()); + } else { + setCurrentDate(date); + } + } + + @Override + protected void updateSpinners() { + super.updateSpinners(); + + // make sure the month names are a zero based array + // with the months in the month spinner + String[] displayedValues = Arrays.copyOfRange(mShortMonths, + getPositionInYearSpinner().getMinValue(), + getPositionInYearSpinner().getMaxValue() + 1); + getPositionInYearSpinner().setDisplayedValues(displayedValues); + } + + /** + * @return The selected month. + */ + public int getMonth() { + return getCurrentDate().get(Calendar.MONTH); + } + + @Override + public int getPositionInYear() { + return getMonth(); + } + + @Override + protected int getMaxYear() { + return getMaxDate().get(Calendar.YEAR); + } + + @Override + protected int getMinYear() { + return getMinDate().get(Calendar.YEAR); + } + + + @Override + protected int getMaxPositionInYear(int year) { + if (year == getMaxDate().get(Calendar.YEAR)) { + return getMaxDate().get(Calendar.MONTH); + } + return MONTHS_NUMBER - 1; + } + + @Override + protected int getMinPositionInYear(int year) { + if (year == getMinDate().get(Calendar.YEAR)) { + return getMinDate().get(Calendar.MONTH); + } + return 0; + } +} diff --git a/ui/android/java/src/org/chromium/ui/picker/MonthPickerDialog.java b/ui/android/java/src/org/chromium/ui/picker/MonthPickerDialog.java new file mode 100644 index 0000000000000..58aafa67d1051 --- /dev/null +++ b/ui/android/java/src/org/chromium/ui/picker/MonthPickerDialog.java @@ -0,0 +1,38 @@ +// Copyright 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.ui.picker; + +import android.content.Context; + +import org.chromium.ui.R; + +public class MonthPickerDialog extends TwoFieldDatePickerDialog { + + /** + * @param context The context the dialog is to run in. + * @param callBack How the parent is notified that the date is set. + * @param year The initial year of the dialog. + * @param monthOfYear The initial month of the dialog. + */ + public MonthPickerDialog(Context context, OnValueSetListener callBack, + int year, int monthOfYear, double minMonth, double maxMonth) { + super(context, callBack, year, monthOfYear, minMonth, maxMonth); + setTitle(R.string.month_picker_dialog_title); + } + + @Override + protected TwoFieldDatePicker createPicker(Context context, double minValue, double maxValue) { + return new MonthPicker(context, minValue, maxValue); + } + + /** + * Gets the {@link MonthPicker} contained in this dialog. + * + * @return The calendar view. + */ + public MonthPicker getMonthPicker() { + return (MonthPicker) mPicker; + } +} diff --git a/ui/android/java/src/org/chromium/ui/picker/MultiFieldTimePickerDialog.java b/ui/android/java/src/org/chromium/ui/picker/MultiFieldTimePickerDialog.java new file mode 100644 index 0000000000000..d03c16509f87a --- /dev/null +++ b/ui/android/java/src/org/chromium/ui/picker/MultiFieldTimePickerDialog.java @@ -0,0 +1,273 @@ +// Copyright 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.ui.picker; + +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.DialogInterface.OnClickListener; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.NumberPicker; + +import org.chromium.ui.R; + +import java.util.ArrayList; + +/** + * A time picker dialog with upto 5 number pickers left to right: + * hour, minute, second, milli, AM/PM. + * + * If is24hourFormat is true then AM/PM picker is not displayed and + * hour range is 0..23. Otherwise hour range is 1..12. + * The milli picker is not displayed if step >= SECOND_IN_MILLIS + * The second picker is not displayed if step >= MINUTE_IN_MILLIS. + */ +public class MultiFieldTimePickerDialog + extends AlertDialog implements OnClickListener { + + private final NumberPicker mHourSpinner; + private final NumberPicker mMinuteSpinner; + private final NumberPicker mSecSpinner; + private final NumberPicker mMilliSpinner; + private final NumberPicker mAmPmSpinner; + private final OnMultiFieldTimeSetListener mListener; + private final int mStep; + private final int mBaseMilli; + private final boolean mIs24hourFormat; + + public interface OnMultiFieldTimeSetListener { + void onTimeSet(int hourOfDay, int minute, int second, int milli); + } + + private static final int SECOND_IN_MILLIS = 1000; + private static final int MINUTE_IN_MILLIS = 60 * SECOND_IN_MILLIS; + private static final int HOUR_IN_MILLIS = 60 * MINUTE_IN_MILLIS; + + public MultiFieldTimePickerDialog( + Context context, + int theme, + int hour, int minute, int second, int milli, + int min, int max, int step, boolean is24hourFormat, + OnMultiFieldTimeSetListener listener) { + super(context, theme); + mListener = listener; + mStep = step; + mIs24hourFormat = is24hourFormat; + + if (min >= max) { + min = 0; + max = 24 * HOUR_IN_MILLIS - 1; + } + if (step < 0 || step >= 24 * HOUR_IN_MILLIS) { + step = MINUTE_IN_MILLIS; + } + + LayoutInflater inflater = + (LayoutInflater) context.getSystemService( + Context.LAYOUT_INFLATER_SERVICE); + View view = inflater.inflate(R.layout.multi_field_time_picker_dialog, null); + setView(view); + + mHourSpinner = (NumberPicker) view.findViewById(R.id.hour); + mMinuteSpinner = (NumberPicker) view.findViewById(R.id.minute); + mSecSpinner = (NumberPicker) view.findViewById(R.id.second); + mMilliSpinner = (NumberPicker) view.findViewById(R.id.milli); + mAmPmSpinner = (NumberPicker) view.findViewById(R.id.ampm); + + int minHour = min / HOUR_IN_MILLIS; + int maxHour = max / HOUR_IN_MILLIS; + min -= minHour * HOUR_IN_MILLIS; + max -= maxHour * HOUR_IN_MILLIS; + + if (minHour == maxHour) { + mHourSpinner.setEnabled(false); + hour = minHour; + } + + if (is24hourFormat) { + mAmPmSpinner.setVisibility(View.GONE); + } else { + int minAmPm = minHour / 12; + int maxAmPm = maxHour / 12; + int amPm = hour / 12; + mAmPmSpinner.setMinValue(minAmPm); + mAmPmSpinner.setMaxValue(maxAmPm); + mAmPmSpinner.setDisplayedValues(new String[] { + context.getString(R.string.time_picker_dialog_am), + context.getString(R.string.time_picker_dialog_pm) + }); + + hour %= 12; + if (hour == 0) { + hour = 12; + } + if (minAmPm == maxAmPm) { + mAmPmSpinner.setEnabled(false); + amPm = minAmPm; + + minHour %= 12; + maxHour %= 12; + if (minHour == 0 && maxHour == 0) { + minHour = 12; + maxHour = 12; + } else if (minHour == 0) { + minHour = maxHour; + maxHour = 12; + } else if (maxHour == 0) { + maxHour = 12; + } + } else { + minHour = 1; + maxHour = 12; + } + mAmPmSpinner.setValue(amPm); + } + + if (minHour == maxHour) { + mHourSpinner.setEnabled(false); + } + mHourSpinner.setMinValue(minHour); + mHourSpinner.setMaxValue(maxHour); + mHourSpinner.setValue(hour); + + NumberFormatter twoDigitPaddingFormatter = new NumberFormatter("%02d"); + + int minMinute = min / MINUTE_IN_MILLIS; + int maxMinute = max / MINUTE_IN_MILLIS; + min -= minMinute * MINUTE_IN_MILLIS; + max -= maxMinute * MINUTE_IN_MILLIS; + + if (minHour == maxHour) { + mMinuteSpinner.setMinValue(minMinute); + mMinuteSpinner.setMaxValue(maxMinute); + if (minMinute == maxMinute) { + // Set this otherwise the box is empty until you stroke it. + mMinuteSpinner.setDisplayedValues( + new String[] { twoDigitPaddingFormatter.format(minMinute) }); + mMinuteSpinner.setEnabled(false); + minute = minMinute; + } + } else { + mMinuteSpinner.setMinValue(0); + mMinuteSpinner.setMaxValue(59); + } + + if (step >= HOUR_IN_MILLIS) { + mMinuteSpinner.setEnabled(false); + } + + mMinuteSpinner.setValue(minute); + mMinuteSpinner.setFormatter(twoDigitPaddingFormatter); + + if (step >= MINUTE_IN_MILLIS) { + // Remove the ':' in front of the second spinner as well. + view.findViewById(R.id.second_colon).setVisibility(View.GONE); + mSecSpinner.setVisibility(View.GONE); + } + + int minSecond = min / SECOND_IN_MILLIS; + int maxSecond = max / SECOND_IN_MILLIS; + min -= minSecond * SECOND_IN_MILLIS; + max -= maxSecond * SECOND_IN_MILLIS; + + if (minHour == maxHour && minMinute == maxMinute) { + mSecSpinner.setMinValue(minSecond); + mSecSpinner.setMaxValue(maxSecond); + if (minSecond == maxSecond) { + // Set this otherwise the box is empty until you stroke it. + mSecSpinner.setDisplayedValues( + new String[] { twoDigitPaddingFormatter.format(minSecond) }); + mSecSpinner.setEnabled(false); + second = minSecond; + } + } else { + mSecSpinner.setMinValue(0); + mSecSpinner.setMaxValue(59); + } + + mSecSpinner.setValue(second); + mSecSpinner.setFormatter(twoDigitPaddingFormatter); + + if (step >= SECOND_IN_MILLIS) { + // Remove the '.' in front of the milli spinner as well. + view.findViewById(R.id.second_dot).setVisibility(View.GONE); + mMilliSpinner.setVisibility(View.GONE); + } + + // Round to the nearest step. + milli = ((milli + step / 2) / step) * step; + if (step == 1 || step == 10 || step == 100) { + if (minHour == maxHour && minMinute == maxMinute && minSecond == maxSecond) { + mMilliSpinner.setMinValue(min / step); + mMilliSpinner.setMaxValue(max / step); + + if (min == max) { + mMilliSpinner.setEnabled(false); + milli = min; + } + } else { + mMilliSpinner.setMinValue(0); + mMilliSpinner.setMaxValue(999 / step); + } + + if (step == 1) { + mMilliSpinner.setFormatter(new NumberFormatter("%03d")); + } else if (step == 10) { + mMilliSpinner.setFormatter(new NumberFormatter("%02d")); + } else if (step == 100) { + mMilliSpinner.setFormatter(new NumberFormatter("%d")); + } + mMilliSpinner.setValue(milli / step); + mBaseMilli = 0; + } else if (step < SECOND_IN_MILLIS) { + // Non-decimal step value. + ArrayList strValue = new ArrayList(); + for (int i = min; i < max; i += step) { + strValue.add(String.format("%03d", i)); + } + mMilliSpinner.setMinValue(0); + mMilliSpinner.setMaxValue(strValue.size() - 1); + mMilliSpinner.setValue((milli - min) / step); + mMilliSpinner.setDisplayedValues(strValue.toArray(new String[strValue.size()])); + mBaseMilli = min; + } else { + mBaseMilli = 0; + } + } + + @Override + public void onClick(DialogInterface dialog, int which) { + notifyDateSet(); + } + + private void notifyDateSet() { + int hour = mHourSpinner.getValue(); + int minute = mMinuteSpinner.getValue(); + int sec = mSecSpinner.getValue(); + int milli = mMilliSpinner.getValue() * mStep + mBaseMilli; + if (!mIs24hourFormat) { + int ampm = mAmPmSpinner.getValue(); + if (hour == 12) { + hour = 0; + } + hour += ampm * 12; + } + mListener.onTimeSet(hour, minute, sec, milli); + } + + private static class NumberFormatter implements NumberPicker.Formatter { + private final String mFormat; + + NumberFormatter(String format) { + mFormat = format; + } + + @Override + public String format(int value) { + return String.format(mFormat, value); + } + } +} diff --git a/ui/android/java/src/org/chromium/ui/picker/OWNERS b/ui/android/java/src/org/chromium/ui/picker/OWNERS new file mode 100644 index 0000000000000..a4238acc2e4c4 --- /dev/null +++ b/ui/android/java/src/org/chromium/ui/picker/OWNERS @@ -0,0 +1,2 @@ +aurimas@chromium.org +miguelg@chromium.org diff --git a/ui/android/java/src/org/chromium/ui/picker/TwoFieldDatePicker.java b/ui/android/java/src/org/chromium/ui/picker/TwoFieldDatePicker.java new file mode 100644 index 0000000000000..4e84683a32c24 --- /dev/null +++ b/ui/android/java/src/org/chromium/ui/picker/TwoFieldDatePicker.java @@ -0,0 +1,250 @@ +// Copyright 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.ui.picker; + +import android.content.Context; +import android.text.format.DateUtils; +import android.view.LayoutInflater; +import android.view.accessibility.AccessibilityEvent; +import android.widget.FrameLayout; +import android.widget.NumberPicker; +import android.widget.NumberPicker.OnValueChangeListener; + +import org.chromium.ui.R; + +import java.util.Calendar; +import java.util.TimeZone; + +/** + * This class is heavily based on android.widget.DatePicker. + */ +public abstract class TwoFieldDatePicker extends FrameLayout { + + private final NumberPicker mPositionInYearSpinner; + + private final NumberPicker mYearSpinner; + + private OnMonthOrWeekChangedListener mMonthOrWeekChangedListener; + + // It'd be nice to use android.text.Time like in other Dialogs but + // it suffers from the 2038 effect so it would prevent us from + // having dates over 2038. + private Calendar mMinDate; + + private Calendar mMaxDate; + + private Calendar mCurrentDate; + + /** + * The callback used to indicate the user changes\d the date. + */ + public interface OnMonthOrWeekChangedListener { + + /** + * Called upon a date change. + * + * @param view The view associated with this listener. + * @param year The year that was set. + * @param positionInYear The month or week in year. + */ + void onMonthOrWeekChanged(TwoFieldDatePicker view, int year, int positionInYear); + } + + public TwoFieldDatePicker(Context context, double minValue, double maxValue) { + super(context, null, android.R.attr.datePickerStyle); + + LayoutInflater inflater = (LayoutInflater) context + .getSystemService(Context.LAYOUT_INFLATER_SERVICE); + inflater.inflate(R.layout.two_field_date_picker, this, true); + + OnValueChangeListener onChangeListener = new OnValueChangeListener() { + @Override + public void onValueChange(NumberPicker picker, int oldVal, int newVal) { + int year = getYear(); + int positionInYear = getPositionInYear(); + // take care of wrapping of days and months to update greater fields + if (picker == mPositionInYearSpinner) { + positionInYear = newVal; + if (oldVal == picker.getMaxValue() && newVal == picker.getMinValue()) { + year += 1; + positionInYear = getMinPositionInYear(year); + } else if (oldVal == picker.getMinValue() && newVal == picker.getMaxValue()) { + year -= 1; + positionInYear = getMaxPositionInYear(year); + } + } else if (picker == mYearSpinner) { + year = newVal; + } else { + throw new IllegalArgumentException(); + } + + // now set the date to the adjusted one + setCurrentDate(year, positionInYear); + updateSpinners(); + notifyDateChanged(); + } + }; + + mCurrentDate = Calendar.getInstance(TimeZone.getTimeZone("UTC")); + if (minValue >= maxValue) { + mMinDate = Calendar.getInstance(TimeZone.getTimeZone("UTC")); + mMinDate.set(0, 0, 1); + mMaxDate = Calendar.getInstance(TimeZone.getTimeZone("UTC")); + mMaxDate.set(9999, 0, 1); + } else { + mMinDate = getDateForValue(minValue); + mMaxDate = getDateForValue(maxValue); + } + + // month + mPositionInYearSpinner = (NumberPicker) findViewById(R.id.position_in_year); + mPositionInYearSpinner.setOnLongPressUpdateInterval(200); + mPositionInYearSpinner.setOnValueChangedListener(onChangeListener); + + // year + mYearSpinner = (NumberPicker) findViewById(R.id.year); + mYearSpinner.setOnLongPressUpdateInterval(100); + mYearSpinner.setOnValueChangedListener(onChangeListener); + } + + /** + * Initialize the state. If the provided values designate an inconsistent + * date the values are normalized before updating the spinners. + * + * @param year The initial year. + * @param positionInYear The initial month starting from zero or week in year. + * @param onMonthOrWeekChangedListener How user is notified date is changed by + * user, can be null. + */ + public void init(int year, int positionInYear, + OnMonthOrWeekChangedListener onMonthOrWeekChangedListener) { + setCurrentDate(year, positionInYear); + updateSpinners(); + mMonthOrWeekChangedListener = onMonthOrWeekChangedListener; + } + + public boolean isNewDate(int year, int positionInYear) { + return (getYear() != year || getPositionInYear() != positionInYear); + } + + /** + * Subclasses know the semantics of @value, and need to return + * a Calendar corresponding to it. + */ + protected abstract Calendar getDateForValue(double value); + + /** + * Updates the current date. + * + * @param year The year. + * @param positionInYear The month or week in year. + */ + public void updateDate(int year, int positionInYear) { + if (!isNewDate(year, positionInYear)) { + return; + } + setCurrentDate(year, positionInYear); + updateSpinners(); + notifyDateChanged(); + } + + /** + * Subclasses know the semantics of @positionInYear, and need to update @mCurrentDate to the + * appropriate date. + */ + protected abstract void setCurrentDate(int year, int positionInYear); + + protected void setCurrentDate(Calendar date) { + mCurrentDate = date; + } + + @Override + public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { + onPopulateAccessibilityEvent(event); + return true; + } + + @Override + public void onPopulateAccessibilityEvent(AccessibilityEvent event) { + super.onPopulateAccessibilityEvent(event); + + final int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR; + String selectedDateUtterance = DateUtils.formatDateTime(getContext(), + mCurrentDate.getTimeInMillis(), flags); + event.getText().add(selectedDateUtterance); + } + + /** + * @return The selected year. + */ + public int getYear() { + return mCurrentDate.get(Calendar.YEAR); + } + + /** + * @return The selected month or week. + */ + public abstract int getPositionInYear(); + + protected abstract int getMaxYear(); + + protected abstract int getMinYear(); + + protected abstract int getMaxPositionInYear(int year); + + protected abstract int getMinPositionInYear(int year); + + protected Calendar getMaxDate() { + return mMaxDate; + } + + protected Calendar getMinDate() { + return mMinDate; + } + + protected Calendar getCurrentDate() { + return mCurrentDate; + } + + protected NumberPicker getPositionInYearSpinner() { + return mPositionInYearSpinner; + } + + protected NumberPicker getYearSpinner() { + return mYearSpinner; + } + + /** + * This method should be subclassed to update the spinners based on mCurrentDate. + */ + protected void updateSpinners() { + mPositionInYearSpinner.setDisplayedValues(null); + + // set the spinner ranges respecting the min and max dates + mPositionInYearSpinner.setMinValue(getMinPositionInYear(getYear())); + mPositionInYearSpinner.setMaxValue(getMaxPositionInYear(getYear())); + mPositionInYearSpinner.setWrapSelectorWheel( + !mCurrentDate.equals(mMinDate) && !mCurrentDate.equals(mMaxDate)); + + // year spinner range does not change based on the current date + mYearSpinner.setMinValue(getMinYear()); + mYearSpinner.setMaxValue(getMaxYear()); + mYearSpinner.setWrapSelectorWheel(false); + + // set the spinner values + mYearSpinner.setValue(getYear()); + mPositionInYearSpinner.setValue(getPositionInYear()); + } + + /** + * Notifies the listener, if such, for a change in the selected date. + */ + protected void notifyDateChanged() { + sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED); + if (mMonthOrWeekChangedListener != null) { + mMonthOrWeekChangedListener.onMonthOrWeekChanged(this, getYear(), getPositionInYear()); + } + } +} diff --git a/ui/android/java/src/org/chromium/ui/picker/TwoFieldDatePickerDialog.java b/ui/android/java/src/org/chromium/ui/picker/TwoFieldDatePickerDialog.java new file mode 100644 index 0000000000000..e3f01e71ee254 --- /dev/null +++ b/ui/android/java/src/org/chromium/ui/picker/TwoFieldDatePickerDialog.java @@ -0,0 +1,113 @@ +// Copyright 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.ui.picker; + +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.DialogInterface.OnClickListener; + +import org.chromium.ui.R; +import org.chromium.ui.picker.TwoFieldDatePicker.OnMonthOrWeekChangedListener; + +public abstract class TwoFieldDatePickerDialog extends AlertDialog implements OnClickListener, + OnMonthOrWeekChangedListener { + + private static final String YEAR = "year"; + private static final String POSITION_IN_YEAR = "position_in_year"; + + protected final TwoFieldDatePicker mPicker; + protected final OnValueSetListener mCallBack; + + /** + * The callback used to indicate the user is done filling in the date. + */ + public interface OnValueSetListener { + + /** + * @param year The year that was set. + * @param positionInYear The position in the year that was set. + */ + void onValueSet(int year, int positionInYear); + } + + /** + * @param context The context the dialog is to run in. + * @param callBack How the parent is notified that the date is set. + * @param year The initial year of the dialog. + * @param weekOfYear The initial week of the dialog. + */ + public TwoFieldDatePickerDialog(Context context, + OnValueSetListener callBack, + int year, + int positionInYear, + double minValue, + double maxValue) { + this(context, 0, callBack, year, positionInYear, minValue, maxValue); + } + + /** + * @param context The context the dialog is to run in. + * @param theme the theme to apply to this dialog + * @param callBack How the parent is notified that the date is set. + * @param year The initial year of the dialog. + * @param weekOfYear The initial week of the dialog. + */ + public TwoFieldDatePickerDialog(Context context, + int theme, + OnValueSetListener callBack, + int year, + int positionInYear, + double minValue, + double maxValue) { + super(context, theme); + + mCallBack = callBack; + + setButton(BUTTON_POSITIVE, context.getText( + R.string.date_picker_dialog_set), this); + setButton(BUTTON_NEGATIVE, context.getText(android.R.string.cancel), + (OnClickListener) null); + setIcon(0); + + mPicker = createPicker(context, minValue, maxValue); + setView(mPicker); + mPicker.init(year, positionInYear, this); + } + + protected TwoFieldDatePicker createPicker(Context context, double minValue, double maxValue) { + return null; + } + + @Override + public void onClick(DialogInterface dialog, int which) { + tryNotifyDateSet(); + } + + /** + * Notifies the listener, if such, that a date has been set. + */ + protected void tryNotifyDateSet() { + if (mCallBack != null) { + mPicker.clearFocus(); + mCallBack.onValueSet(mPicker.getYear(), mPicker.getPositionInYear()); + } + } + + @Override + public void onMonthOrWeekChanged(TwoFieldDatePicker view, int year, int positionInYear) { + mPicker.init(year, positionInYear, null); + } + + /** + * Sets the current date. + * + * @param year The date week year. + * @param weekOfYear The date week. + */ + public void updateDate(int year, int weekOfYear) { + mPicker.updateDate(year, weekOfYear); + } +} diff --git a/ui/android/java/src/org/chromium/ui/picker/WeekPicker.java b/ui/android/java/src/org/chromium/ui/picker/WeekPicker.java new file mode 100644 index 0000000000000..bb9a25d08b461 --- /dev/null +++ b/ui/android/java/src/org/chromium/ui/picker/WeekPicker.java @@ -0,0 +1,141 @@ +// Copyright 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.ui.picker; + +import android.content.Context; + +import org.chromium.ui.R; + +import java.util.Calendar; +import java.util.TimeZone; + +// This class is heavily based on android.widget.DatePicker. +public class WeekPicker extends TwoFieldDatePicker { + + public WeekPicker(Context context, double minValue, double maxValue) { + super(context, minValue, maxValue); + + getPositionInYearSpinner().setContentDescription( + getResources().getString(R.string.accessibility_date_picker_week)); + + // initialize to current date + Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("UTC")); + cal.setFirstDayOfWeek(Calendar.MONDAY); + cal.setMinimalDaysInFirstWeek(4); + cal.setTimeInMillis(System.currentTimeMillis()); + init(getISOWeekYearForDate(cal), getWeekForDate(cal), null); + } + + /** + * Creates a date object from the |year| and |week|. + */ + public static Calendar createDateFromWeek(int year, int week) { + Calendar date = Calendar.getInstance(TimeZone.getTimeZone("UTC")); + date.clear(); + date.setFirstDayOfWeek(Calendar.MONDAY); + date.setMinimalDaysInFirstWeek(4); + date.set(Calendar.DAY_OF_WEEK, Calendar.MONDAY); + date.set(Calendar.YEAR, year); + date.set(Calendar.WEEK_OF_YEAR, week); + return date; + } + + /** + * Creates a date object from the |value| which is milliseconds since epoch. + */ + public static Calendar createDateFromValue(double value) { + Calendar date = Calendar.getInstance(TimeZone.getTimeZone("UTC")); + date.clear(); + date.setFirstDayOfWeek(Calendar.MONDAY); + date.setMinimalDaysInFirstWeek(4); + date.setTimeInMillis((long) value); + return date; + } + + @Override + protected Calendar getDateForValue(double value) { + return WeekPicker.createDateFromValue(value); + } + + public static int getISOWeekYearForDate(Calendar date) { + int year = date.get(Calendar.YEAR); + int month = date.get(Calendar.MONTH); + int week = date.get(Calendar.WEEK_OF_YEAR); + if (month == 0 && week > 51) { + year--; + } else if (month == 11 && week == 1) { + year++; + } + return year; + } + + public static int getWeekForDate(Calendar date) { + return date.get(Calendar.WEEK_OF_YEAR); + } + + @Override + protected void setCurrentDate(int year, int week) { + Calendar date = createDateFromWeek(year, week); + if (date.before(getMinDate())) { + setCurrentDate(getMinDate()); + } else if (date.after(getMaxDate())) { + setCurrentDate(getMaxDate()); + } else { + setCurrentDate(date); + } + } + + private int getNumberOfWeeks(int year) { + // Create a date in the middle of the year, where the week year matches the year. + Calendar date = createDateFromWeek(year, 20); + return date.getActualMaximum(Calendar.WEEK_OF_YEAR); + } + + /** + * @return The selected year. + */ + @Override + public int getYear() { + return getISOWeekYearForDate(getCurrentDate()); + } + + /** + * @return The selected week. + */ + public int getWeek() { + return getWeekForDate(getCurrentDate()); + } + + @Override + public int getPositionInYear() { + return getWeek(); + } + + @Override + protected int getMaxYear() { + return getISOWeekYearForDate(getMaxDate()); + } + + @Override + protected int getMinYear() { + return getISOWeekYearForDate(getMinDate()); + } + + @Override + protected int getMaxPositionInYear(int year) { + if (year == getISOWeekYearForDate(getMaxDate())) { + return getWeekForDate(getMaxDate()); + } + return getNumberOfWeeks(year); + } + + @Override + protected int getMinPositionInYear(int year) { + if (year == getISOWeekYearForDate(getMinDate())) { + return getWeekForDate(getMinDate()); + } + return 1; + } +} diff --git a/ui/android/java/src/org/chromium/ui/picker/WeekPickerDialog.java b/ui/android/java/src/org/chromium/ui/picker/WeekPickerDialog.java new file mode 100644 index 0000000000000..e769c1a25c472 --- /dev/null +++ b/ui/android/java/src/org/chromium/ui/picker/WeekPickerDialog.java @@ -0,0 +1,56 @@ +// Copyright 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.ui.picker; + +import android.content.Context; + +import org.chromium.ui.R; + +public class WeekPickerDialog extends TwoFieldDatePickerDialog { + + /** + * @param context The context the dialog is to run in. + * @param callBack How the parent is notified that the date is set. + * @param year The initial year of the dialog. + * @param weekOfYear The initial week of the dialog. + */ + public WeekPickerDialog(Context context, + OnValueSetListener callBack, + int year, int weekOfYear, + double minValue, double maxValue) { + this(context, 0, callBack, year, weekOfYear, minValue, maxValue); + } + + /** + * @param context The context the dialog is to run in. + * @param theme the theme to apply to this dialog + * @param callBack How the parent is notified that the date is set. + * @param year The initial year of the dialog. + * @param weekOfYear The initial week of the dialog. + */ + public WeekPickerDialog(Context context, + int theme, + OnValueSetListener callBack, + int year, + int weekOfYear, + double minValue, double maxValue) { + super(context, theme, callBack, year, weekOfYear, minValue, maxValue); + setTitle(R.string.week_picker_dialog_title); + } + + @Override + protected TwoFieldDatePicker createPicker(Context context, double minValue, double maxValue) { + return new WeekPicker(context, minValue, maxValue); + } + + /** + * Gets the {@link WeekPicker} contained in this dialog. + * + * @return The calendar view. + */ + public WeekPicker getWeekPicker() { + return (WeekPicker) mPicker; + } +} diff --git a/ui/android/java/strings/android_ui_strings.grd b/ui/android/java/strings/android_ui_strings.grd new file mode 100644 index 0000000000000..c2a42a5eaedb7 --- /dev/null +++ b/ui/android/java/strings/android_ui_strings.grd @@ -0,0 +1,221 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Unable to complete previous operation due to low memory + + + Failed to open selected file + + + More + + + Hue + + + Saturation + + + Value + + + Set + + + Cancel + + + Select color + + + Red + + + Cyan + + + Blue + + + Green + + + Magenta + + + Yellow + + + Black + + + White + + + Failed to copy to the clipboard + + + Month + + + Year + + + Set + + + Set month + + + Week + + + Set week + + + AM + + + PM + + + Set time + + + Hour + + + Minute + + + Second + + + Millisecond + + + AM/PM + + + : + + + : + + + . + + + Set date and time + + + Date + + + Time + + + Other + + + Set date + + + Clear + + + + diff --git a/ui/android/java/strings/translations/android_ui_strings_am.xtb b/ui/android/java/strings/translations/android_ui_strings_am.xtb new file mode 100644 index 0000000000000..f5ac9dbecfb99 --- /dev/null +++ b/ui/android/java/strings/translations/android_ui_strings_am.xtb @@ -0,0 +1,43 @@ + + + +አረንጓዴ +: +ዓመት +ቀን ያዘጋጁ +ይቅር +ቀለም ይምረጡ +ሮዝ +የተመረጠውን ፋይል መክፈት አልተሳካም +ጥቁር +ሰዓት +ቢጫ +ቀን +ቀይ +ሰማያዊ +አዘጋጅ +ደቂቃ +ሰከንድ +ሳምንት +ሰዓት ያዘጋጁ +ወር +ሳምንት ያዘጋጁ +በአነስተኛ ማህደረ ትውስታ ምክንያት ቀዳሚውን ክወና ማጠናቀቅ አልተቻለም +ጥዋት +ተጨማሪ +እሴት +ውሃ ሰማያዊ +ጥዋት/ከሰዓት +ለይ ቀለም +ነጭ +. +የቀለም ሙሌት +ሚሊሰከንድ +ሰዓት +ሌላ +አጽዳ +ከሰዓት +ወር ያዘጋጁ +ወደ ቅንጥብ ሰሌዳው መቅዳት አልተሳካም +ቀን እና ሰዓት ያዘጋጁ + \ No newline at end of file diff --git a/ui/android/java/strings/translations/android_ui_strings_ar.xtb b/ui/android/java/strings/translations/android_ui_strings_ar.xtb new file mode 100644 index 0000000000000..369d494bb57f9 --- /dev/null +++ b/ui/android/java/strings/translations/android_ui_strings_ar.xtb @@ -0,0 +1,43 @@ + + + +أخضر +: +عام +تعيين التاريخ +إلغاء +اختيار اللون +أرجواني +أخفق فتح الملف المحدد +أسود +الساعات +أصفر +التاريخ +أحمر +أزرق +تعيين +الدقائق +الثواني +الأسبوع +تعيين الوقت +شهر +تعيين الأسبوع +تعذر إكمال العملية السابقة نظرًا لانخفاض الذاكرة +صباحًا +المزيد +القيمة +سماوي +صباحًا/مساءً +تدرج اللون +أبيض +. +تشبع اللون +مللي ثانية +الوقت +أخرى +محو +مساءً +تعيين الشهر +أخفق النسخ إلى الحافظة +تعيين التاريخ والوقت + \ No newline at end of file diff --git a/ui/android/java/strings/translations/android_ui_strings_bg.xtb b/ui/android/java/strings/translations/android_ui_strings_bg.xtb new file mode 100644 index 0000000000000..d65a3ac76c851 --- /dev/null +++ b/ui/android/java/strings/translations/android_ui_strings_bg.xtb @@ -0,0 +1,43 @@ + + + +зелено +: +Година +Задаване на датата +Отказ +Избор на цвят +пурпурно +Файлът не можа да се отвори +черно +Час +жълто +Дата +червено +синьо +Задаване +Mинута +Секунда +Седмица +Задаване на часа +Месец +Задаване на седмицата +Предишната операция не можа да завърши поради недостиг на памет +AM +Още +Стойност +синьозелено +AM/PM +Цветови тон +бяло +. +Насищане +Милисекунда +Време +Друго +Изчистване +PM +Задаване на месеца +Копирането в буферната памет не бе успешно +Задаване на датата и часа + \ No newline at end of file diff --git a/ui/android/java/strings/translations/android_ui_strings_ca.xtb b/ui/android/java/strings/translations/android_ui_strings_ca.xtb new file mode 100644 index 0000000000000..668c7b9fc3845 --- /dev/null +++ b/ui/android/java/strings/translations/android_ui_strings_ca.xtb @@ -0,0 +1,43 @@ + + + +Verd +: +Any +Defineix la data +Cancel·la +Selecció de color +Magenta +No s'ha pogut obrir fitxer sel. +Negre +Hora +Groc +Data +Vermell +Blau +Configura +Minut +Segon +Setmana +Defineix l'hora +Mes +Defineix la setmana +No es pot completar l'operació anterior perquè hi ha poca memòria. +a.m. +Més +Valor +Cian +a. m./p. m. +To +Blanc +. +Saturació +Mil·lisegon +Hora +Altres +Esborra +p.m. +Defineix el mes +No s'ha pogut copiar el contingut al porta-retalls. +Defineix la data i l'hora + \ No newline at end of file diff --git a/ui/android/java/strings/translations/android_ui_strings_cs.xtb b/ui/android/java/strings/translations/android_ui_strings_cs.xtb new file mode 100644 index 0000000000000..c673bd20d1e7d --- /dev/null +++ b/ui/android/java/strings/translations/android_ui_strings_cs.xtb @@ -0,0 +1,43 @@ + + + +Zelená +: +Rok +Nastavení data +Zrušit +Výběr barvy +Purpurová +Vybraný soubor nelze otevřít +Černá +Hodina +Žlutá +Datum +Červená +Modrá +Nastavit +Minuta +Sekunda +Týden +Nastavení času +Měsíc +Nastavení týdne +Předchozí operaci nelze dokončit z důvodu nedostatku paměti +AM +Více +Hodnota +Azurová +AM/PM +Odstín +Bílá +, +Sytost +Milisekunda +Čas +Ostatní +Vymazat +PM +Nastavení měsíce +Zkopírování obsahu do schránky se nezdařilo +Nastavení data a času + \ No newline at end of file diff --git a/ui/android/java/strings/translations/android_ui_strings_da.xtb b/ui/android/java/strings/translations/android_ui_strings_da.xtb new file mode 100644 index 0000000000000..ffad8c634f20a --- /dev/null +++ b/ui/android/java/strings/translations/android_ui_strings_da.xtb @@ -0,0 +1,43 @@ + + + +Grøn +: +År +Angiv dato +Annuller +Vælg farve +Magenta +Den valgte fil kunne ikke åbnes +Sort +Time +Gul +Dato +Rød +Blå +Angiv +Minut +Sekund +Uge +Angiv klokkeslæt +Måned +Angiv uge +Den tidligerere handling kunne ikke fuldføres på grund af manglende hukommelse +AM +Mere +Værdi +Cyan +AM/PM +Farvetone +Hvid +. +Mætning +Millisekund +Tid +Andet +Ryd +PM +Angiv måned +Der kunne ikke kopieres til udklipsholder +Angiv dato og klokkeslæt + \ No newline at end of file diff --git a/ui/android/java/strings/translations/android_ui_strings_de.xtb b/ui/android/java/strings/translations/android_ui_strings_de.xtb new file mode 100644 index 0000000000000..144213320eb89 --- /dev/null +++ b/ui/android/java/strings/translations/android_ui_strings_de.xtb @@ -0,0 +1,43 @@ + + + +Grün +: +Jahr +Datum festlegen +Abbrechen +Farbe auswählen +Magenta +Fehler beim Öffnen der Datei +Schwarz +Stunde +Gelb +Datum +Rot +Blau +Festlegen +Minute +Sekunde +Woche +Uhrzeit festlegen +Monat +Woche festlegen +Zu wenig Speicher für vorherige Operation +AM +Mehr +Wert +Cyan +AM/PM +Farbton +Weiß +. +Sättigung +Millisekunde +Zeit +Sonstiges +Löschen +PM +Monat festlegen +Fehler beim Kopieren in die Zwischenablage +Datum und Uhrzeit festlegen + \ No newline at end of file diff --git a/ui/android/java/strings/translations/android_ui_strings_el.xtb b/ui/android/java/strings/translations/android_ui_strings_el.xtb new file mode 100644 index 0000000000000..1403909258b0e --- /dev/null +++ b/ui/android/java/strings/translations/android_ui_strings_el.xtb @@ -0,0 +1,43 @@ + + + +Πράσινο +: +Έτος +Ορισμός ημερομηνίας +Ακύρωση +Επιλογή χρώματος +Ματζέντα +Αποτυχ. ανοίγμ. επιλεγμ. αρχείου +Μαύρο +Ώρα +Κίτρινο +Ημερομηνία +Κόκκινο +Μπλε +Ορισμός +Λεπτό +Δευτερόλεπτο +Εβδομάδα +Ορισμός χρόνου +Μήνας +Ορισμός εβδομάδας +Δεν ήταν δυνατή η ολοκλήρωση της προηγούμενης λειτουργίας λόγω χαμηλού επιπέδου μνήμης +π.μ. +Περισσότερα +Τιμή +Κυανό +Π.Μ./Μ.Μ. +Απόχρωση +Λευκό +. +Κορεσμός +Χιλιοστό του δευτερολέπτου +Ώρα +Άλλο +Διαγραφή +μ.μ. +Ορισμός μήνα +Αποτυχία αντιγραφής στο πρόχειρο +Ορισμός ημερομηνίας και ώρας + \ No newline at end of file diff --git a/ui/android/java/strings/translations/android_ui_strings_en-GB.xtb b/ui/android/java/strings/translations/android_ui_strings_en-GB.xtb new file mode 100644 index 0000000000000..e8d01090f9d8d --- /dev/null +++ b/ui/android/java/strings/translations/android_ui_strings_en-GB.xtb @@ -0,0 +1,43 @@ + + + +Green +: +Year +Set date +Cancel +Select colour +Magenta +Failed to open selected file +Black +Hour +Yellow +Date +Red +Blue +Set +Minute +Second +Week +Set time +Month +Set week +Unable to complete previous operation due to low memory +a.m. +More +Value +Cyan +AM/PM +Hue +White +. +Saturation +Millisecond +Time +Other +Clear +p.m. +Set month +Failed to copy to the clipboard +Set date and time + \ No newline at end of file diff --git a/ui/android/java/strings/translations/android_ui_strings_es-419.xtb b/ui/android/java/strings/translations/android_ui_strings_es-419.xtb new file mode 100644 index 0000000000000..7cf6daf686a5f --- /dev/null +++ b/ui/android/java/strings/translations/android_ui_strings_es-419.xtb @@ -0,0 +1,43 @@ + + + +Verde +: +Año +Establecer fecha +Cancelar +Seleccionar color +Magenta +Error al abrir el archivo +Negro +Hora +Amarillo +Fecha +Rojo +Azul +Establecer +Minuto +Segundo +Semana +Establecer hora +Mes +Establecer semana +Memoria insuficiente para completar la operación anterior +a. m. +Más +Valor +Cian +AM/PM +Tono +Blanco +. +Saturación +Milisegundo +Hora +Otros +Borrar +p. m. +Establecer mes +Error al copiar al portapapeles +Establecer fecha y hora + \ No newline at end of file diff --git a/ui/android/java/strings/translations/android_ui_strings_es.xtb b/ui/android/java/strings/translations/android_ui_strings_es.xtb new file mode 100644 index 0000000000000..4bc723ce14e21 --- /dev/null +++ b/ui/android/java/strings/translations/android_ui_strings_es.xtb @@ -0,0 +1,43 @@ + + + +Verde +: +Año +Establecer fecha +Cancelar +Seleccionar color +Magenta +Error abrir archivo seleccionado +Negro +Hora +Amarillo +Fecha +Rojo +Azul +Establecer +Minuto +Segundo +Semana +Establecer hora +Mes +Establecer semana +No se ha podido completar la operación anterior por falta de memoria +AM +Más +Valor +Cian +A.M./P.M. +Matiz +Blanco +. +Saturación +Milisegundo +Hora +Otro +Eliminar +PM +Establecer mes +Error al copiar en el portapapeles +Establecer fecha y hora + \ No newline at end of file diff --git a/ui/android/java/strings/translations/android_ui_strings_fa.xtb b/ui/android/java/strings/translations/android_ui_strings_fa.xtb new file mode 100644 index 0000000000000..4f23d1f5503bc --- /dev/null +++ b/ui/android/java/strings/translations/android_ui_strings_fa.xtb @@ -0,0 +1,43 @@ + + + +سبز +: +سال +تنظیم تاریخ +لغو +انتخاب رنگ +سرخابی +باز کردن فایل انتخابی انجام نشد +سیاه +ساعت +زرد +تاریخ +قرمز +آبی +تنظیم +دقیقه +ثانیه +هفته +تنظیم زمان +ماه +تنظیم هفته +به دلیل کم بودن حافظه، تکمیل عملیات قبلی امکان‌پذیر نیست +ق.ظ +بیشتر +مقدار +فیروزه‌ای +ق.ظ/ب.ظ +رنگ‌مایه +سفید +. +اشباع رنگ +میلی‌ثانیه +زمان +دیگر +پاک کردن +ب.ظ +تنظیم ماه +کپی در کلیپ بورد ناموفق بود +تنظیم تاریخ و زمان + \ No newline at end of file diff --git a/ui/android/java/strings/translations/android_ui_strings_fi.xtb b/ui/android/java/strings/translations/android_ui_strings_fi.xtb new file mode 100644 index 0000000000000..49b29a15d2e60 --- /dev/null +++ b/ui/android/java/strings/translations/android_ui_strings_fi.xtb @@ -0,0 +1,43 @@ + + + +Vihreä +. +Vuosi +Aseta päivämäärä +Peruuta +Valitse väri +Purppura +Valittua tiedostoa ei voi avata +Musta +Tunti +Keltainen +Päiväys +Punainen +Sininen +Aseta +Minuutti +Sekunti +Viikko +Aseta aika +Kuukausi +Aseta viikko +Edellistä toimintoa ei voi suorittaa. Muisti ei riitä. +ap +Lisää +Arvo +Turkoosi +AP/IP +Sävy +Valkoinen +, +Värikylläisyys +Millisekunti +Aika +Muu +Tyhjennä +ip +Aseta kuukausi +Kopiointi leikepöydälle epäonnistui +Aseta päivämäärä ja aika + \ No newline at end of file diff --git a/ui/android/java/strings/translations/android_ui_strings_fil.xtb b/ui/android/java/strings/translations/android_ui_strings_fil.xtb new file mode 100644 index 0000000000000..247e6afa5c832 --- /dev/null +++ b/ui/android/java/strings/translations/android_ui_strings_fil.xtb @@ -0,0 +1,43 @@ + + + +Berde +: +Taon +Magtakda ng petsa +Ikansela +Pumili ng kulay +Magenta +Hindi mabuksan ang napiling file +Itim +Oras +Dilaw +Petsa +Pula +Asul +Itakda +Minuto +Segundo +Linggo +Itakda ang oras +Buwan +Itakda ang linggo +Hindi makumpleto ang nakaraang operasyon dahil sa mababang memory +AM +Higit pa +Value +Cyan +AM/PM +Hue +Puti +. +Saturation +Millisecond +Oras +Iba pa +I-clear +PM +Itakda ang buwan +Nabigong kopyahin sa clipboard +Itakda ang petsa at oras + \ No newline at end of file diff --git a/ui/android/java/strings/translations/android_ui_strings_fr.xtb b/ui/android/java/strings/translations/android_ui_strings_fr.xtb new file mode 100644 index 0000000000000..314fc115eeec2 --- /dev/null +++ b/ui/android/java/strings/translations/android_ui_strings_fr.xtb @@ -0,0 +1,43 @@ + + + +Vert +: +Année +Définir la date +Annuler +Sélectionner couleur +Magenta +Échec de l'ouverture du fichier. +Noir +Heure +Jaune +Date +Rouge +Bleu +Définir +Minute +Seconde +Semaine +Définir l'heure +Mois +Définir la semaine +Impossible de terminer l'opération précédente. Mémoire insuffisante. +a.m. +Plus +Valeur +Cyan +AM/PM +Teinte +Blanc +. +Saturation +Milliseconde +Heure +Autre +Effacer +p.m. +Définir le mois +Échec de la copie du contenu dans le Presse-papiers. +Définir la date et l'heure + \ No newline at end of file diff --git a/ui/android/java/strings/translations/android_ui_strings_hi.xtb b/ui/android/java/strings/translations/android_ui_strings_hi.xtb new file mode 100644 index 0000000000000..57e74a7ce3bfd --- /dev/null +++ b/ui/android/java/strings/translations/android_ui_strings_hi.xtb @@ -0,0 +1,43 @@ + + + +हरा +: +वर्ष +दिनांक सेट करें +रहने दें +रंग चुनें +मैजेंटा +चयनित फ़ाइल खोलने में विफल +काला +घंटा +पीला +दिनांक +लाल +नीला +सेट करें +मिनट +सेकंड +सप्ताह +समय सेट करें +माह +सप्ताह सेट करें +कम स्‍मृति के कारण पिछली कार्रवाई पूरी करने में असमर्थ +पूर्वाह्न +अधिक +मान +स्यान +पूर्वाह्न/अपराह्न +रंग +सफ़ेद +. +संतृप्तता +मिलीसेकंड +समय +अन्य +साफ़ करें +अपराह्न +माह सेट करें +क्लिपबोर्ड पर प्रतिलिपि बनाने में विफल रहा +दिनांक और समय सेट करें + \ No newline at end of file diff --git a/ui/android/java/strings/translations/android_ui_strings_hr.xtb b/ui/android/java/strings/translations/android_ui_strings_hr.xtb new file mode 100644 index 0000000000000..6837ecacd56bf --- /dev/null +++ b/ui/android/java/strings/translations/android_ui_strings_hr.xtb @@ -0,0 +1,43 @@ + + + +Zelena +. +Godina +Postavite datum +Odustani +Odaberite boju +Magenta +Odabrana datoteka nije otvorena +Crna +sat +Žuta +Datum +Crvena +Plava +Postavi +minuta +sekunda +Tjedan +Postavite vrijeme +Mjesec +Postavite tjedan +Nije moguće dovršiti prethodnu operaciju jer nema dovoljno memorije +AM +Više +Vrijednost +Cijan +prijepodne/poslijepodne +Ton +Bijela +. +Zasićenje +milisekunda +Vrijeme +Ostalo +Izbriši +PM +Postavite mjesec +Nije kopirano u međuspremnik +Postavite datum i vrijeme + \ No newline at end of file diff --git a/ui/android/java/strings/translations/android_ui_strings_hu.xtb b/ui/android/java/strings/translations/android_ui_strings_hu.xtb new file mode 100644 index 0000000000000..0ab3e9dabc82c --- /dev/null +++ b/ui/android/java/strings/translations/android_ui_strings_hu.xtb @@ -0,0 +1,43 @@ + + + +Zöld +: +Év +Dátum beállítása +Mégse +Szín kiválasztása +Magenta +A fájl megnyitása sikertelen +Fekete +Óra +Sárga +Dátum +Piros +Kék +Beállítás +Perc +Másodperc +Hét +Idő beállítása +hónap +Hét beállítása +Az előző műveletet memóriahiány miatt nem lehet elvégezni +de. +Hosszabban +Érték +Cián +de./du. +Színárnyalat +Fehér +. +Telítettség +Ezredmásodperc +Idő +Egyéb +Törlés +du. +Hónap beállítása +Nem sikerült a vágólapra másolni +Dátum és idő beállítása + \ No newline at end of file diff --git a/ui/android/java/strings/translations/android_ui_strings_id.xtb b/ui/android/java/strings/translations/android_ui_strings_id.xtb new file mode 100644 index 0000000000000..b1793ba2832a4 --- /dev/null +++ b/ui/android/java/strings/translations/android_ui_strings_id.xtb @@ -0,0 +1,43 @@ + + + +Hijau +. +Tahun +Setel tanggal +Batal +Pilih warna +Magenta +Gagal membuka file terpilih +Hitam +Jam +Kuning +Tanggal +Merah +Biru +Setel +Menit +Detik +Minggu +Setel waktu +Bulan +Setel minggu +Tidak dapat menyelesaikan operasi sebelumnya karena sisa memori sedikit +AM +Lainnya +Nilai +Sian +AM/PM +Rona +Putih +: +Saturasi +Milidetik +Waktu +Lainnya +Hapus +PM +Setel bulan +Gagal menyalin ke papan klip +Setel tanggal dan waktu + \ No newline at end of file diff --git a/ui/android/java/strings/translations/android_ui_strings_it.xtb b/ui/android/java/strings/translations/android_ui_strings_it.xtb new file mode 100644 index 0000000000000..7ef7536e9d70f --- /dev/null +++ b/ui/android/java/strings/translations/android_ui_strings_it.xtb @@ -0,0 +1,43 @@ + + + +Verde +: +Anno +Imposta data +Annulla +Seleziona colore +Magenta +Impossibile aprire file selez. +Nero +Ora +Giallo +Data +Rosso +Blu +Imposta +Minuto +Secondo +Settimana +Imposta ora +Mese +Imposta settimana +Impossibile completare l'operazione precedente. Memoria insufficiente. +AM +Più +Valore +Ciano +AM/PM +Tonalità +Bianco +. +Saturazione +Millisecondo +Ora +Altro +Cancella +PM +Imposta mese +Impossibile copiare negli appunti +Imposta data e ora + \ No newline at end of file diff --git a/ui/android/java/strings/translations/android_ui_strings_iw.xtb b/ui/android/java/strings/translations/android_ui_strings_iw.xtb new file mode 100644 index 0000000000000..8116871141bc3 --- /dev/null +++ b/ui/android/java/strings/translations/android_ui_strings_iw.xtb @@ -0,0 +1,43 @@ + + + +ירוק +: +שנה +הגדרת תאריך +ביטול +בחירת צבע +מגנטה +פתיחת הקובץ הנבחר נכשלה +שחור +שעה +צהוב +תאריך +אדום +כחול +הגדר +דקה +שנייה +שבוע +הגדרת שעה +חודש +הגדרת שבוע +לא ניתן להשלים את הפעולה הקודמת עקב מחסור בזיכרון +AM +עוד +ערך +ציאן +AM/PM +גוון +לבן +. +רווייה +אלפית שנייה +שעה +אחר +נקה +PM +הגדרת חודש +ההעתקה אל הלוח נכשלה +הגדרת תאריך ושעה + \ No newline at end of file diff --git a/ui/android/java/strings/translations/android_ui_strings_ja.xtb b/ui/android/java/strings/translations/android_ui_strings_ja.xtb new file mode 100644 index 0000000000000..d87e73c17afcb --- /dev/null +++ b/ui/android/java/strings/translations/android_ui_strings_ja.xtb @@ -0,0 +1,43 @@ + + + + +: + +日付の設定 +キャンセル +色の選択 +マゼンタ +選択したファイルを開けません + + + +日付 + + +設定 + + + +時間の設定 + +週の設定 +メモリ不足のため直前の操作を完了できません +AM +詳細表示 + +シアン +AM/PM +色調 + +. +彩度 +ミリ秒 +時間 +その他 +クリア +PM +月の設定 +クリップボードにコピーできませんでした +日時の設定 + \ No newline at end of file diff --git a/ui/android/java/strings/translations/android_ui_strings_ko.xtb b/ui/android/java/strings/translations/android_ui_strings_ko.xtb new file mode 100644 index 0000000000000..1f9856f545e06 --- /dev/null +++ b/ui/android/java/strings/translations/android_ui_strings_ko.xtb @@ -0,0 +1,43 @@ + + + +녹색 +: +연도 +날짜 설정 +취소 +색상 선택 +자홍색 +선택한 파일을 열지 못했습니다. +검정색 + +노란색 +날짜 +빨간색 +파란색 +설정 + + + +시간 설정 + +주 설정 +메모리가 부족하여 이전 작업을 완료할 수 없습니다. +오전 +더보기 + +청록색 +오전/오후 +색조 +흰색 +. +채도 +밀리초 +시간 +기타 +삭제 +오후 +월 설정 +클립보드로 복사하지 못했습니다. +날짜 및 시간 설정 + \ No newline at end of file diff --git a/ui/android/java/strings/translations/android_ui_strings_lt.xtb b/ui/android/java/strings/translations/android_ui_strings_lt.xtb new file mode 100644 index 0000000000000..5abc0d1513fd3 --- /dev/null +++ b/ui/android/java/strings/translations/android_ui_strings_lt.xtb @@ -0,0 +1,43 @@ + + + +Žalia +: +Metai +Nustatykite datą +Atšaukti +Pasirinkite spalvą +Purpurinė +Atid. pasir. failą įvyko klaida +Juoda +Valanda +Geltona +Data +Raudona +Mėlyna +Nustatyti +Minutė +Sekundė +Savaitė +Nustatykite laiką +Mėnuo +Nustatykite savaitę +Nepavyko baigti ankstesnio veiksmo dėl atminties trūkumo +priešpiet +Daugiau +Reikšmė +Žydra +iki pietų / po pietų +Spalva +Balta +. +Spalvų sodrumas +Milisekundė +Laikas +Kitas +Išvalyti +popiet +Nustatykite mėnesį +Nepavyko nukopijuoti į iškarpinę +Nustatykite datą ir laiką + \ No newline at end of file diff --git a/ui/android/java/strings/translations/android_ui_strings_lv.xtb b/ui/android/java/strings/translations/android_ui_strings_lv.xtb new file mode 100644 index 0000000000000..600c434c195ca --- /dev/null +++ b/ui/android/java/strings/translations/android_ui_strings_lv.xtb @@ -0,0 +1,43 @@ + + + +Zaļa +: +Gads +Datuma iestatīšana +Atcelt +Krāsas izvēle +Fuksīnsarkana +Neizdevās atvērt atlasīto failu. +Melna +Stundas +Dzeltena +Datums +Sarkana +Zila +Iestatīt +Minūtes +Sekundes +Nedēļa +Laika iestatīšana +Mēnesis +Nedēļas iestatīšana +Iepriekšējo darbību nevar pabeigt mazā atmiņas apjoma dēļ. +AM +Vairāk +Vērtība +Ciānzila +priekšpusdienā/pēcpusdienā +Nokrāsa +Balta +: +Piesātinājums +Milisekundes +Laiks +Cits +Notīrīt +PM +Mēneša iestatīšana +Neizdevās kopēt starpliktuvē. +Datuma un laika iestatīšana + \ No newline at end of file diff --git a/ui/android/java/strings/translations/android_ui_strings_nl.xtb b/ui/android/java/strings/translations/android_ui_strings_nl.xtb new file mode 100644 index 0000000000000..4a9d795bee474 --- /dev/null +++ b/ui/android/java/strings/translations/android_ui_strings_nl.xtb @@ -0,0 +1,43 @@ + + + +Groen +: +Jaar +Datum instellen +Annuleren +Kleur selecteren +Magenta +Kan geselec. bestand niet openen +Zwart +Uur +Geel +Datum +Rood +Blauw +Instellen +Minuut +Seconde +Week +Tijd instellen +Maand +Week instellen +Kan vorige bewerking niet voltooien. Te weinig geheugen +a.m. +Meer +Waarde +Cyaan +a.m./p.m. +Kleurtoon +Wit +. +Verzadiging +Milliseconde +Tijd +Overige +Wissen +p.m. +Maand instellen +Kopiëren naar het klembord mislukt +Datum en tijd instellen + \ No newline at end of file diff --git a/ui/android/java/strings/translations/android_ui_strings_no.xtb b/ui/android/java/strings/translations/android_ui_strings_no.xtb new file mode 100644 index 0000000000000..3b40da4c217a1 --- /dev/null +++ b/ui/android/java/strings/translations/android_ui_strings_no.xtb @@ -0,0 +1,43 @@ + + + +Grønn +: +År +Angi dato +Avbryt +Velg farge +Magenta +Kunne ikke åpne den valgte filen +Svart +Time +Gul +Dato +Rød +Blå +Angi +Minutt +Sekund +Uke +Angi tid +Måned +Angi uke +Kan ikke fullføre forrige handling på grunn av lite minne +AM +Mer +Verdi +Cyan +AM/PM +Fargetone +Hvit +. +Metning +Millisekund +Klokkeslett +Annet +Tøm +PM +Angi måned +Kunne ikke kopiere til utklippstavlen +Angi dato og klokkeslett + \ No newline at end of file diff --git a/ui/android/java/strings/translations/android_ui_strings_pl.xtb b/ui/android/java/strings/translations/android_ui_strings_pl.xtb new file mode 100644 index 0000000000000..88ea63b36aa77 --- /dev/null +++ b/ui/android/java/strings/translations/android_ui_strings_pl.xtb @@ -0,0 +1,43 @@ + + + +Zielony +: +Rok +Ustaw datę +Anuluj +Wybierz kolor +Magenta +Nie udało się otworzyć pliku +Czarny +Godzina +Żółty +Data +Czerwony +Niebieski +Ustaw +Minuta +Sekunda +Tydzień +Ustaw czas +Miesiąc +Ustaw tydzień +Zbyt mało pamięci, by ukończyć poprzednią operację +AM +Więcej +Wartość +Cyjan +rano/po południu +Odcień +Biały +. +Nasycenie +Milisekunda +Godzina +Inne +Wyczyść +PM +Ustaw miesiąc +Nie udało się skopiować do schowka +Ustaw datę i godzinę + \ No newline at end of file diff --git a/ui/android/java/strings/translations/android_ui_strings_pt-BR.xtb b/ui/android/java/strings/translations/android_ui_strings_pt-BR.xtb new file mode 100644 index 0000000000000..bbf856f52b849 --- /dev/null +++ b/ui/android/java/strings/translations/android_ui_strings_pt-BR.xtb @@ -0,0 +1,43 @@ + + + +Verde +: +Ano +Definir data +Cancelar +Selecionar cor +Magenta +Erro ao abrir arq. selecionado +Preto +Hora +Amarelo +Data +Vermelho +Azul +Definir +Minuto +Segundo +Semana +Definir hora +Mês +Definir semana +Devido à insuficiência de memória, não foi possível concluir a operação anterior +AM +Mais +Valor +Ciano +AM/PM +Matiz +Branco +. +Saturação +Milissegundo +Tempo +Outro +Limpar +PM +Definir mês +Falha ao copiar para a área de transferência +Definir data e hora + \ No newline at end of file diff --git a/ui/android/java/strings/translations/android_ui_strings_pt-PT.xtb b/ui/android/java/strings/translations/android_ui_strings_pt-PT.xtb new file mode 100644 index 0000000000000..79e78c65d134b --- /dev/null +++ b/ui/android/java/strings/translations/android_ui_strings_pt-PT.xtb @@ -0,0 +1,43 @@ + + + +Verde +: +Ano +Definir data +Cancelar +Selecionar cor +Magenta +Falha ao abrir o fich. selec. +Preto +Hora +Amarelo +Data +Vermelho +Azul +Definir +Minuto +Segundo +Semana +Definir hora +Mês +Definir semana +Não foi possível concluir a operação anterior devido à baixa memória disponível +AM +Mais +Valor +Turquesa +AM/PM +Tonalidade +Branco +. +Saturação +Milissegundo +Tempo +Outros +Limpar +PM +Definir mês +Falha ao copiar para a área de transferência +Definir data e hora + \ No newline at end of file diff --git a/ui/android/java/strings/translations/android_ui_strings_ro.xtb b/ui/android/java/strings/translations/android_ui_strings_ro.xtb new file mode 100644 index 0000000000000..8bb0643284445 --- /dev/null +++ b/ui/android/java/strings/translations/android_ui_strings_ro.xtb @@ -0,0 +1,43 @@ + + + +Verde +: +An +Setați data +Anulați +Selectați culoarea +Magenta +Fișier. select. nu s-a deschis +Negru +Oră +Galben +Data +Roșu +Albastru +Setați +Minut +Secundă +Săptămână +Setați ora +Lună +Setați săptămâna +Operația anterioară nu se poate finaliza, din cauza memoriei insuficiente +a.m. +Mai multe +Valoare +Cyan +AM/PM +Nuanță +Alb +. +Saturație +Milisecundă +Oră +Altele +Ștergeți +p.m. +Setați luna +Nu s-a copiat în clipboard +Setați data și ora + \ No newline at end of file diff --git a/ui/android/java/strings/translations/android_ui_strings_ru.xtb b/ui/android/java/strings/translations/android_ui_strings_ru.xtb new file mode 100644 index 0000000000000..fcdc91ce86121 --- /dev/null +++ b/ui/android/java/strings/translations/android_ui_strings_ru.xtb @@ -0,0 +1,43 @@ + + + +Зеленый +: +Год +Выберите дату +Отмена +Выберите цвет +Пурпурный +Не удалось открыть файл +Черный +Часы +Желтый +Дата +Красный +Синий +Установить +Минуты +Секунды +Неделя +Установите время +Месяц +Выберите неделю +Не удалось завершить операцию (недостаточно памяти) +AM +Подробнее... +Значение +Голубой +AM/PM +Тон +Белый +: +Насыщенность +Миллисекунды +Время +Другое +Очистить +PM +Выберите месяц +Не удалось копировать данные в буфер обмена +Установите дату и время + \ No newline at end of file diff --git a/ui/android/java/strings/translations/android_ui_strings_sk.xtb b/ui/android/java/strings/translations/android_ui_strings_sk.xtb new file mode 100644 index 0000000000000..c47e79c01c484 --- /dev/null +++ b/ui/android/java/strings/translations/android_ui_strings_sk.xtb @@ -0,0 +1,43 @@ + + + +Zelená +: +Rok +Nastavenie dátumu +Zrušiť +Výber farby +Purpurová +Vybr. súbor sa nepodar. otvoriť +Čierna +Hodina +Žltá +Dátum +Červená +Modrá +Nastaviť +Minúta +Sekunda +Týždeň +Nastavenie času +Mesiac +Nastavenie týždňa +Predchádzajúca operácia sa nedokončila z dôvodu nedostatku pamäte +AM +Viac +Hodnota +Azúrová +dop. / odp. +Odtieň +Biela +. +Sýtosť +Milisekunda +Čas +Iné +Vymazať +PM +Nastavenie mesiaca +Skopírovanie do schránky sa nepodarilo +Nastavenie dátumu a času + \ No newline at end of file diff --git a/ui/android/java/strings/translations/android_ui_strings_sl.xtb b/ui/android/java/strings/translations/android_ui_strings_sl.xtb new file mode 100644 index 0000000000000..badfb7ee33690 --- /dev/null +++ b/ui/android/java/strings/translations/android_ui_strings_sl.xtb @@ -0,0 +1,43 @@ + + + +Zelena +: +Leto +Nastavitev datuma +Prekliči +Izbira barve +Škrlatna +Izb. dat. ni bilo mogoče odpreti +Črna +Ura +Rumena +Datum +Rdeča +Modra +Nastavi +Minuta +Sekunda +Teden +Nastavitev časa +Mesec +Nastavitev tedna +Prejšnjega dejanja ni mogoče končati, ker primanjkuje pomnilnika +dop. +Več +Vrednost +Cianova +Dopoldne/popoldne +Odtenek +Bela +. +Nasičenost +Milisekunda +Čas +Drugo +Počisti +pop. +Nastavitev meseca +Kopiranje v odložišče ni uspelo +Nastavitev datuma in časa + \ No newline at end of file diff --git a/ui/android/java/strings/translations/android_ui_strings_sr.xtb b/ui/android/java/strings/translations/android_ui_strings_sr.xtb new file mode 100644 index 0000000000000..47825d7aa0e75 --- /dev/null +++ b/ui/android/java/strings/translations/android_ui_strings_sr.xtb @@ -0,0 +1,43 @@ + + + +Зелена +: +Година +Подесите датум +Откажи +Изаберите боју +Циклама +Неуспешно отварање изабр. датот. +Црна +Сат +Жута +Датум +Црвена +Плава +Постави +Минут +Секунд +Недеља +Подесите време +Месец +Подесите недељу +Није могуће довршити претходну радњу због недостатка меморије +AM +Више +Вредност +Плавозелена +пре подне/по подне +Нијанса +Бела +, +Засићеност боја +Милисекунд +Време +Друго +Обриши +PM +Подесите месец +Копирање у привремену меморију није успело +Подесите датум и време + \ No newline at end of file diff --git a/ui/android/java/strings/translations/android_ui_strings_sv.xtb b/ui/android/java/strings/translations/android_ui_strings_sv.xtb new file mode 100644 index 0000000000000..252968df1ef26 --- /dev/null +++ b/ui/android/java/strings/translations/android_ui_strings_sv.xtb @@ -0,0 +1,43 @@ + + + +Grön +. +År +Ange datum +Avbryt +Välj färg +Magenta +Det gick inte att öppna filen +Svart +Timme +Gul +Datum +Röd +Blå +Ange +Minut +Sekund +Vecka +Ange tid +Månad +Ange vecka +Föregående åtgärd kan inte slutföras. För lite minne. +AM +Mer +Värde +Cyanblå +FM/EM +Nyans +Vit +, +Mättnad +Millisekund +Tid +Övrigt +Rensa +PM +Ange månad +Det gick inte att kopiera till Urklipp +Ange datum och tid + \ No newline at end of file diff --git a/ui/android/java/strings/translations/android_ui_strings_sw.xtb b/ui/android/java/strings/translations/android_ui_strings_sw.xtb new file mode 100644 index 0000000000000..a1b2c441b1242 --- /dev/null +++ b/ui/android/java/strings/translations/android_ui_strings_sw.xtb @@ -0,0 +1,43 @@ + + + +Kijani +: +Mwaka +Weka tarehe +Ghairi +Chagua rangi +Rangi ya damu ya mzee +Imeshindwa kufungua faili iliyochaguliwa +Nyeusi +Saa +Manjano +Tarehe +Nyekundu +Samawati +Weka +Dakika +Sekunde +Juma +Weka muda +Mwezi +Weka wiki +Imeshindwa kukamilisha jukumu lililotangulia kwa sababu ya nafasi ndogo ya hifadhi +AM +Zaidi +Thamani +Samawati-Kijani +AM / PM +Rangi +Nyeupe +. +Kukolea +Milisekunde +Muda +Nyingine +Futa +PM +Weka mwezi +Ilishindwa kunakiliwa kwenda kwenye ubao klipu. +Weka tarehe na saa + \ No newline at end of file diff --git a/ui/android/java/strings/translations/android_ui_strings_th.xtb b/ui/android/java/strings/translations/android_ui_strings_th.xtb new file mode 100644 index 0000000000000..7039d1bdf0eb4 --- /dev/null +++ b/ui/android/java/strings/translations/android_ui_strings_th.xtb @@ -0,0 +1,43 @@ + + + +สีเขียว +. +ปี +ตั้งวันที่ +ยกเลิก +เลือกสี +สีม่วงแดง +ไม่สามารถเปิดไฟล์ที่เลือก +สีดำ +ชั่วโมง +สีเหลือง +วันที่ +สีแดง +สีน้ำเงิน +ตั้งค่า +นาที +วินาที +สัปดาห์ +ตั้งเวลา +เดือน +ตั้งสัปดาห์ +ไม่สามารถดำเนินการก่อนหน้าให้สิ้นสุดได้เพราะหน่วยความจำเหลือน้อย +AM +เพิ่มเติม +ราคา +สีฟ้า +AM/PM +โทนสี +สีขาว +. +ความอิ่มตัวของสี +มิลลิวินาที +เวลา +อื่นๆ +ล้าง +PM +ตั้งเดือน +ไม่สามารถคัดลอกไปยังคลิปบอร์ด +ตั้งวันที่และเวลา + \ No newline at end of file diff --git a/ui/android/java/strings/translations/android_ui_strings_tr.xtb b/ui/android/java/strings/translations/android_ui_strings_tr.xtb new file mode 100644 index 0000000000000..4c44caf5b94f3 --- /dev/null +++ b/ui/android/java/strings/translations/android_ui_strings_tr.xtb @@ -0,0 +1,43 @@ + + + +Yeşil +: +Yıl +Tarihi ayarlayın +İptal +Renk seçin +Macenta +Seçilen dosya açılamadı +Siyah +Saat +Sarı +Tarih +Kırmızı +Mavi +Ayarla +Dakika +Saniye +Hafta +Saati ayarlayın +Ay +Haftayı ayarlayın +Bellek yetersiz olduğundan önceki işlem tamamlanamadı +ÖÖ +Daha fazla +Değer +Camgöbeği +AM/PM +Ton +Beyaz +. +Doygunluk +Milisaniye +Zaman +Diğer +Temizle +ÖS +Ayı ayarlayın +Panoya kopyalanamadı +Tarihi ve saati ayarlayın + \ No newline at end of file diff --git a/ui/android/java/strings/translations/android_ui_strings_uk.xtb b/ui/android/java/strings/translations/android_ui_strings_uk.xtb new file mode 100644 index 0000000000000..976dc0844633d --- /dev/null +++ b/ui/android/java/strings/translations/android_ui_strings_uk.xtb @@ -0,0 +1,43 @@ + + + +Зелений +: +Рік +Вибрати дату +Скасувати +Вибрати колір +Пурпурний +Не вдалося відкрити файл +Чорний +Години +Жовтий +Дата +Червоний +Синій +Встановити +Хвилини +Секунди +Тиждень +Вибрати час +Місяць +Вибрати тиждень +Не вдається закінчити попередню операцію через нестачу пам’яті +дп +Більше +Яскравість +Бірюзовий +д.п./п.п. +Тон +Білий +. +Насиченість +Мілісекунди +Час +Інше +Очистити +пп +Вибрати місяць +Не вдалося скопіювати в буфер обміну +Вибрати дату й час + \ No newline at end of file diff --git a/ui/android/java/strings/translations/android_ui_strings_vi.xtb b/ui/android/java/strings/translations/android_ui_strings_vi.xtb new file mode 100644 index 0000000000000..deaf2d2ab36ca --- /dev/null +++ b/ui/android/java/strings/translations/android_ui_strings_vi.xtb @@ -0,0 +1,43 @@ + + + +Xanh lục +: +Năm +Đặt ngày +Hủy +Chọn màu +Đỏ thẫm +Không mở được tệp đã chọn +Đen +Giờ +Vàng +Ngày Tháng +Đỏ +Xanh lam +Đặt +Phút +Giây +Tuần +Đặt thời gian +Tháng +Đặt tuần +Không thể hoàn tất thao tác trước do bộ nhớ thấp +SA +Thêm +Giá trị +Lục lam +SA/CH +Màu sắc +Trắng +. +Độ bão hòa +Mili giây +Thời gian +Khác +Xóa +CH +Đặt tháng +Sao chép sang khay nhớ tạm không thành công +Đặt ngày giờ + \ No newline at end of file diff --git a/ui/android/java/strings/translations/android_ui_strings_zh-CN.xtb b/ui/android/java/strings/translations/android_ui_strings_zh-CN.xtb new file mode 100644 index 0000000000000..c5bc02e039fb2 --- /dev/null +++ b/ui/android/java/strings/translations/android_ui_strings_zh-CN.xtb @@ -0,0 +1,43 @@ + + + +绿色 +: + +设置日期 +取消 +选择颜色 +洋红色 +无法打开所选文件 +黑色 +小时 +黄色 +日期 +红色 +蓝色 +设置 +分钟 + + +设置时间 + +设置星期 +内存不足,无法完成上一操作 +上午 +更多 + +青色 +上午/下午 +色调 +白色 +. +饱和度 +毫秒 +时间 +其他 +清除 +下午 +设置月份 +未能复制到剪贴板 +设置日期和时间 + \ No newline at end of file diff --git a/ui/android/java/strings/translations/android_ui_strings_zh-TW.xtb b/ui/android/java/strings/translations/android_ui_strings_zh-TW.xtb new file mode 100644 index 0000000000000..0eb11180bff0c --- /dev/null +++ b/ui/android/java/strings/translations/android_ui_strings_zh-TW.xtb @@ -0,0 +1,43 @@ + + + +綠色 +: + +設定日期 +取消 +選取顏色 +洋紅色 +無法開啟選取的檔案 +黑色 +小時 +黃色 +日期 +紅色 +藍色 +設定 +分鐘 + + +設定時間 + +設定週次 +記憶體不足,無法完成前一項操作 +AM +詳細資訊 + +青色 +AM/PM +色調 +白色 +. +飽和度 +毫秒 +時間 +其他 +清除 +PM +設定月份 +無法複製到剪貼簿 +設定日期和時間 + \ No newline at end of file