Commit e1dded82 authored by Xing Liu's avatar Xing Liu Committed by Commit Bot

Download later: Add date time picker UI.

This CL adds a date time picker dialog for download later feature. The
logic is not wired to DownloadDialogBridge yet.

Bug: 1078454
Change-Id: I543f389a5c357db49e2f478533f2a367a46a06cc
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2245932
Commit-Queue: Xing Liu <xingliu@chromium.org>
Reviewed-by: default avatarShakti Sahu <shaktisahu@chromium.org>
Reviewed-by: default avatarHesen Zhang <hesen@chromium.org>
Cr-Commit-Position: refs/heads/master@{#778508}
parent 94a7f527
......@@ -168,6 +168,7 @@ chrome_test_java_sources = [
"javatests/src/org/chromium/chrome/browser/download/ServicificationDownloadTest.java",
"javatests/src/org/chromium/chrome/browser/download/SystemDownloadNotifierTest.java",
"javatests/src/org/chromium/chrome/browser/download/TestDownloadDirectoryProvider.java",
"javatests/src/org/chromium/chrome/browser/download/dialogs/DownloadDateTimePickerDialogTest.java",
"javatests/src/org/chromium/chrome/browser/download/dialogs/DownloadLaterDialogTest.java",
"javatests/src/org/chromium/chrome/browser/download/home/DownloadActivityV2Test.java",
"javatests/src/org/chromium/chrome/browser/download/home/StubbedOfflineContentProvider.java",
......
// Copyright 2020 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.chrome.browser.download.dialogs;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.app.Activity;
import android.support.test.filters.MediumTest;
import android.view.View;
import android.widget.DatePicker;
import android.widget.TimePicker;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.chromium.base.test.util.CommandLineFlags;
import org.chromium.chrome.browser.download.R;
import org.chromium.chrome.browser.download.dialogs.DownloadDateTimePickerDialogProperties.State;
import org.chromium.chrome.browser.flags.ChromeSwitches;
import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
import org.chromium.chrome.test.ChromeTabbedActivityTestRule;
import org.chromium.content_public.browser.test.util.TestThreadUtils;
import org.chromium.ui.base.WindowAndroid;
import org.chromium.ui.modaldialog.ModalDialogManager;
import org.chromium.ui.modaldialog.ModalDialogProperties;
import org.chromium.ui.modaldialog.ModalDialogProperties.ButtonType;
import org.chromium.ui.modelutil.PropertyModel;
import java.lang.ref.WeakReference;
/**
* Test to verify download date time picker.
*/
@RunWith(ChromeJUnit4ClassRunner.class)
@CommandLineFlags.Add({ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE})
public class DownloadDateTimePickerDialogTest {
@Rule
public ChromeTabbedActivityTestRule mActivityTestRule = new ChromeTabbedActivityTestRule();
private DownloadDateTimePickerDialogCoordinator mDialog;
private PropertyModel mModel;
@Mock
private DownloadDateTimePickerDialogCoordinator.Controller mController;
@Mock
private WindowAndroid mWindowAndroid;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
mActivityTestRule.startMainActivityOnBlankPage();
TestThreadUtils.runOnUiThreadBlocking(() -> {
when(mWindowAndroid.getActivity())
.thenReturn(new WeakReference<Activity>(mActivityTestRule.getActivity()));
long now = System.currentTimeMillis();
mModel = new PropertyModel.Builder(DownloadDateTimePickerDialogProperties.ALL_KEYS)
.with(DownloadDateTimePickerDialogProperties.STATE, State.DATE)
.with(DownloadDateTimePickerDialogProperties.INITIAL_TIME, now)
.with(DownloadDateTimePickerDialogProperties.MIN_TIME, now)
.with(DownloadDateTimePickerDialogProperties.MIN_TIME,
now + DownloadDateTimePickerDialogCoordinator.MAX_TIME)
.build();
mDialog = new DownloadDateTimePickerDialogCoordinator();
Assert.assertNotNull(mController);
mDialog.initialize(mController);
});
}
private void showDialog() {
mDialog.showDialog(mWindowAndroid, mModel);
}
private ModalDialogManager getModalDialogManager() {
return mActivityTestRule.getActivity().getModalDialogManager();
}
private void clickButton(@ButtonType int type) {
PropertyModel modalDialogModel = getModalDialogManager().getCurrentDialogForTest();
modalDialogModel.get(ModalDialogProperties.CONTROLLER).onClick(modalDialogModel, type);
}
/**
* Returns the {@link DownloadDateTimePickerView}. The date time picker dialog must be showing.
*/
private DownloadDateTimePickerView getView() {
PropertyModel modalDialogModel = getModalDialogManager().getCurrentDialogForTest();
return (DownloadDateTimePickerView) (modalDialogModel.get(
ModalDialogProperties.CUSTOM_VIEW));
}
private DatePicker getDatePicker() {
return (DatePicker) (getView().findViewById(R.id.date_picker));
}
private TimePicker getTimePicker() {
return (TimePicker) (getView().findViewById(R.id.time_picker));
}
@Test
@MediumTest
public void testSelectDateAndTimeThenDestroy() {
TestThreadUtils.runOnUiThreadBlocking(() -> {
showDialog();
Assert.assertTrue(getModalDialogManager().isShowing());
Assert.assertEquals(View.VISIBLE, getDatePicker().getVisibility());
Assert.assertEquals(View.GONE, getTimePicker().getVisibility());
// Pass through the date picker.
clickButton(ButtonType.POSITIVE);
Assert.assertEquals(View.GONE, getDatePicker().getVisibility());
Assert.assertEquals(View.VISIBLE, getTimePicker().getVisibility());
// Pass through the time picker.
clickButton(ButtonType.POSITIVE);
mDialog.destroy();
// TODO(xingliu): Verify the timestamp.
verify(mController).onDateTimePicked(anyLong());
verify(mController, times(0)).onDateTimePickerCanceled();
});
}
@Test
@MediumTest
public void testCancelDateSelection() {
TestThreadUtils.runOnUiThreadBlocking(() -> {
showDialog();
// Cancel the date picker.
clickButton(ButtonType.NEGATIVE);
Assert.assertFalse(getModalDialogManager().isShowing());
verify(mController, times(0)).onDateTimePicked(anyLong());
verify(mController).onDateTimePickerCanceled();
});
}
@Test
@MediumTest
public void testCancelTimeSelection() {
TestThreadUtils.runOnUiThreadBlocking(() -> {
showDialog();
// Pass through the date picker.
clickButton(ButtonType.POSITIVE);
Assert.assertTrue(getModalDialogManager().isShowing());
// Cancel the time picker.
clickButton(ButtonType.NEGATIVE);
Assert.assertFalse(getModalDialogManager().isShowing());
verify(mController, times(0)).onDateTimePicked(anyLong());
verify(mController).onDateTimePickerCanceled();
});
}
@Test
@MediumTest
public void testShowDialogThenDestroy() {
TestThreadUtils.runOnUiThreadBlocking(() -> {
showDialog();
Assert.assertTrue(getModalDialogManager().isShowing());
mDialog.destroy();
verify(mController, times(0)).onDateTimePicked(anyLong());
verify(mController, times(0)).onDateTimePickerCanceled();
});
}
}
......@@ -21,6 +21,9 @@ android_library("java") {
"java/src/org/chromium/chrome/browser/download/MediaStoreHelper.java",
"java/src/org/chromium/chrome/browser/download/MimeUtils.java",
"java/src/org/chromium/chrome/browser/download/StringUtils.java",
"java/src/org/chromium/chrome/browser/download/dialogs/DownloadDateTimePickerDialogCoordinator.java",
"java/src/org/chromium/chrome/browser/download/dialogs/DownloadDateTimePickerDialogProperties.java",
"java/src/org/chromium/chrome/browser/download/dialogs/DownloadDateTimePickerView.java",
"java/src/org/chromium/chrome/browser/download/dialogs/DownloadLaterDialogChoice.java",
"java/src/org/chromium/chrome/browser/download/dialogs/DownloadLaterDialogController.java",
"java/src/org/chromium/chrome/browser/download/dialogs/DownloadLaterDialogCoordinator.java",
......@@ -160,6 +163,7 @@ android_resources("java_resources") {
"java/res/layout/confirm_oma_download.xml",
"java/res/layout/download_home_tabs.xml",
"java/res/layout/download_home_toolbar.xml",
"java/res/layout/download_later_date_time_picker_dialog.xml",
"java/res/layout/download_later_dialog.xml",
"java/res/layout/download_location_dialog.xml",
"java/res/layout/download_location_preference.xml",
......
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2020 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.
-->
<org.chromium.chrome.browser.download.dialogs.DownloadDateTimePickerView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content" >
<DatePicker
android:id="@+id/date_picker"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:datePickerMode="calendar"/>
<TimePicker
android:id="@+id/time_picker"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:timePickerMode="clock"/>
</org.chromium.chrome.browser.download.dialogs.DownloadDateTimePickerView>
// Copyright 2020 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.chrome.browser.download.dialogs;
import android.app.Activity;
import android.content.Context;
import android.view.LayoutInflater;
import androidx.annotation.NonNull;
import org.chromium.chrome.browser.download.R;
import org.chromium.chrome.browser.download.dialogs.DownloadDateTimePickerDialogProperties.State;
import org.chromium.ui.base.WindowAndroid;
import org.chromium.ui.modaldialog.DialogDismissalCause;
import org.chromium.ui.modaldialog.ModalDialogManager;
import org.chromium.ui.modaldialog.ModalDialogManagerHolder;
import org.chromium.ui.modaldialog.ModalDialogProperties;
import org.chromium.ui.modelutil.PropertyModel;
import org.chromium.ui.modelutil.PropertyModelChangeProcessor;
import java.util.concurrent.TimeUnit;
/**
* The coordinator for download date time picker. The user can pick an exact time to start the
* download later. The dialog has two stage:
* 1. A calendar to let the user to pick the date.
* 2. A clock to let the user to pick a time.
*/
public class DownloadDateTimePickerDialogCoordinator implements ModalDialogProperties.Controller {
/** The maximum time that the user can select in the dialog.*/
public static final long MAX_TIME = TimeUnit.DAYS.toMillis(7); /* 7 days */
/**
* The controller that receives events from the date time picker.
*/
public interface Controller {
/**
* Called when the user picked the time from date picker and time picker.
* @param time The time the user picked as a unix timestamp.
*/
void onDateTimePicked(long time);
/**
* The user canceled date time picking flow.
*/
void onDateTimePickerCanceled();
}
private Controller mController;
private ModalDialogManager mModalDialogManager;
private PropertyModel mModel;
private DownloadDateTimePickerView mView;
private PropertyModelChangeProcessor mProcessor;
/**
* Initializes the download date time picker dialog.
* @param controller The controller that receives events from the date time picker.
*/
public void initialize(@NonNull Controller controller) {
mController = controller;
}
/**
* Shows the date time picker.
* @param windowAndroid The window android handle that provides contexts.
* @param model The model that defines the application data used to update the UI view.
*/
public void showDialog(WindowAndroid windowAndroid, PropertyModel model) {
Activity activity = windowAndroid.getActivity().get();
// If the activity has gone away, just clean up the native pointer.
if (activity == null) {
onDismiss(null, DialogDismissalCause.ACTIVITY_DESTROYED);
return;
}
mModalDialogManager = ((ModalDialogManagerHolder) (activity)).getModalDialogManager();
mModel = model;
mView = (DownloadDateTimePickerView) LayoutInflater.from(activity).inflate(
R.layout.download_later_date_time_picker_dialog, null);
mProcessor = PropertyModelChangeProcessor.create(mModel, mView,
DownloadDateTimePickerView.Binder::bind, true /*performInitialBind*/);
mModalDialogManager.showDialog(
getModalDialogModel(activity), ModalDialogManager.ModalDialogType.APP);
}
/**
* Destroys the download date time picker dialog.
*/
public void destroy() {
if (mProcessor != null) mProcessor.destroy();
}
// ModalDialogProperties.Controller implementation.
@Override
public void onClick(PropertyModel model, int buttonType) {
switch (buttonType) {
case ModalDialogProperties.ButtonType.POSITIVE:
@State
int state = mModel.get(DownloadDateTimePickerDialogProperties.STATE);
if (state == State.DATE) {
mModel.set(DownloadDateTimePickerDialogProperties.STATE, State.TIME);
} else if (state == State.TIME) {
mModalDialogManager.dismissDialog(
model, DialogDismissalCause.POSITIVE_BUTTON_CLICKED);
}
break;
case ModalDialogProperties.ButtonType.NEGATIVE:
mModalDialogManager.dismissDialog(
model, DialogDismissalCause.NEGATIVE_BUTTON_CLICKED);
break;
default:
}
}
@Override
public void onDismiss(PropertyModel model, int dismissalCause) {
assert mController != null;
if (dismissalCause == DialogDismissalCause.POSITIVE_BUTTON_CLICKED) {
// TODO(xingliu): Handle edge cases that the time is before the current time.
mController.onDateTimePicked(mView.getTime());
return;
}
mController.onDateTimePickerCanceled();
}
private PropertyModel getModalDialogModel(Context context) {
assert mView != null;
return new PropertyModel.Builder(ModalDialogProperties.ALL_KEYS)
.with(ModalDialogProperties.CONTROLLER, this)
.with(ModalDialogProperties.CUSTOM_VIEW, mView)
.with(ModalDialogProperties.POSITIVE_BUTTON_TEXT, context.getResources(),
R.string.download_date_time_picker_next_text)
.with(ModalDialogProperties.NEGATIVE_BUTTON_TEXT, context.getResources(),
R.string.cancel)
.build();
}
}
// Copyright 2020 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.chrome.browser.download.dialogs;
import androidx.annotation.IntDef;
import org.chromium.ui.modelutil.PropertyKey;
import org.chromium.ui.modelutil.PropertyModel.ReadableObjectPropertyKey;
import org.chromium.ui.modelutil.PropertyModel.WritableIntPropertyKey;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* The properties for download date time picker dialog UI MVC.
*/
public class DownloadDateTimePickerDialogProperties {
/**
* The state of the date time picker dialog.
*/
@Retention(RetentionPolicy.SOURCE)
@IntDef({State.DATE, State.TIME})
public @interface State {
/** The user is picking the date in the dialog. */
int DATE = 0;
/** The user is picking the time in the dialog. */
int TIME = 1;
}
/**
* The initial date and time as a unix timestamp shown on the download date time picker dialog.
*/
public static final ReadableObjectPropertyKey<Long> INITIAL_TIME =
new ReadableObjectPropertyKey<>();
/**
* The minimum time for the user to select in the date time picker. The time to select should be
* a future time.
*/
public static final ReadableObjectPropertyKey<Long> MIN_TIME =
new ReadableObjectPropertyKey<>();
/**
* The maximum time for the user to select in the date time picker.
*/
public static final ReadableObjectPropertyKey<Long> MAX_TIME =
new ReadableObjectPropertyKey<>();
/**
* The state of the download date time picker dialog. See {@link #STATE}.
*/
public static final WritableIntPropertyKey STATE = new WritableIntPropertyKey();
public static final PropertyKey[] ALL_KEYS =
new PropertyKey[] {INITIAL_TIME, MIN_TIME, MAX_TIME, STATE};
}
// Copyright 2020 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.chrome.browser.download.dialogs;
import android.content.Context;
import android.os.Build;
import android.util.AttributeSet;
import android.widget.DatePicker;
import android.widget.LinearLayout;
import android.widget.TimePicker;
import androidx.annotation.Nullable;
import org.chromium.chrome.browser.download.R;
import org.chromium.chrome.browser.download.dialogs.DownloadDateTimePickerDialogProperties.State;
import org.chromium.ui.modelutil.PropertyKey;
import org.chromium.ui.modelutil.PropertyModel;
import java.util.Calendar;
/**
* The view for download date time picker. Contains a {@link DatePicker} and a {@link TimePicker}.
*/
public class DownloadDateTimePickerView extends LinearLayout {
/**
* The view binder to propagate events from model to view.
*/
public static class Binder {
public static void bind(
PropertyModel model, DownloadDateTimePickerView view, PropertyKey propertyKey) {
if (propertyKey == DownloadDateTimePickerDialogProperties.INITIAL_TIME) {
view.setTime(model.get(DownloadDateTimePickerDialogProperties.INITIAL_TIME));
} else if (propertyKey == DownloadDateTimePickerDialogProperties.MIN_TIME) {
view.setMinTime(model.get(DownloadDateTimePickerDialogProperties.MIN_TIME));
} else if (propertyKey == DownloadDateTimePickerDialogProperties.MAX_TIME) {
view.setMaxTime(model.get(DownloadDateTimePickerDialogProperties.MAX_TIME));
} else if (propertyKey == DownloadDateTimePickerDialogProperties.STATE) {
view.setState(model.get(DownloadDateTimePickerDialogProperties.STATE));
}
}
}
private DatePicker mDatePicker;
private TimePicker mTimePicker;
public DownloadDateTimePickerView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mDatePicker = findViewById(R.id.date_picker);
mTimePicker = findViewById(R.id.time_picker);
}
/**
* Sets the state of the date time picker dialog.
* @param state The state of the picker.
*/
void setState(@State int state) {
switch (state) {
case State.DATE:
// Show the date picker.
mDatePicker.setVisibility(VISIBLE);
mTimePicker.setVisibility(GONE);
break;
case State.TIME:
// Show the time picker.
mDatePicker.setVisibility(GONE);
mTimePicker.setVisibility(VISIBLE);
break;
default:
assert false;
}
}
/**
* Sets the initial time shown on the {@link DatePicker} and {@link TimePicker}.
* @param initialTime The initial time shown on the time picker.
*/
void setTime(long initialTime) {
Calendar calendar = Calendar.getInstance();
calendar.setTimeInMillis(initialTime);
mDatePicker.updateDate(calendar.get(Calendar.YEAR), calendar.get(Calendar.MONTH),
calendar.get(Calendar.DAY_OF_MONTH));
setHour(mTimePicker, calendar.get(Calendar.HOUR));
setMinute(mTimePicker, calendar.get(Calendar.MINUTE));
}
/**
* Gets the unix timestamp of the selected date and time.
* @return The selected time.
*/
public long getTime() {
Calendar calendar = Calendar.getInstance();
calendar.set(Calendar.YEAR, mDatePicker.getYear());
calendar.set(Calendar.MONTH, mDatePicker.getMonth());
calendar.set(Calendar.DAY_OF_MONTH, mDatePicker.getDayOfMonth());
calendar.set(Calendar.HOUR, getHour(mTimePicker));
calendar.set(Calendar.MINUTE, getMinute(mTimePicker));
return calendar.getTimeInMillis();
}
/**
* Sets the minimum time for the user to select. It is possible for the user to select a time
* before this when the user select today as the date and a time before the current time. The
* caller should safely handle this edge case.
* @param minTime The minimum time as a unix timestamp for the user to select.
*/
public void setMinTime(long minTime) {
mDatePicker.setMinDate(minTime);
}
/**
* Sets the maximum time for the user to select. It is possible for the user to select a time
* after this when the user select the maximum date and a time after the maximum time. The
* caller should safely handle this edge case, or ignore this since the error will be less than
* 24 hours.
* @param maxTime The maximum time as a unix timestamp for the user to select.
*/
public void setMaxTime(long maxTime) {
mDatePicker.setMaxDate(maxTime);
}
// TODO(xingliu): Move these to a util class and use them for all TimePicker call sites.
private static int getHour(TimePicker timePicker) {
assert (timePicker != null);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) return timePicker.getCurrentHour();
return timePicker.getHour();
}
private static int getMinute(TimePicker timePicker) {
assert (timePicker != null);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) return timePicker.getCurrentMinute();
return timePicker.getMinute();
}
private static void setHour(TimePicker timePicker, int hour) {
assert (timePicker != null);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
timePicker.setCurrentHour(hour);
return;
}
timePicker.setHour(hour);
}
private static void setMinute(TimePicker timePicker, int minute) {
assert (timePicker != null);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
timePicker.setCurrentMinute(minute);
return;
}
timePicker.setMinute(minute);
}
}
......@@ -1016,6 +1016,9 @@ Your Google account may have other forms of browsing history like searches and a
<message name="IDS_DOWNLOAD_LATER_DOWNLOAD_NOW_TEXT" desc="The text for the radio button to trigger the download now in download later dialog.">
Now
</message>
<message name="IDS_DOWNLOAD_DATE_TIME_PICKER_NEXT_TEXT" desc="The text for the positive button of the download date time picker dialog.">
Next
</message>
<message name="IDS_DOWNLOAD_LATER_ON_WIFI_TEXT" desc="The text for the radio button to trigger the download on WIFI in download later dialog.">
On Wi-Fi
</message>
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment