Commit 775ebf6c authored by Friedrich Horschig's avatar Friedrich Horschig Committed by Commit Bot

Wire the password generation to the keyboard accessory

Experimentally wire the password generation into the keyboard
accessory.
When the PasswordsKeyboardAccessory feature is enabled, the
accessory should provide an option to use the generated password.

With this CL, the popup should still be shown.

Bug: 835234

Change-Id: I5a0693430ca55d67ad8c922caacfce02ab23112a
Reviewed-on: https://chromium-review.googlesource.com/1013943
Commit-Queue: Friedrich Horschig <fhorschig@chromium.org>
Reviewed-by: default avatarBernhard Bauer <bauerb@chromium.org>
Cr-Commit-Position: refs/heads/master@{#557255}
parent 239d8abb
......@@ -13,19 +13,32 @@ import android.widget.PopupWindow;
import org.chromium.base.annotations.CalledByNative;
import org.chromium.base.annotations.JNINamespace;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.ChromeActivity;
import org.chromium.chrome.browser.ChromeFeatureList;
import org.chromium.chrome.browser.autofill.keyboard_accessory.KeyboardAccessoryCoordinator;
import org.chromium.chrome.browser.autofill.keyboard_accessory.KeyboardAccessoryData;
import org.chromium.chrome.browser.autofill.keyboard_accessory.KeyboardAccessoryData.Action;
import org.chromium.ui.DropdownPopupWindow;
import org.chromium.ui.base.WindowAndroid;
import javax.annotation.Nullable;
/**
* JNI call glue for password generation between native and Java objects.
*/
@JNINamespace("autofill")
public class PasswordGenerationPopupBridge implements AdapterView.OnItemClickListener,
PopupWindow.OnDismissListener, PasswordGenerationAdapter.Delegate {
public class PasswordGenerationPopupBridge
implements AdapterView.OnItemClickListener, PopupWindow.OnDismissListener,
PasswordGenerationAdapter.Delegate, Action.Delegate {
private final long mNativePasswordGenerationPopupViewAndroid;
private final Context mContext;
private final DropdownPopupWindow mPopup;
private final View mAnchorView;
private final KeyboardAccessoryData.PropertyProvider<Action> mActionProvider =
new KeyboardAccessoryData.PropertyProvider<>();
private @Nullable Action mKeyboardAccessoryAction;
private final @Nullable KeyboardAccessoryCoordinator mKeyboardAccessory;
/**
* A convenience method for the constructor to be invoked from the native counterpart.
* @param anchorView View anchored for popup.
......@@ -52,10 +65,17 @@ public class PasswordGenerationPopupBridge implements AdapterView.OnItemClickLis
// mContext could've been garbage collected.
if (mContext == null) {
mPopup = null;
mKeyboardAccessory = null;
// Prevent destroying the native counterpart when it's about to derefence its own
// members in UpdateBoundsAndRedrawPopup().
new Handler().post(() -> onDismiss());
new Handler().post(this::onDismiss);
} else {
if (mContext instanceof ChromeActivity) {
mKeyboardAccessory = ((ChromeActivity) mContext).getKeyboardAccessory();
mKeyboardAccessory.registerActionListProvider(mActionProvider);
} else {
mKeyboardAccessory = null;
}
mPopup = new DropdownPopupWindow(mContext, anchorView);
mPopup.setOnItemClickListener(this);
mPopup.setOnDismissListener(this);
......@@ -109,6 +129,13 @@ public class PasswordGenerationPopupBridge implements AdapterView.OnItemClickLis
String suggestionTitle, String explanationText, int explanationTextLinkRangeStart,
int explanationTextLinkRangeEnd) {
if (mPopup != null) {
// If an action can be shown in the popup, provide the same in the accessory or sheet.
if (providesGenerationAction()) {
// TODO(ioanap): Move these lines to a new native call or even a separate bridge.
createGeneratePasswordAction(suggestionTitle, this);
mActionProvider.notifyObservers(new Action[] {mKeyboardAccessoryAction});
// Don't return for now. We want to have both mechanisms in place initially.
}
float anchorWidth = mAnchorView.getLayoutParams().width;
assert anchorWidth > 0;
PasswordGenerationAdapter adapter = new PasswordGenerationAdapter(mContext, this,
......@@ -138,4 +165,32 @@ public class PasswordGenerationPopupBridge implements AdapterView.OnItemClickLis
private void hide() {
if (mPopup != null) mPopup.dismiss();
}
@Override
public void onActionTriggered(Action action) {
assert action == mKeyboardAccessoryAction;
nativePasswordSelected(mNativePasswordGenerationPopupViewAndroid);
// TODO(ioanap): Create and use this call instead to start the ModalDialog flow:
// nativePasswordGenerationRequested(mNativePasswordGenerationPopupViewAndroid);
}
private boolean providesGenerationAction() {
return mKeyboardAccessory != null && ChromeFeatureList.isInitialized()
&& ChromeFeatureList.isEnabled(ChromeFeatureList.PASSWORDS_KEYBOARD_ACCESSORY);
}
private void createGeneratePasswordAction(String caption, Action.Delegate delegate) {
assert mKeyboardAccessoryAction == null : "Accessory Action should only be created once!";
mKeyboardAccessoryAction = new Action() {
@Override
public String getCaption() {
return caption;
}
@Override
public Delegate getDelegate() {
return delegate;
}
};
}
}
......@@ -71,11 +71,12 @@ public class KeyboardAccessoryCoordinator {
}
/**
* Allows any {@link KeyboardAccessoryData.ActionListProvider} to communicate with the
* Allows any {@link KeyboardAccessoryData.Provider} to communicate with the
* {@link KeyboardAccessoryMediator} of this component.
* @param provider The object providing action lists to observers in this component.
*/
public void registerActionListProvider(KeyboardAccessoryData.ActionListProvider provider) {
public void registerActionListProvider(
KeyboardAccessoryData.Provider<KeyboardAccessoryData.Action> provider) {
provider.addObserver(mMediator);
}
......
......@@ -4,33 +4,39 @@
package org.chromium.chrome.browser.autofill.keyboard_accessory;
import java.util.ArrayList;
import java.util.List;
/**
* Interfaces in this class are used to pass data into keyboard accessory component.
*/
public class KeyboardAccessoryData {
/**
* A provider notifies all registered {@link ActionListObserver} if the list of actions
* A provider notifies all registered {@link Observer} if the list of actions
* changes.
* TODO(fhorschig): Replace with android.databinding.ObservableField if available.
* @param <T> Either an {@link Action} or a {@link Tab} that this instance provides.
*/
public interface ActionListProvider {
public interface Provider<T> {
/**
* Every observer added by this need to be notified whenever the list of action changes
* Every observer added by this need to be notified whenever the list of items changes.
* @param observer The observer to be notified.
*/
void addObserver(ActionListObserver observer);
void addObserver(Observer<T> observer);
}
/**
* An observer receives notifications from an {@link ActionListProvider} it is subscribed to.
* An observer receives notifications from an {@link Provider} it is subscribed to.
* @param <T> Either an {@link Action} or a {@link Tab} that this instance observes.
*/
public interface ActionListObserver {
public interface Observer<T> {
/**
* A provider calls this function with a list of actions that should be available in the
* A provider calls this function with a list of items that should be available in the
* keyboard accessory.
* @param actions The actions to be displayed in the Accessory. It's a native array as the
* provider is typically a bridge called via JNI which prefers native types.
*/
void onActionsAvailable(Action[] actions);
void onItemsAvailable(T[] actions);
}
/**
......@@ -60,5 +66,29 @@ public class KeyboardAccessoryData {
Delegate getDelegate();
}
/**
* A simple class that holds a list of {@link Observer}s which can be notified about new data by
* directly passing that data into {@link PropertyProvider#notifyObservers(T[])}.
* @param <T> Either {@link Action}s or {@link Tab}s provided by this class.
*/
public static class PropertyProvider<T> implements Provider<T> {
private final List<Observer<T>> mObservers = new ArrayList<>();
@Override
public void addObserver(Observer<T> observer) {
mObservers.add(observer);
}
/**
* Passes the given items to all subscribed {@link Observer}s.
* @param items The array of items to be passed to the {@link Observer}s.
*/
public void notifyObservers(T[] items) {
for (Observer<T> observer : mObservers) {
observer.onItemsAvailable(items);
}
}
}
private KeyboardAccessoryData() {}
}
......@@ -21,9 +21,9 @@ import org.chromium.ui.base.WindowAndroid;
* callback to trigger when selecting them.
*/
class KeyboardAccessoryMediator
implements KeyboardAccessoryData.ActionListObserver,
WindowAndroid.KeyboardVisibilityListener, ListObservable.ListObserver,
PropertyObservable.PropertyObserver<KeyboardAccessoryModel.PropertyKey> {
implements WindowAndroid.KeyboardVisibilityListener, ListObservable.ListObserver,
PropertyObservable.PropertyObserver<KeyboardAccessoryModel.PropertyKey>,
KeyboardAccessoryData.Observer<KeyboardAccessoryData.Action> {
private final KeyboardAccessoryModel mModel;
private final WindowAndroid mWindowAndroid;
......@@ -48,7 +48,7 @@ class KeyboardAccessoryMediator
}
@Override
public void onActionsAvailable(KeyboardAccessoryData.Action[] actions) {
public void onItemsAvailable(KeyboardAccessoryData.Action[] actions) {
mModel.setActions(actions);
}
......
......@@ -28,14 +28,12 @@ import org.robolectric.annotation.Config;
import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.base.test.util.Feature;
import org.chromium.chrome.browser.autofill.AutofillKeyboardSuggestions;
import org.chromium.chrome.browser.autofill.keyboard_accessory.KeyboardAccessoryData.Action;
import org.chromium.chrome.browser.autofill.keyboard_accessory.KeyboardAccessoryData.PropertyProvider;
import org.chromium.chrome.browser.modelutil.ListObservable;
import org.chromium.chrome.browser.modelutil.PropertyModelChangeProcessor;
import org.chromium.chrome.browser.modelutil.PropertyObservable.PropertyObserver;
import org.chromium.ui.base.WindowAndroid;
import java.util.ArrayList;
import java.util.List;
/**
* Controller tests for the keyboard accessory component.
*/
......@@ -54,27 +52,9 @@ public class KeyboardAccessoryControllerTest {
private ViewStub mMockViewStub;
@Mock
private KeyboardAccessoryView mMockView;
@Mock
private PropertyModelChangeProcessor<KeyboardAccessoryModel, KeyboardAccessoryView,
KeyboardAccessoryModel.PropertyKey> mMockModelChangeProcessor;
private class TestActionListProvider implements KeyboardAccessoryData.ActionListProvider {
private final List<KeyboardAccessoryData.ActionListObserver> mObservers = new ArrayList<>();
@Override
public void addObserver(KeyboardAccessoryData.ActionListObserver observer) {
mObservers.add(observer);
}
public void sendActionsToReceivers(KeyboardAccessoryData.Action[] actions) {
for (KeyboardAccessoryData.ActionListObserver observer : mObservers) {
observer.onActionsAvailable(actions);
}
}
}
private static class FakeTab implements KeyboardAccessoryData.Tab {}
private static class FakeAction implements KeyboardAccessoryData.Action {
private static class FakeAction implements Action {
@Override
public String getCaption() {
return null;
......@@ -151,32 +131,32 @@ public class KeyboardAccessoryControllerTest {
@SmallTest
@Feature({"keyboard-accessory"})
public void testModelNotifiesAboutActionsChangedByProvider() {
final TestActionListProvider testProvider = new TestActionListProvider();
final PropertyProvider<Action> testProvider = new PropertyProvider<>();
final FakeAction testAction = new FakeAction();
mModel.addActionListObserver(mMockActionListObserver);
mCoordinator.registerActionListProvider(testProvider);
// If the coordinator receives an initial actions, the model should report an insertion.
testProvider.sendActionsToReceivers(new KeyboardAccessoryData.Action[] {testAction});
testProvider.notifyObservers(new Action[] {testAction});
verify(mMockActionListObserver).onItemRangeInserted(mModel.getActionList(), 0, 1);
assertThat(mModel.getActionList().getItemCount(), is(1));
assertThat(mModel.getActionList().get(0), is(equalTo(testAction)));
// If the coordinator receives a new set of actions, the model should report a change.
testProvider.sendActionsToReceivers(new KeyboardAccessoryData.Action[] {testAction});
testProvider.notifyObservers(new Action[] {testAction});
verify(mMockActionListObserver)
.onItemRangeChanged(mModel.getActionList(), 0, 1, mModel.getActionList());
assertThat(mModel.getActionList().getItemCount(), is(1));
assertThat(mModel.getActionList().get(0), is(equalTo(testAction)));
// If the coordinator receives an empty set of actions, the model should report a deletion.
testProvider.sendActionsToReceivers(new KeyboardAccessoryData.Action[] {});
testProvider.notifyObservers(new Action[] {});
verify(mMockActionListObserver).onItemRangeRemoved(mModel.getActionList(), 0, 1);
assertThat(mModel.getActionList().getItemCount(), is(0));
// There should be no notification if no actions are reported repeatedly.
testProvider.sendActionsToReceivers(new KeyboardAccessoryData.Action[] {});
testProvider.notifyObservers(new Action[] {});
verifyNoMoreInteractions(mMockActionListObserver);
}
......
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