Commit d4b7aed8 authored by Nicolas Dossou-gbete's avatar Nicolas Dossou-gbete Committed by Commit Bot

Reland "[Suggestions] Remove CTAs when feature is disabled"

This is a reland of 139a79ec
Original change's description:
> [Suggestions] Remove CTAs when feature is disabled
>
> Stops showing the Sign in Promo and the All Dismissed item when remote
> suggestions are disabled and performing the advertised actions would not
> provide the user with new suggestions.
>
> Part of this CL involves making the SuggestionsSources notifies a list
> of observer rather than a single one.
>
> Bug: 738872
> Change-Id: I16c300fb14a1d5780578387d48d848570339befe
> Reviewed-on: https://chromium-review.googlesource.com/567182
> Reviewed-by: Bernhard Bauer <bauerb@chromium.org>
> Commit-Queue: Nicolas Dossou-Gbété <dgn@chromium.org>
> Cr-Commit-Position: refs/heads/master@{#486034}

Bug: 738872
Change-Id: Ifc5f6643b4402657b9a62e08596d9e3267ce79a8
Reviewed-on: https://chromium-review.googlesource.com/571722Reviewed-by: default avatarBernhard Bauer <bauerb@chromium.org>
Commit-Queue: Nicolas Dossou-Gbété <dgn@chromium.org>
Cr-Commit-Position: refs/heads/master@{#486723}
parent 7ff93d98
...@@ -32,7 +32,8 @@ public class ActionItem extends OptionalLeaf { ...@@ -32,7 +32,8 @@ public class ActionItem extends OptionalLeaf {
mCategoryInfo = section.getCategoryInfo(); mCategoryInfo = section.getCategoryInfo();
mParentSection = section; mParentSection = section;
mSuggestionsRanker = ranker; mSuggestionsRanker = ranker;
setVisible(mCategoryInfo.getAdditionalAction() != ContentSuggestionsAdditionalAction.NONE); setVisibilityInternal(
mCategoryInfo.getAdditionalAction() != ContentSuggestionsAdditionalAction.NONE);
} }
@Override @Override
......
...@@ -40,6 +40,10 @@ public class AllDismissedItem extends OptionalLeaf { ...@@ -40,6 +40,10 @@ public class AllDismissedItem extends OptionalLeaf {
visitor.visitAllDismissedItem(); visitor.visitAllDismissedItem();
} }
public void setVisible(boolean visible) {
setVisibilityInternal(visible);
}
/** /**
* ViewHolder for an item of type {@link ItemViewType#ALL_DISMISSED}. * ViewHolder for an item of type {@link ItemViewType#ALL_DISMISSED}.
*/ */
......
...@@ -37,6 +37,10 @@ public class Footer extends OptionalLeaf { ...@@ -37,6 +37,10 @@ public class Footer extends OptionalLeaf {
visitor.visitFooter(); visitor.visitFooter();
} }
public void setVisible(boolean visible) {
setVisibilityInternal(visible);
}
/** /**
* The {@code ViewHolder} for the {@link Footer}. * The {@code ViewHolder} for the {@link Footer}.
*/ */
......
...@@ -15,9 +15,14 @@ import org.chromium.base.VisibleForTesting; ...@@ -15,9 +15,14 @@ import org.chromium.base.VisibleForTesting;
import org.chromium.chrome.browser.ChromeFeatureList; import org.chromium.chrome.browser.ChromeFeatureList;
import org.chromium.chrome.browser.ntp.ContextMenuManager; import org.chromium.chrome.browser.ntp.ContextMenuManager;
import org.chromium.chrome.browser.ntp.cards.NewTabPageViewHolder.PartialBindCallback; import org.chromium.chrome.browser.ntp.cards.NewTabPageViewHolder.PartialBindCallback;
import org.chromium.chrome.browser.ntp.snippets.CategoryInt;
import org.chromium.chrome.browser.ntp.snippets.CategoryStatus;
import org.chromium.chrome.browser.ntp.snippets.SectionHeaderViewHolder; import org.chromium.chrome.browser.ntp.snippets.SectionHeaderViewHolder;
import org.chromium.chrome.browser.ntp.snippets.SnippetArticleViewHolder; import org.chromium.chrome.browser.ntp.snippets.SnippetArticleViewHolder;
import org.chromium.chrome.browser.ntp.snippets.SnippetsBridge;
import org.chromium.chrome.browser.ntp.snippets.SuggestionsSource;
import org.chromium.chrome.browser.offlinepages.OfflinePageBridge; import org.chromium.chrome.browser.offlinepages.OfflinePageBridge;
import org.chromium.chrome.browser.suggestions.DestructionObserver;
import org.chromium.chrome.browser.suggestions.SuggestionsRecyclerView; import org.chromium.chrome.browser.suggestions.SuggestionsRecyclerView;
import org.chromium.chrome.browser.suggestions.SuggestionsUiDelegate; import org.chromium.chrome.browser.suggestions.SuggestionsUiDelegate;
import org.chromium.chrome.browser.suggestions.TileGrid; import org.chromium.chrome.browser.suggestions.TileGrid;
...@@ -103,6 +108,9 @@ public class NewTabPageAdapter extends Adapter<NewTabPageViewHolder> implements ...@@ -103,6 +108,9 @@ public class NewTabPageAdapter extends Adapter<NewTabPageViewHolder> implements
mRoot.addChild(mBottomSpacer); mRoot.addChild(mBottomSpacer);
} }
RemoteSuggestionsStatusObserver suggestionsObserver = new RemoteSuggestionsStatusObserver();
mUiDelegate.addDestructionObserver(suggestionsObserver);
updateAllDismissedVisibility(); updateAllDismissedVisibility();
mRoot.setParent(this); mRoot.setParent(this);
} }
...@@ -222,7 +230,8 @@ public class NewTabPageAdapter extends Adapter<NewTabPageViewHolder> implements ...@@ -222,7 +230,8 @@ public class NewTabPageAdapter extends Adapter<NewTabPageViewHolder> implements
} }
private void updateAllDismissedVisibility() { private void updateAllDismissedVisibility() {
boolean showAllDismissed = hasAllBeenDismissed(); boolean showAllDismissed = hasAllBeenDismissed()
&& mUiDelegate.getSuggestionsSource().areRemoteSuggestionsEnabled();
mAllDismissed.setVisible(showAllDismissed); mAllDismissed.setVisible(showAllDismissed);
mFooter.setVisible(!showAllDismissed); mFooter.setVisible(!showAllDismissed);
} }
...@@ -309,4 +318,24 @@ public class NewTabPageAdapter extends Adapter<NewTabPageViewHolder> implements ...@@ -309,4 +318,24 @@ public class NewTabPageAdapter extends Adapter<NewTabPageViewHolder> implements
InnerNode getRootForTesting() { InnerNode getRootForTesting() {
return mRoot; return mRoot;
} }
private class RemoteSuggestionsStatusObserver
extends SuggestionsSource.EmptyObserver implements DestructionObserver {
public RemoteSuggestionsStatusObserver() {
mUiDelegate.getSuggestionsSource().addObserver(this);
}
@Override
public void onCategoryStatusChanged(
@CategoryInt int category, @CategoryStatus int newStatus) {
if (!SnippetsBridge.isCategoryRemote(category)) return;
updateAllDismissedVisibility();
}
@Override
public void onDestroy() {
mUiDelegate.getSuggestionsSource().removeObserver(this);
}
}
} }
...@@ -67,7 +67,7 @@ public abstract class OptionalLeaf extends ChildNode { ...@@ -67,7 +67,7 @@ public abstract class OptionalLeaf extends ChildNode {
* initially considered hidden. * initially considered hidden.
*/ */
@CallSuper @CallSuper
public void setVisible(boolean visible) { protected void setVisibilityInternal(boolean visible) {
if (mVisible == visible) return; if (mVisible == visible) return;
mVisible = visible; mVisible = visible;
......
...@@ -28,4 +28,8 @@ class ProgressItem extends OptionalLeaf { ...@@ -28,4 +28,8 @@ class ProgressItem extends OptionalLeaf {
protected void visitOptionalItem(NodeVisitor visitor) { protected void visitOptionalItem(NodeVisitor visitor) {
visitor.visitProgressItem(); visitor.visitProgressItem();
} }
public void setVisible(boolean visible) {
setVisibilityInternal(visible);
}
} }
...@@ -43,7 +43,7 @@ public class SectionList ...@@ -43,7 +43,7 @@ public class SectionList
public SectionList(SuggestionsUiDelegate uiDelegate, OfflinePageBridge offlinePageBridge) { public SectionList(SuggestionsUiDelegate uiDelegate, OfflinePageBridge offlinePageBridge) {
mUiDelegate = uiDelegate; mUiDelegate = uiDelegate;
mUiDelegate.getSuggestionsSource().setObserver(this); mUiDelegate.getSuggestionsSource().addObserver(this);
mOfflinePageBridge = offlinePageBridge; mOfflinePageBridge = offlinePageBridge;
mUiDelegate.addDestructionObserver(new DestructionObserver() { mUiDelegate.addDestructionObserver(new DestructionObserver() {
......
...@@ -15,6 +15,10 @@ import org.chromium.base.VisibleForTesting; ...@@ -15,6 +15,10 @@ import org.chromium.base.VisibleForTesting;
import org.chromium.base.metrics.RecordUserAction; import org.chromium.base.metrics.RecordUserAction;
import org.chromium.chrome.R; import org.chromium.chrome.R;
import org.chromium.chrome.browser.ntp.ContextMenuManager; import org.chromium.chrome.browser.ntp.ContextMenuManager;
import org.chromium.chrome.browser.ntp.snippets.CategoryInt;
import org.chromium.chrome.browser.ntp.snippets.CategoryStatus;
import org.chromium.chrome.browser.ntp.snippets.SnippetsBridge;
import org.chromium.chrome.browser.ntp.snippets.SuggestionsSource;
import org.chromium.chrome.browser.preferences.ChromePreferenceManager; import org.chromium.chrome.browser.preferences.ChromePreferenceManager;
import org.chromium.chrome.browser.signin.AccountSigninActivity; import org.chromium.chrome.browser.signin.AccountSigninActivity;
import org.chromium.chrome.browser.signin.SigninAccessPoint; import org.chromium.chrome.browser.signin.SigninAccessPoint;
...@@ -39,23 +43,37 @@ public class SignInPromo extends OptionalLeaf ...@@ -39,23 +43,37 @@ public class SignInPromo extends OptionalLeaf
*/ */
private boolean mDismissed; private boolean mDismissed;
/**
* Whether the signin status means that the user has the possibility to sign in.
*/
private boolean mCanSignIn;
/**
* Whether personalized suggestions can be shown. If it's not the case, we have no reason to
* offer the user to sign in.
*/
private boolean mCanShowPersonalizedSuggestions;
private final ImpressionTracker mImpressionTracker = new ImpressionTracker(null, this); private final ImpressionTracker mImpressionTracker = new ImpressionTracker(null, this);
@Nullable @Nullable
private final SigninObserver mObserver; private final SigninObserver mSigninObserver;
public SignInPromo(SuggestionsUiDelegate uiDelegate) { public SignInPromo(SuggestionsUiDelegate uiDelegate) {
mDismissed = ChromePreferenceManager.getInstance().getNewTabPageSigninPromoDismissed(); mDismissed = ChromePreferenceManager.getInstance().getNewTabPageSigninPromoDismissed();
SuggestionsSource suggestionsSource = uiDelegate.getSuggestionsSource();
SigninManager signinManager = SigninManager.get(ContextUtils.getApplicationContext()); SigninManager signinManager = SigninManager.get(ContextUtils.getApplicationContext());
if (mDismissed) { if (mDismissed) {
mObserver = null; mSigninObserver = null;
} else { } else {
mObserver = new SigninObserver(signinManager); mSigninObserver = new SigninObserver(signinManager, suggestionsSource);
uiDelegate.addDestructionObserver(mObserver); uiDelegate.addDestructionObserver(mSigninObserver);
} }
setVisible(signinManager.isSignInAllowed() && !signinManager.isSignedInOnNative()); mCanSignIn = signinManager.isSignInAllowed() && !signinManager.isSignedInOnNative();
mCanShowPersonalizedSuggestions = suggestionsSource.areRemoteSuggestionsEnabled();
updateVisibility();
} }
@Override @Override
...@@ -70,7 +88,7 @@ public class SignInPromo extends OptionalLeaf ...@@ -70,7 +88,7 @@ public class SignInPromo extends OptionalLeaf
*/ */
@Nullable @Nullable
public DestructionObserver getObserver() { public DestructionObserver getObserver() {
return mObserver; return mSigninObserver;
} }
@Override @Override
...@@ -114,9 +132,8 @@ public class SignInPromo extends OptionalLeaf ...@@ -114,9 +132,8 @@ public class SignInPromo extends OptionalLeaf
mImpressionTracker.reset(null); mImpressionTracker.reset(null);
} }
@Override private void updateVisibility() {
public void setVisible(boolean visible) { setVisibilityInternal(!mDismissed && mCanSignIn && mCanShowPersonalizedSuggestions);
super.setVisible(!mDismissed && visible);
} }
@Override @Override
...@@ -127,26 +144,31 @@ public class SignInPromo extends OptionalLeaf ...@@ -127,26 +144,31 @@ public class SignInPromo extends OptionalLeaf
/** Hides the sign in promo and sets a preference to make sure it is not shown again. */ /** Hides the sign in promo and sets a preference to make sure it is not shown again. */
@Override @Override
public void dismiss(Callback<String> itemRemovedCallback) { public void dismiss(Callback<String> itemRemovedCallback) {
assert mSigninObserver != null;
mDismissed = true; mDismissed = true;
setVisible(false); updateVisibility();
ChromePreferenceManager.getInstance().setNewTabPageSigninPromoDismissed(true); ChromePreferenceManager.getInstance().setNewTabPageSigninPromoDismissed(true);
mObserver.unregister(); mSigninObserver.unregister();
itemRemovedCallback.onResult(ContextUtils.getApplicationContext().getString(getHeader())); itemRemovedCallback.onResult(ContextUtils.getApplicationContext().getString(getHeader()));
} }
@VisibleForTesting @VisibleForTesting
class SigninObserver class SigninObserver extends SuggestionsSource.EmptyObserver
implements SignInStateObserver, SignInAllowedObserver, DestructionObserver { implements SignInStateObserver, SignInAllowedObserver, DestructionObserver {
private final SigninManager mSigninManager; private final SigninManager mSigninManager;
private final SuggestionsSource mSuggestionsSource;
/** Guards {@link #unregister()}, which can be called multiple times. */ /** Guards {@link #unregister()}, which can be called multiple times. */
private boolean mUnregistered; private boolean mUnregistered;
private SigninObserver(SigninManager signinManager) { private SigninObserver(SigninManager signinManager, SuggestionsSource suggestionsSource) {
mSigninManager = signinManager; mSigninManager = signinManager;
mSigninManager.addSignInAllowedObserver(this); mSigninManager.addSignInAllowedObserver(this);
mSigninManager.addSignInStateObserver(this); mSigninManager.addSignInStateObserver(this);
mSuggestionsSource = suggestionsSource;
mSuggestionsSource.addObserver(this);
} }
private void unregister() { private void unregister() {
...@@ -155,6 +177,8 @@ public class SignInPromo extends OptionalLeaf ...@@ -155,6 +177,8 @@ public class SignInPromo extends OptionalLeaf
mSigninManager.removeSignInAllowedObserver(this); mSigninManager.removeSignInAllowedObserver(this);
mSigninManager.removeSignInStateObserver(this); mSigninManager.removeSignInStateObserver(this);
mSuggestionsSource.removeObserver(this);
} }
@Override @Override
...@@ -167,17 +191,31 @@ public class SignInPromo extends OptionalLeaf ...@@ -167,17 +191,31 @@ public class SignInPromo extends OptionalLeaf
// Listening to onSignInAllowedChanged is important for the FRE. Sign in is not allowed // Listening to onSignInAllowedChanged is important for the FRE. Sign in is not allowed
// until it is completed, but the NTP is initialised before the FRE is even shown. By // until it is completed, but the NTP is initialised before the FRE is even shown. By
// implementing this we can show the promo if the user did not sign in during the FRE. // implementing this we can show the promo if the user did not sign in during the FRE.
setVisible(mSigninManager.isSignInAllowed()); mCanSignIn = mSigninManager.isSignInAllowed();
updateVisibility();
} }
@Override @Override
public void onSignedIn() { public void onSignedIn() {
setVisible(false); mCanSignIn = false;
updateVisibility();
} }
@Override @Override
public void onSignedOut() { public void onSignedOut() {
setVisible(true); mCanSignIn = mSigninManager.isSignInAllowed();
updateVisibility();
}
@Override
public void onCategoryStatusChanged(
@CategoryInt int category, @CategoryStatus int newStatus) {
if (!SnippetsBridge.isCategoryRemote(category)) return;
// Checks whether the category is enabled first to avoid unnecessary calls across JNI.
mCanShowPersonalizedSuggestions = SnippetsBridge.isCategoryEnabled(category)
|| mSuggestionsSource.areRemoteSuggestionsEnabled();
updateVisibility();
} }
} }
......
...@@ -63,4 +63,8 @@ public abstract class StatusItem extends OptionalLeaf implements StatusCardViewH ...@@ -63,4 +63,8 @@ public abstract class StatusItem extends OptionalLeaf implements StatusCardViewH
assert holder instanceof StatusCardViewHolder; assert holder instanceof StatusCardViewHolder;
((StatusCardViewHolder) holder).onBindViewHolder(this); ((StatusCardViewHolder) holder).onBindViewHolder(this);
} }
public void setVisible(boolean visible) {
setVisibilityInternal(visible);
}
} }
...@@ -18,7 +18,7 @@ public class SectionHeader extends OptionalLeaf { ...@@ -18,7 +18,7 @@ public class SectionHeader extends OptionalLeaf {
public SectionHeader(String headerText) { public SectionHeader(String headerText) {
this.mHeaderText = headerText; this.mHeaderText = headerText;
setVisible(true); setVisibilityInternal(true);
} }
@Override @Override
...@@ -41,4 +41,8 @@ public class SectionHeader extends OptionalLeaf { ...@@ -41,4 +41,8 @@ public class SectionHeader extends OptionalLeaf {
public void visitOptionalItem(NodeVisitor visitor) { public void visitOptionalItem(NodeVisitor visitor) {
visitor.visitHeader(); visitor.visitHeader();
} }
public void setVisible(boolean visible) {
setVisibilityInternal(visible);
}
} }
...@@ -7,6 +7,7 @@ package org.chromium.chrome.browser.ntp.snippets; ...@@ -7,6 +7,7 @@ package org.chromium.chrome.browser.ntp.snippets;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import org.chromium.base.Callback; import org.chromium.base.Callback;
import org.chromium.base.ObserverList;
import org.chromium.base.annotations.CalledByNative; import org.chromium.base.annotations.CalledByNative;
import org.chromium.chrome.browser.ntp.cards.SuggestionsCategoryInfo; import org.chromium.chrome.browser.ntp.cards.SuggestionsCategoryInfo;
import org.chromium.chrome.browser.profiles.Profile; import org.chromium.chrome.browser.profiles.Profile;
...@@ -22,13 +23,17 @@ public class SnippetsBridge implements SuggestionsSource { ...@@ -22,13 +23,17 @@ public class SnippetsBridge implements SuggestionsSource {
private static final String TAG = "SnippetsBridge"; private static final String TAG = "SnippetsBridge";
private long mNativeSnippetsBridge; private long mNativeSnippetsBridge;
private SuggestionsSource.Observer mObserver; private final ObserverList<Observer> mObserverList = new ObserverList<>();
public static boolean isCategoryStatusAvailable(@CategoryStatus int status) { public static boolean isCategoryStatusAvailable(@CategoryStatus int status) {
// Note: This code is duplicated in content_suggestions_category_status.cc. // Note: This code is duplicated in category_status.cc.
return status == CategoryStatus.AVAILABLE_LOADING || status == CategoryStatus.AVAILABLE; return status == CategoryStatus.AVAILABLE_LOADING || status == CategoryStatus.AVAILABLE;
} }
public static boolean isCategoryRemote(@CategoryInt int category) {
return category > KnownCategories.REMOTE_CATEGORIES_OFFSET;
}
/** Returns whether the category is considered "enabled", and can show content suggestions. */ /** Returns whether the category is considered "enabled", and can show content suggestions. */
public static boolean isCategoryEnabled(@CategoryStatus int status) { public static boolean isCategoryEnabled(@CategoryStatus int status) {
switch (status) { switch (status) {
...@@ -58,7 +63,7 @@ public class SnippetsBridge implements SuggestionsSource { ...@@ -58,7 +63,7 @@ public class SnippetsBridge implements SuggestionsSource {
assert mNativeSnippetsBridge != 0; assert mNativeSnippetsBridge != 0;
nativeDestroy(mNativeSnippetsBridge); nativeDestroy(mNativeSnippetsBridge);
mNativeSnippetsBridge = 0; mNativeSnippetsBridge = 0;
mObserver = null; mObserverList.clear();
} }
/** /**
...@@ -79,8 +84,9 @@ public class SnippetsBridge implements SuggestionsSource { ...@@ -79,8 +84,9 @@ public class SnippetsBridge implements SuggestionsSource {
nativeSetRemoteSuggestionsEnabled(enabled); nativeSetRemoteSuggestionsEnabled(enabled);
} }
public static boolean areRemoteSuggestionsEnabled() { @Override
return nativeAreRemoteSuggestionsEnabled(); public boolean areRemoteSuggestionsEnabled() {
return nativeAreRemoteSuggestionsEnabled(mNativeSnippetsBridge);
} }
public static boolean areRemoteSuggestionsManaged() { public static boolean areRemoteSuggestionsManaged() {
...@@ -171,9 +177,14 @@ public class SnippetsBridge implements SuggestionsSource { ...@@ -171,9 +177,14 @@ public class SnippetsBridge implements SuggestionsSource {
} }
@Override @Override
public void setObserver(Observer observer) { public void addObserver(Observer observer) {
assert observer != null; assert observer != null;
mObserver = observer; mObserverList.addObserver(observer);
}
@Override
public void removeObserver(Observer observer) {
mObserverList.removeObserver(observer);
} }
@Override @Override
...@@ -227,22 +238,26 @@ public class SnippetsBridge implements SuggestionsSource { ...@@ -227,22 +238,26 @@ public class SnippetsBridge implements SuggestionsSource {
@CalledByNative @CalledByNative
private void onNewSuggestions(@CategoryInt int category) { private void onNewSuggestions(@CategoryInt int category) {
if (mObserver != null) mObserver.onNewSuggestions(category); for (Observer observer : mObserverList) observer.onNewSuggestions(category);
} }
@CalledByNative @CalledByNative
private void onCategoryStatusChanged(@CategoryInt int category, @CategoryStatus int newStatus) { private void onCategoryStatusChanged(@CategoryInt int category, @CategoryStatus int newStatus) {
if (mObserver != null) mObserver.onCategoryStatusChanged(category, newStatus); for (Observer observer : mObserverList) {
observer.onCategoryStatusChanged(category, newStatus);
}
} }
@CalledByNative @CalledByNative
private void onSuggestionInvalidated(@CategoryInt int category, String idWithinCategory) { private void onSuggestionInvalidated(@CategoryInt int category, String idWithinCategory) {
if (mObserver != null) mObserver.onSuggestionInvalidated(category, idWithinCategory); for (Observer observer : mObserverList) {
observer.onSuggestionInvalidated(category, idWithinCategory);
}
} }
@CalledByNative @CalledByNative
private void onFullRefreshRequired() { private void onFullRefreshRequired() {
if (mObserver != null) mObserver.onFullRefreshRequired(); for (Observer observer : mObserverList) observer.onFullRefreshRequired();
} }
private native long nativeInit(Profile profile); private native long nativeInit(Profile profile);
...@@ -251,7 +266,7 @@ public class SnippetsBridge implements SuggestionsSource { ...@@ -251,7 +266,7 @@ public class SnippetsBridge implements SuggestionsSource {
private static native void nativeRemoteSuggestionsSchedulerOnPersistentSchedulerWakeUp(); private static native void nativeRemoteSuggestionsSchedulerOnPersistentSchedulerWakeUp();
private static native void nativeRemoteSuggestionsSchedulerOnBrowserUpgraded(); private static native void nativeRemoteSuggestionsSchedulerOnBrowserUpgraded();
private static native void nativeSetRemoteSuggestionsEnabled(boolean enabled); private static native void nativeSetRemoteSuggestionsEnabled(boolean enabled);
private static native boolean nativeAreRemoteSuggestionsEnabled(); private native boolean nativeAreRemoteSuggestionsEnabled(long nativeNTPSnippetsBridge);
private static native boolean nativeAreRemoteSuggestionsManaged(); private static native boolean nativeAreRemoteSuggestionsManaged();
private static native boolean nativeAreRemoteSuggestionsManagedByCustodian(); private static native boolean nativeAreRemoteSuggestionsManagedByCustodian();
private static native void nativeSetContentSuggestionsNotificationsEnabled(boolean enabled); private static native void nativeSetContentSuggestionsNotificationsEnabled(boolean enabled);
......
...@@ -47,6 +47,11 @@ public interface SuggestionsSource { ...@@ -47,6 +47,11 @@ public interface SuggestionsSource {
*/ */
void fetchRemoteSuggestions(); void fetchRemoteSuggestions();
/**
* @return Whether remote suggestions are enabled.
*/
boolean areRemoteSuggestionsEnabled();
/** /**
* Gets the categories in the order in which they should be displayed. * Gets the categories in the order in which they should be displayed.
* @return The categories. * @return The categories.
...@@ -125,5 +130,26 @@ public interface SuggestionsSource { ...@@ -125,5 +130,26 @@ public interface SuggestionsSource {
/** /**
* Sets the recipient for update events from the source. * Sets the recipient for update events from the source.
*/ */
void setObserver(Observer observer); void addObserver(Observer observer);
/**
* Removes an observer. Is no-op if the observer was not already registered.
*/
void removeObserver(Observer observer);
/** No-op implementation of {@link SuggestionsSource.Observer}. */
class EmptyObserver implements Observer {
@Override
public void onNewSuggestions(@CategoryInt int category) {}
@Override
public void onCategoryStatusChanged(
@CategoryInt int category, @CategoryStatus int newStatus) {}
@Override
public void onSuggestionInvalidated(@CategoryInt int category, String idWithinCategory) {}
@Override
public void onFullRefreshRequired() {}
}
} }
...@@ -23,7 +23,9 @@ import org.chromium.chrome.browser.ntp.ContentSuggestionsNotificationHelper; ...@@ -23,7 +23,9 @@ import org.chromium.chrome.browser.ntp.ContentSuggestionsNotificationHelper;
import org.chromium.chrome.browser.ntp.snippets.ContentSuggestionsNotificationAction; import org.chromium.chrome.browser.ntp.snippets.ContentSuggestionsNotificationAction;
import org.chromium.chrome.browser.ntp.snippets.ContentSuggestionsNotificationOptOut; import org.chromium.chrome.browser.ntp.snippets.ContentSuggestionsNotificationOptOut;
import org.chromium.chrome.browser.ntp.snippets.SnippetsBridge; import org.chromium.chrome.browser.ntp.snippets.SnippetsBridge;
import org.chromium.chrome.browser.ntp.snippets.SuggestionsSource;
import org.chromium.chrome.browser.profiles.Profile; import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.suggestions.SuggestionsDependencyFactory;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
...@@ -74,7 +76,12 @@ public class ContentSuggestionsPreferences extends PreferenceFragment { ...@@ -74,7 +76,12 @@ public class ContentSuggestionsPreferences extends PreferenceFragment {
setHasOptionsMenu(true); setHasOptionsMenu(true);
finishSwitchInitialisation(); finishSwitchInitialisation();
boolean isEnabled = SnippetsBridge.areRemoteSuggestionsEnabled(); SuggestionsSource suggestionsSource =
SuggestionsDependencyFactory.getInstance().createSuggestionSource(
Profile.getLastUsedProfile());
boolean isEnabled = suggestionsSource.areRemoteSuggestionsEnabled();
suggestionsSource.destroy();
mIsEnabled = !isEnabled; // Opposite so that we trigger side effects below. mIsEnabled = !isEnabled; // Opposite so that we trigger side effects below.
updatePreferences(isEnabled); updatePreferences(isEnabled);
......
...@@ -66,7 +66,7 @@ public class TileGrid extends OptionalLeaf implements TileGroup.Observer { ...@@ -66,7 +66,7 @@ public class TileGrid extends OptionalLeaf implements TileGroup.Observer {
@Override @Override
public void onTileDataChanged() { public void onTileDataChanged() {
setVisible(mTileGroup.getTiles().length != 0); setVisibilityInternal(mTileGroup.getTiles().length != 0);
if (isVisible()) notifyItemChanged(0, new ViewHolder.UpdateTilesCallback(mTileGroup)); if (isVisible()) notifyItemChanged(0, new ViewHolder.UpdateTilesCallback(mTileGroup));
} }
......
...@@ -11,7 +11,6 @@ import static org.junit.Assert.assertTrue; ...@@ -11,7 +11,6 @@ import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.reset; import static org.mockito.Mockito.reset;
...@@ -63,6 +62,7 @@ import org.chromium.chrome.browser.ntp.snippets.CategoryInt; ...@@ -63,6 +62,7 @@ import org.chromium.chrome.browser.ntp.snippets.CategoryInt;
import org.chromium.chrome.browser.ntp.snippets.CategoryStatus; import org.chromium.chrome.browser.ntp.snippets.CategoryStatus;
import org.chromium.chrome.browser.ntp.snippets.KnownCategories; import org.chromium.chrome.browser.ntp.snippets.KnownCategories;
import org.chromium.chrome.browser.ntp.snippets.SnippetArticle; import org.chromium.chrome.browser.ntp.snippets.SnippetArticle;
import org.chromium.chrome.browser.ntp.snippets.SuggestionsSource;
import org.chromium.chrome.browser.offlinepages.OfflinePageBridge; import org.chromium.chrome.browser.offlinepages.OfflinePageBridge;
import org.chromium.chrome.browser.preferences.ChromePreferenceManager; import org.chromium.chrome.browser.preferences.ChromePreferenceManager;
import org.chromium.chrome.browser.signin.SigninManager; import org.chromium.chrome.browser.signin.SigninManager;
...@@ -80,6 +80,7 @@ import org.chromium.chrome.test.util.browser.suggestions.FakeSuggestionsSource; ...@@ -80,6 +80,7 @@ import org.chromium.chrome.test.util.browser.suggestions.FakeSuggestionsSource;
import org.chromium.testing.local.LocalRobolectricTestRunner; import org.chromium.testing.local.LocalRobolectricTestRunner;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
...@@ -254,10 +255,7 @@ public class NewTabPageAdapterTest { ...@@ -254,10 +255,7 @@ public class NewTabPageAdapterTest {
mSource.setInfoForCategory( mSource.setInfoForCategory(
TEST_CATEGORY, new CategoryInfoBuilder(TEST_CATEGORY).showIfEmpty().build()); TEST_CATEGORY, new CategoryInfoBuilder(TEST_CATEGORY).showIfEmpty().build());
when(mUiDelegate.getSuggestionsSource()).thenReturn(mSource); resetUiDelegate();
when(mUiDelegate.getEventReporter()).thenReturn(mock(SuggestionsEventReporter.class));
when(mUiDelegate.getSuggestionsRanker()).thenReturn(mock(SuggestionsRanker.class));
reloadNtp(); reloadNtp();
} }
...@@ -892,28 +890,26 @@ public class NewTabPageAdapterTest { ...@@ -892,28 +890,26 @@ public class NewTabPageAdapterTest {
@Test @Test
@Feature({"Ntp"}) @Feature({"Ntp"})
public void testSigninPromo() { public void testSigninPromo() {
@CategoryInt
final int remoteCategory = KnownCategories.REMOTE_CATEGORIES_OFFSET + TEST_CATEGORY;
when(mMockSigninManager.isSignInAllowed()).thenReturn(true); when(mMockSigninManager.isSignInAllowed()).thenReturn(true);
when(mMockSigninManager.isSignedInOnNative()).thenReturn(false); when(mMockSigninManager.isSignedInOnNative()).thenReturn(false);
ArgumentCaptor<DestructionObserver> observers = mSource.setRemoteSuggestionsEnabled(true);
ArgumentCaptor.forClass(DestructionObserver.class); resetUiDelegate();
doNothing().when(mUiDelegate).addDestructionObserver(observers.capture());
reloadNtp(); reloadNtp();
assertTrue(isSignInPromoVisible()); assertTrue(isSignInPromoVisible());
// Note: As currently implemented, these two variables should point to the same object, a // Note: As currently implemented, these variables should point to the same object, a
// SignInPromo.SigninObserver // SignInPromo.SigninObserver
SignInStateObserver signInStateObserver = null; List<DestructionObserver> observers = getDestructionObserver(mUiDelegate);
SignInAllowedObserver signInAllowedObserver = null; SignInStateObserver signInStateObserver =
for (DestructionObserver observer : observers.getAllValues()) { findFirstInstanceOf(observers, SignInStateObserver.class);
if (observer instanceof SignInStateObserver) { SignInAllowedObserver signInAllowedObserver =
signInStateObserver = (SignInStateObserver) observer; findFirstInstanceOf(observers, SignInAllowedObserver.class);
} SuggestionsSource.Observer suggestionsObserver =
if (observer instanceof SignInAllowedObserver) { findFirstInstanceOf(observers, SuggestionsSource.Observer.class);
signInAllowedObserver = (SignInAllowedObserver) observer;
}
}
signInStateObserver.onSignedIn(); signInStateObserver.onSignedIn();
assertFalse(isSignInPromoVisible()); assertFalse(isSignInPromoVisible());
...@@ -928,6 +924,15 @@ public class NewTabPageAdapterTest { ...@@ -928,6 +924,15 @@ public class NewTabPageAdapterTest {
when(mMockSigninManager.isSignInAllowed()).thenReturn(true); when(mMockSigninManager.isSignInAllowed()).thenReturn(true);
signInAllowedObserver.onSignInAllowedChanged(); signInAllowedObserver.onSignInAllowedChanged();
assertTrue(isSignInPromoVisible()); assertTrue(isSignInPromoVisible());
mSource.setRemoteSuggestionsEnabled(false);
suggestionsObserver.onCategoryStatusChanged(
remoteCategory, CategoryStatus.CATEGORY_EXPLICITLY_DISABLED);
assertFalse(isSignInPromoVisible());
mSource.setRemoteSuggestionsEnabled(true);
suggestionsObserver.onCategoryStatusChanged(remoteCategory, CategoryStatus.AVAILABLE);
assertTrue(isSignInPromoVisible());
} }
@Test @Test
...@@ -960,17 +965,8 @@ public class NewTabPageAdapterTest { ...@@ -960,17 +965,8 @@ public class NewTabPageAdapterTest {
@Test @Test
@Feature({"Ntp"}) @Feature({"Ntp"})
public void testAllDismissedVisibility() { public void testAllDismissedVisibility() {
ArgumentCaptor<DestructionObserver> observers = SigninObserver signinObserver =
ArgumentCaptor.forClass(DestructionObserver.class); findFirstInstanceOf(getDestructionObserver(mUiDelegate), SigninObserver.class);
verify(mUiDelegate, atLeastOnce()).addDestructionObserver(observers.capture());
SigninObserver signinObserver = null;
for (DestructionObserver observer : observers.getAllValues()) {
if (observer instanceof SigninObserver) {
signinObserver = (SigninObserver) observer;
}
}
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
Callback<String> itemDismissedCallback = mock(Callback.class); Callback<String> itemDismissedCallback = mock(Callback.class);
...@@ -1045,8 +1041,28 @@ public class NewTabPageAdapterTest { ...@@ -1045,8 +1041,28 @@ public class NewTabPageAdapterTest {
assertEquals(RecyclerView.NO_POSITION, assertEquals(RecyclerView.NO_POSITION,
mAdapter.getFirstPositionForType(ItemViewType.ALL_DISMISSED)); mAdapter.getFirstPositionForType(ItemViewType.ALL_DISMISSED));
// Disabling remote suggestions should remove both the promo and the AllDismissed item
mSource.setRemoteSuggestionsEnabled(false);
signinObserver.onCategoryStatusChanged(
KnownCategories.REMOTE_CATEGORIES_OFFSET + TEST_CATEGORY,
CategoryStatus.CATEGORY_EXPLICITLY_DISABLED);
// Adapter content:
// Idx | Item
// ----|--------------------
// 0 | Above-the-fold
// 1 | Footer
// 2 | Spacer
assertEquals(ItemViewType.FOOTER, mAdapter.getItemViewType(1));
assertEquals(RecyclerView.NO_POSITION,
mAdapter.getFirstPositionForType(ItemViewType.ALL_DISMISSED));
assertEquals(
RecyclerView.NO_POSITION, mAdapter.getFirstPositionForType(ItemViewType.PROMO));
// Prepare some suggestions. They should not load because the category is dismissed on // Prepare some suggestions. They should not load because the category is dismissed on
// the current NTP. // the current NTP.
mSource.setRemoteSuggestionsEnabled(true);
signinObserver.onCategoryStatusChanged(
KnownCategories.REMOTE_CATEGORIES_OFFSET + TEST_CATEGORY, CategoryStatus.AVAILABLE);
mSource.setStatusForCategory(TEST_CATEGORY, CategoryStatus.AVAILABLE); mSource.setStatusForCategory(TEST_CATEGORY, CategoryStatus.AVAILABLE);
mSource.setSuggestionsForCategory(TEST_CATEGORY, createDummySuggestions(1, TEST_CATEGORY)); mSource.setSuggestionsForCategory(TEST_CATEGORY, createDummySuggestions(1, TEST_CATEGORY));
mSource.setInfoForCategory(TEST_CATEGORY, new CategoryInfoBuilder(TEST_CATEGORY).build()); mSource.setInfoForCategory(TEST_CATEGORY, new CategoryInfoBuilder(TEST_CATEGORY).build());
...@@ -1137,6 +1153,13 @@ public class NewTabPageAdapterTest { ...@@ -1137,6 +1153,13 @@ public class NewTabPageAdapterTest {
return new SectionDescriptor(Collections.<SnippetArticle>emptyList()); return new SectionDescriptor(Collections.<SnippetArticle>emptyList());
} }
private void resetUiDelegate() {
reset(mUiDelegate);
when(mUiDelegate.getSuggestionsSource()).thenReturn(mSource);
when(mUiDelegate.getEventReporter()).thenReturn(mock(SuggestionsEventReporter.class));
when(mUiDelegate.getSuggestionsRanker()).thenReturn(mock(SuggestionsRanker.class));
}
private void reloadNtp() { private void reloadNtp() {
mAdapter = new NewTabPageAdapter(mUiDelegate, mock(View.class), makeUiConfig(), mAdapter = new NewTabPageAdapter(mUiDelegate, mock(View.class), makeUiConfig(),
mOfflinePageBridge, mock(ContextMenuManager.class), /* tileGroupDelegate = mOfflinePageBridge, mock(ContextMenuManager.class), /* tileGroupDelegate =
...@@ -1151,4 +1174,25 @@ public class NewTabPageAdapterTest { ...@@ -1151,4 +1174,25 @@ public class NewTabPageAdapterTest {
private int getCategory(TreeNode item) { private int getCategory(TreeNode item) {
return ((SuggestionsSection) item).getCategory(); return ((SuggestionsSection) item).getCategory();
} }
/**
* Note: Currently the observers need to be re-registered to be returned again if this method
* has been called, as it relies on argument captors that don't repeatedly capture individual
* calls.
* @return The currently registered destruction observers.
*/
private List<DestructionObserver> getDestructionObserver(SuggestionsUiDelegate delegate) {
ArgumentCaptor<DestructionObserver> observers =
ArgumentCaptor.forClass(DestructionObserver.class);
verify(delegate, atLeastOnce()).addDestructionObserver(observers.capture());
return observers.getAllValues();
}
@SuppressWarnings("unchecked")
private static <T> T findFirstInstanceOf(Collection<?> collection, Class<T> clazz) {
for (Object item : collection) {
if (clazz.isAssignableFrom(item.getClass())) return (T) item;
}
return null;
}
} }
...@@ -160,18 +160,6 @@ static void SetRemoteSuggestionsEnabled(JNIEnv* env, ...@@ -160,18 +160,6 @@ static void SetRemoteSuggestionsEnabled(JNIEnv* env,
content_suggestions_service->SetRemoteSuggestionsEnabled(enabled); content_suggestions_service->SetRemoteSuggestionsEnabled(enabled);
} }
static jboolean AreRemoteSuggestionsEnabled(
JNIEnv* env,
const JavaParamRef<jclass>& caller) {
ntp_snippets::ContentSuggestionsService* content_suggestions_service =
ContentSuggestionsServiceFactory::GetForProfile(
ProfileManager::GetLastUsedProfile());
if (!content_suggestions_service)
return false;
return content_suggestions_service->AreRemoteSuggestionsEnabled();
}
// Returns true if the remote provider is managed by an adminstrator's policy. // Returns true if the remote provider is managed by an adminstrator's policy.
static jboolean AreRemoteSuggestionsManaged( static jboolean AreRemoteSuggestionsManaged(
JNIEnv* env, JNIEnv* env,
...@@ -285,6 +273,12 @@ ScopedJavaLocalRef<jobject> NTPSnippetsBridge::GetSuggestionsForCategory( ...@@ -285,6 +273,12 @@ ScopedJavaLocalRef<jobject> NTPSnippetsBridge::GetSuggestionsForCategory(
content_suggestions_service_->GetSuggestionsForCategory(category)); content_suggestions_service_->GetSuggestionsForCategory(category));
} }
jboolean NTPSnippetsBridge::AreRemoteSuggestionsEnabled(
JNIEnv* env,
const JavaParamRef<jobject>& obj) {
return content_suggestions_service_->AreRemoteSuggestionsEnabled();
}
void NTPSnippetsBridge::FetchSuggestionImage( void NTPSnippetsBridge::FetchSuggestionImage(
JNIEnv* env, JNIEnv* env,
const JavaParamRef<jobject>& obj, const JavaParamRef<jobject>& obj,
......
...@@ -53,6 +53,10 @@ class NTPSnippetsBridge ...@@ -53,6 +53,10 @@ class NTPSnippetsBridge
const base::android::JavaParamRef<jobject>& obj, const base::android::JavaParamRef<jobject>& obj,
jint j_category_id); jint j_category_id);
jboolean AreRemoteSuggestionsEnabled(
JNIEnv* env,
const base::android::JavaParamRef<jobject>& obj);
void FetchSuggestionImage( void FetchSuggestionImage(
JNIEnv* env, JNIEnv* env,
const base::android::JavaParamRef<jobject>& obj, const base::android::JavaParamRef<jobject>& obj,
......
...@@ -9,6 +9,7 @@ import static org.chromium.chrome.test.util.browser.suggestions.ContentSuggestio ...@@ -9,6 +9,7 @@ import static org.chromium.chrome.test.util.browser.suggestions.ContentSuggestio
import android.graphics.Bitmap; import android.graphics.Bitmap;
import org.chromium.base.Callback; import org.chromium.base.Callback;
import org.chromium.base.ObserverList;
import org.chromium.base.ThreadUtils; import org.chromium.base.ThreadUtils;
import org.chromium.chrome.browser.ntp.cards.SuggestionsCategoryInfo; import org.chromium.chrome.browser.ntp.cards.SuggestionsCategoryInfo;
import org.chromium.chrome.browser.ntp.snippets.CategoryInt; import org.chromium.chrome.browser.ntp.snippets.CategoryInt;
...@@ -28,7 +29,7 @@ import java.util.Map; ...@@ -28,7 +29,7 @@ import java.util.Map;
* A fake Suggestions source for use in unit and instrumentation tests. * A fake Suggestions source for use in unit and instrumentation tests.
*/ */
public class FakeSuggestionsSource implements SuggestionsSource { public class FakeSuggestionsSource implements SuggestionsSource {
private SuggestionsSource.Observer mObserver; private ObserverList<Observer> mObserverList = new ObserverList<>();
private final List<Integer> mCategories = new ArrayList<>(); private final List<Integer> mCategories = new ArrayList<>();
private final Map<Integer, List<SnippetArticle>> mSuggestions = new HashMap<>(); private final Map<Integer, List<SnippetArticle>> mSuggestions = new HashMap<>();
private final Map<Integer, Integer> mCategoryStatus = new LinkedHashMap<>(); private final Map<Integer, Integer> mCategoryStatus = new LinkedHashMap<>();
...@@ -45,6 +46,9 @@ public class FakeSuggestionsSource implements SuggestionsSource { ...@@ -45,6 +46,9 @@ public class FakeSuggestionsSource implements SuggestionsSource {
new HashMap<>(); new HashMap<>();
private final Map<Integer, Integer> mDismissedCategoryStatus = new LinkedHashMap<>(); private final Map<Integer, Integer> mDismissedCategoryStatus = new LinkedHashMap<>();
private final Map<Integer, SuggestionsCategoryInfo> mDismissedCategoryInfo = new HashMap<>(); private final Map<Integer, SuggestionsCategoryInfo> mDismissedCategoryInfo = new HashMap<>();
private boolean mRemoteSuggestionsEnabled = true;
/** /**
* Sets the status to be returned for a given category. * Sets the status to be returned for a given category.
*/ */
...@@ -55,7 +59,7 @@ public class FakeSuggestionsSource implements SuggestionsSource { ...@@ -55,7 +59,7 @@ public class FakeSuggestionsSource implements SuggestionsSource {
} else if (!mCategories.contains(category)) { } else if (!mCategories.contains(category)) {
mCategories.add(category); mCategories.add(category);
} }
if (mObserver != null) mObserver.onCategoryStatusChanged(category, status); for (Observer observer : mObserverList) observer.onCategoryStatusChanged(category, status);
} }
/** /**
...@@ -65,7 +69,7 @@ public class FakeSuggestionsSource implements SuggestionsSource { ...@@ -65,7 +69,7 @@ public class FakeSuggestionsSource implements SuggestionsSource {
@CategoryInt int category, List<SnippetArticle> suggestions) { @CategoryInt int category, List<SnippetArticle> suggestions) {
// Copy the suggestions list so that it can't be modified anymore. // Copy the suggestions list so that it can't be modified anymore.
mSuggestions.put(category, new ArrayList<>(suggestions)); mSuggestions.put(category, new ArrayList<>(suggestions));
if (mObserver != null) mObserver.onNewSuggestions(category); for (Observer observer : mObserverList) observer.onNewSuggestions(category);
} }
/** /**
...@@ -136,14 +140,16 @@ public class FakeSuggestionsSource implements SuggestionsSource { ...@@ -136,14 +140,16 @@ public class FakeSuggestionsSource implements SuggestionsSource {
break; break;
} }
} }
mObserver.onSuggestionInvalidated(category, idWithinCategory); for (Observer observer : mObserverList) {
observer.onSuggestionInvalidated(category, idWithinCategory);
}
} }
/** /**
* Notifies the observer that a full refresh is required. * Notifies the observer that a full refresh is required.
*/ */
public void fireFullRefreshRequired() { public void fireFullRefreshRequired() {
mObserver.onFullRefreshRequired(); for (Observer observer : mObserverList) observer.onFullRefreshRequired();
} }
/** /**
...@@ -159,6 +165,15 @@ public class FakeSuggestionsSource implements SuggestionsSource { ...@@ -159,6 +165,15 @@ public class FakeSuggestionsSource implements SuggestionsSource {
@Override @Override
public void fetchRemoteSuggestions() {} public void fetchRemoteSuggestions() {}
@Override
public boolean areRemoteSuggestionsEnabled() {
return mRemoteSuggestionsEnabled;
}
public void setRemoteSuggestionsEnabled(boolean enabled) {
mRemoteSuggestionsEnabled = enabled;
}
@Override @Override
public void dismissSuggestion(SnippetArticle suggestion) { public void dismissSuggestion(SnippetArticle suggestion) {
for (List<SnippetArticle> suggestions : mSuggestions.values()) { for (List<SnippetArticle> suggestions : mSuggestions.values()) {
...@@ -229,8 +244,13 @@ public class FakeSuggestionsSource implements SuggestionsSource { ...@@ -229,8 +244,13 @@ public class FakeSuggestionsSource implements SuggestionsSource {
} }
@Override @Override
public void setObserver(Observer observer) { public void addObserver(Observer observer) {
mObserver = observer; mObserverList.addObserver(observer);
}
@Override
public void removeObserver(Observer observer) {
mObserverList.removeObserver(observer);
} }
@Override @Override
......
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