Add a plus button to the mobile pref on Network & internet page

On the Network & internet page, we have a "Mobile network" pref that in
single-SIM mode leads to a detail page for the current SIM. In multi-SIM
mode it has more complicated behavior (leading either to details about
the current subscription if there is only one, or a list of
subscriptions if more than one).

One of the things we wanted to add was a shortcut to add another eSIM
subscription. So this CL adds a plus button control on the right of the
preference which leads to a flow to add another mobile subscription via
the eSIM manager.

Bug: 116349402
Test: make RunSettingsRoboTests
Change-Id: I38e0031e3bd603e93c45dcb4557750e7bc1b8b5a
diff --git a/res/layout/preference_widget_add.xml b/res/layout/preference_widget_add.xml
new file mode 100644
index 0000000..6e33a4d
--- /dev/null
+++ b/res/layout/preference_widget_add.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright (C) 2019 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<ImageView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/add_preference_widget"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:layout_gravity="center"
+    android:paddingStart="?android:attr/listPreferredItemPaddingEnd"
+    android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+    android:background="?android:attr/selectableItemBackground"
+    android:scaleType="center"
+    android:src="@drawable/ic_add_24dp"
+    android:contentDescription="@string/add" />
+
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 36fba35..29cdffa 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -658,6 +658,8 @@
     <string name="apply">Apply</string>
     <!-- Button label for generic share action [CHAR LIMIT=20] -->
     <string name="share">Share</string>
+    <!-- Button label for generic add action [CHAR LIMIT=20] -->
+    <string name="add">Add</string>
 
     <!-- Title of the Settings activity shown within the application itself. -->
     <string name="settings_label">Settings</string>
diff --git a/res/xml/mobile_network_list.xml b/res/xml/mobile_network_list.xml
index 81704e5..5bffa2f 100644
--- a/res/xml/mobile_network_list.xml
+++ b/res/xml/mobile_network_list.xml
@@ -23,6 +23,8 @@
         android:key="add_more"
         android:title="@string/mobile_network_list_add_more"
         android:icon="@drawable/ic_menu_add"
-        android:order="100" />
+        android:order="100" >
+        <intent android:action="android.telephony.euicc.action.PROVISION_EMBEDDED_SUBSCRIPTION" />
+    </Preference>
 
 </PreferenceScreen>
diff --git a/res/xml/network_and_internet_v2.xml b/res/xml/network_and_internet_v2.xml
index 83499d7..fd29c41 100644
--- a/res/xml/network_and_internet_v2.xml
+++ b/res/xml/network_and_internet_v2.xml
@@ -39,7 +39,7 @@
             android:targetClass="Settings$WifiSettingsActivity" />
     </com.android.settings.widget.MasterSwitchPreference>
 
-    <com.android.settingslib.RestrictedPreference
+    <com.android.settings.widget.AddPreference
         android:key="mobile_network_list"
         android:title="@string/network_settings_title"
         android:summary="@string/summary_placeholder"
diff --git a/src/com/android/settings/network/MobileNetworkSummaryController.java b/src/com/android/settings/network/MobileNetworkSummaryController.java
index cc15676..fff1fea 100644
--- a/src/com/android/settings/network/MobileNetworkSummaryController.java
+++ b/src/com/android/settings/network/MobileNetworkSummaryController.java
@@ -16,6 +16,8 @@
 
 package com.android.settings.network;
 
+import static android.telephony.TelephonyManager.MultiSimVariants.UNKNOWN;
+
 import static androidx.lifecycle.Lifecycle.Event.ON_PAUSE;
 import static androidx.lifecycle.Lifecycle.Event.ON_RESUME;
 
@@ -23,6 +25,15 @@
 import android.content.Intent;
 import android.telephony.SubscriptionInfo;
 import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyManager;
+import android.telephony.euicc.EuiccManager;
+
+import com.android.settings.R;
+import com.android.settings.network.telephony.MobileNetworkActivity;
+import com.android.settings.widget.AddPreference;
+import com.android.settingslib.core.AbstractPreferenceController;
+
+import java.util.List;
 
 import androidx.lifecycle.Lifecycle;
 import androidx.lifecycle.LifecycleObserver;
@@ -30,12 +41,6 @@
 import androidx.preference.Preference;
 import androidx.preference.PreferenceScreen;
 
-import com.android.settings.R;
-import com.android.settings.network.telephony.MobileNetworkActivity;
-import com.android.settingslib.core.AbstractPreferenceController;
-
-import java.util.List;
-
 public class MobileNetworkSummaryController extends AbstractPreferenceController implements
         SubscriptionsChangeListener.SubscriptionsChangeListenerClient, LifecycleObserver {
     private static final String TAG = "MobileNetSummaryCtlr";
@@ -44,7 +49,8 @@
 
     private SubscriptionManager mSubscriptionManager;
     private SubscriptionsChangeListener mChangeListener;
-    private PreferenceScreen mScreen;
+    private TelephonyManager mTelephonyMgr;
+    private AddPreference mPreference;
 
     /**
      * This controls the summary text and click behavior of the "Mobile network" item on the
@@ -64,6 +70,7 @@
     public MobileNetworkSummaryController(Context context, Lifecycle lifecycle) {
         super(context);
         mSubscriptionManager = context.getSystemService(SubscriptionManager.class);
+        mTelephonyMgr = mContext.getSystemService(TelephonyManager.class);
         mChangeListener = new SubscriptionsChangeListener(context, this);
         lifecycle.addObserver(this);
     }
@@ -82,7 +89,7 @@
     @Override
     public void displayPreference(PreferenceScreen screen) {
         super.displayPreference(screen);
-        mScreen = screen;
+        mPreference = screen.findPreference(getPreferenceKey());
     }
 
     @Override
@@ -100,29 +107,51 @@
         }
     }
 
-    private void update() {
-        if (mScreen != null) {
-            final Preference preference = mScreen.findPreference(getPreferenceKey());
-            refreshSummary(preference);
-            final List<SubscriptionInfo> subs = SubscriptionUtil.getAvailableSubscriptions(
-                    mSubscriptionManager);
+    private void startAddSimFlow() {
+        final Intent intent = new Intent(EuiccManager.ACTION_PROVISION_EMBEDDED_SUBSCRIPTION);
+        mContext.startActivity(intent);
+    }
 
-            preference.setOnPreferenceClickListener(null);
-            preference.setFragment(null);
-            if (subs.size() == 0) {
-                preference.setOnPreferenceClickListener((Preference pref) -> {
-                    // TODO(asargent) - need to get correct intent to fire here
-                    return true;
-                });
-            } else if (subs.size() == 1) {
-                preference.setOnPreferenceClickListener((Preference pref) -> {
-                    final Intent intent = new Intent(mContext, MobileNetworkActivity.class);
-                    mContext.startActivity(intent);
-                    return true;
-                });
+    private boolean shouldEnableAddButton() {
+        // The add button should only show up if the device is in multi-sim mode.
+        return mTelephonyMgr.getMultiSimConfiguration() != UNKNOWN;
+    }
+
+    private void update() {
+        if (mPreference == null) {
+            return;
+        }
+        final boolean enableAddButton = shouldEnableAddButton();
+        refreshSummary(mPreference);
+        if (!enableAddButton) {
+            mPreference.setOnAddClickListener(null);
+        } else {
+            mPreference.setOnAddClickListener(p -> {
+                startAddSimFlow();
+            });
+        }
+        final List<SubscriptionInfo> subs = SubscriptionUtil.getAvailableSubscriptions(
+                mSubscriptionManager);
+        mPreference.setOnPreferenceClickListener(null);
+        mPreference.setFragment(null);
+        mPreference.setEnabled(true);
+        if (subs.isEmpty()) {
+            if (enableAddButton) {
+                mPreference.setEnabled(false);
             } else {
-                preference.setFragment(MobileNetworkListFragment.class.getCanonicalName());
+                mPreference.setOnPreferenceClickListener((Preference pref) -> {
+                    startAddSimFlow();
+                    return true;
+                });
             }
+        } else if (subs.size() == 1) {
+            mPreference.setOnPreferenceClickListener((Preference pref) -> {
+                final Intent intent = new Intent(mContext, MobileNetworkActivity.class);
+                mContext.startActivity(intent);
+                return true;
+            });
+        } else {
+            mPreference.setFragment(MobileNetworkListFragment.class.getCanonicalName());
         }
     }
 
@@ -142,6 +171,7 @@
 
     @Override
     public void onSubscriptionsChanged() {
+        refreshSummary(mPreference);
         update();
     }
 }
diff --git a/src/com/android/settings/widget/AddPreference.java b/src/com/android/settings/widget/AddPreference.java
new file mode 100644
index 0000000..ce36ab3
--- /dev/null
+++ b/src/com/android/settings/widget/AddPreference.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.widget;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.View;
+
+import com.android.settings.R;
+import com.android.settingslib.RestrictedPreference;
+
+import androidx.annotation.VisibleForTesting;
+import androidx.preference.PreferenceViewHolder;
+
+/**
+ * A preference with a plus button on the side representing an "add" action. The plus button will
+ * only be visible when a non-null click listener is registered.
+ */
+public class AddPreference extends RestrictedPreference implements View.OnClickListener {
+
+    private OnAddClickListener mListener;
+    private View mWidgetFrame;
+
+    public AddPreference(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    @VisibleForTesting
+    int getAddWidgetResId() {
+        return R.id.add_preference_widget;
+    }
+
+    /** Sets a listener for clicks on the plus button. Passing null will cause the button to be
+     * hidden. */
+    public void setOnAddClickListener(OnAddClickListener listener) {
+        mListener = listener;
+       if (mWidgetFrame != null) {
+           mWidgetFrame.setVisibility(shouldHideSecondTarget() ? View.GONE : View.VISIBLE);
+       }
+    }
+
+    @Override
+    protected int getSecondTargetResId() {
+        return R.layout.preference_widget_add;
+    }
+
+    @Override
+    protected boolean shouldHideSecondTarget() {
+        return mListener == null;
+    }
+
+    @Override
+    public void onBindViewHolder(PreferenceViewHolder holder) {
+        super.onBindViewHolder(holder);
+        mWidgetFrame = holder.findViewById(android.R.id.widget_frame);
+        final View addWidget = holder.findViewById(getAddWidgetResId());
+        addWidget.setEnabled(true);
+        addWidget.setOnClickListener(this);
+    }
+
+    @Override
+    public void onClick(View view) {
+        if (view.getId() == getAddWidgetResId() && mListener != null) {
+            mListener.onAddClick(this);
+        }
+    }
+
+    public interface OnAddClickListener {
+        void onAddClick(AddPreference p);
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/network/MobileNetworkSummaryControllerTest.java b/tests/robotests/src/com/android/settings/network/MobileNetworkSummaryControllerTest.java
index f0c012d..6a1abd1 100644
--- a/tests/robotests/src/com/android/settings/network/MobileNetworkSummaryControllerTest.java
+++ b/tests/robotests/src/com/android/settings/network/MobileNetworkSummaryControllerTest.java
@@ -16,10 +16,16 @@
 
 package com.android.settings.network;
 
+import static android.telephony.TelephonyManager.MultiSimVariants.DSDS;
+import static android.telephony.TelephonyManager.MultiSimVariants.UNKNOWN;
+
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.isNull;
+import static org.mockito.ArgumentMatchers.notNull;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
@@ -28,8 +34,11 @@
 import android.content.Context;
 import android.content.Intent;
 import android.telephony.SubscriptionInfo;
+import android.telephony.TelephonyManager;
+import android.telephony.euicc.EuiccManager;
 
 import com.android.settings.network.telephony.MobileNetworkActivity;
+import com.android.settings.widget.AddPreference;
 
 import org.junit.After;
 import org.junit.Before;
@@ -44,7 +53,6 @@
 import java.util.Arrays;
 
 import androidx.lifecycle.Lifecycle;
-import androidx.preference.Preference;
 import androidx.preference.PreferenceScreen;
 
 @RunWith(RobolectricTestRunner.class)
@@ -52,10 +60,12 @@
     @Mock
     private Lifecycle mLifecycle;
     @Mock
-    PreferenceScreen mPreferenceScreen;
+    private TelephonyManager mTelephonyManager;
 
-    Preference mPreference;
+    @Mock
+    private PreferenceScreen mPreferenceScreen;
 
+    private AddPreference mPreference;
     private Context mContext;
     private MobileNetworkSummaryController mController;
 
@@ -63,8 +73,11 @@
     public void setUp() {
         MockitoAnnotations.initMocks(this);
         mContext = spy(Robolectric.setupActivity(Activity.class));
+        when(mContext.getSystemService(eq(TelephonyManager.class))).thenReturn(mTelephonyManager);
+        when(mTelephonyManager.getMultiSimConfiguration()).thenReturn(UNKNOWN);
+
         mController = new MobileNetworkSummaryController(mContext, mLifecycle);
-        mPreference = new Preference(mContext);
+        mPreference = spy(new AddPreference(mContext, null));
         mPreference.setKey(mController.getPreferenceKey());
         when(mPreferenceScreen.findPreference(eq(mController.getPreferenceKey()))).thenReturn(
                 mPreference);
@@ -76,15 +89,21 @@
     }
 
     @Test
-    public void getSummary_noSubscriptions_correctSummary() {
+    public void getSummary_noSubscriptions_correctSummaryAndClickHandler() {
         mController.displayPreference(mPreferenceScreen);
         mController.onResume();
         assertThat(mController.getSummary()).isEqualTo("Add a network");
+
+        mPreference.getOnPreferenceClickListener().onPreferenceClick(mPreference);
+        final ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class);
+        verify(mContext).startActivity(intentCaptor.capture());
+        assertThat(intentCaptor.getValue().getAction()).isEqualTo(
+                EuiccManager.ACTION_PROVISION_EMBEDDED_SUBSCRIPTION);
     }
 
     @Test
     public void getSummary_oneSubscription_correctSummaryAndClickHandler() {
-        SubscriptionInfo sub1 = mock(SubscriptionInfo.class);
+        final SubscriptionInfo sub1 = mock(SubscriptionInfo.class);
         when(sub1.getSubscriptionId()).thenReturn(1);
         when(sub1.getDisplayName()).thenReturn("sub1");
         SubscriptionUtil.setAvailableSubscriptionsForTesting(Arrays.asList(sub1));
@@ -93,7 +112,7 @@
         assertThat(mController.getSummary()).isEqualTo("sub1");
         assertThat(mPreference.getFragment()).isNull();
         mPreference.getOnPreferenceClickListener().onPreferenceClick(mPreference);
-        ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class);
+        final ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class);
         verify(mContext).startActivity(intentCaptor.capture());
         assertThat(intentCaptor.getValue().getComponent().getClassName()).isEqualTo(
                 MobileNetworkActivity.class.getName());
@@ -101,8 +120,8 @@
 
     @Test
     public void getSummary_twoSubscriptions_correctSummaryAndFragment() {
-        SubscriptionInfo sub1 = mock(SubscriptionInfo.class);
-        SubscriptionInfo sub2 = mock(SubscriptionInfo.class);
+        final SubscriptionInfo sub1 = mock(SubscriptionInfo.class);
+        final SubscriptionInfo sub2 = mock(SubscriptionInfo.class);
         when(sub1.getSubscriptionId()).thenReturn(1);
         when(sub2.getSubscriptionId()).thenReturn(2);
 
@@ -115,8 +134,8 @@
 
     @Test
     public void getSummaryAfterUpdate_twoSubscriptionsBecomesOne_correctSummaryAndFragment() {
-        SubscriptionInfo sub1 = mock(SubscriptionInfo.class);
-        SubscriptionInfo sub2 = mock(SubscriptionInfo.class);
+        final SubscriptionInfo sub1 = mock(SubscriptionInfo.class);
+        final SubscriptionInfo sub2 = mock(SubscriptionInfo.class);
         when(sub1.getSubscriptionId()).thenReturn(1);
         when(sub2.getSubscriptionId()).thenReturn(2);
         when(sub1.getDisplayName()).thenReturn("sub1");
@@ -135,7 +154,7 @@
         assertThat(mController.getSummary()).isEqualTo("sub1");
         assertThat(mPreference.getFragment()).isNull();
         mPreference.getOnPreferenceClickListener().onPreferenceClick(mPreference);
-        ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class);
+        final ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class);
         verify(mContext).startActivity(intentCaptor.capture());
         assertThat(intentCaptor.getValue().getComponent().getClassName()).isEqualTo(
                 MobileNetworkActivity.class.getName());
@@ -143,8 +162,8 @@
 
     @Test
     public void getSummaryAfterUpdate_oneSubscriptionBecomesTwo_correctSummaryAndFragment() {
-        SubscriptionInfo sub1 = mock(SubscriptionInfo.class);
-        SubscriptionInfo sub2 = mock(SubscriptionInfo.class);
+        final SubscriptionInfo sub1 = mock(SubscriptionInfo.class);
+        final SubscriptionInfo sub2 = mock(SubscriptionInfo.class);
         when(sub1.getSubscriptionId()).thenReturn(1);
         when(sub2.getSubscriptionId()).thenReturn(2);
         when(sub1.getDisplayName()).thenReturn("sub1");
@@ -156,7 +175,7 @@
         assertThat(mController.getSummary()).isEqualTo("sub1");
         assertThat(mPreference.getFragment()).isNull();
         mPreference.getOnPreferenceClickListener().onPreferenceClick(mPreference);
-        ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class);
+        final ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class);
         verify(mContext).startActivity(intentCaptor.capture());
         assertThat(intentCaptor.getValue().getComponent().getClassName()).isEqualTo(
                 MobileNetworkActivity.class.getName());
@@ -168,4 +187,53 @@
         assertThat(mController.getSummary()).isEqualTo("2 SIMs");
         assertThat(mPreference.getFragment()).isEqualTo(MobileNetworkListFragment.class.getName());
     }
+
+    @Test
+    public void addButton_noSubscriptionsSingleSimMode_noAddClickListener() {
+        mController.displayPreference(mPreferenceScreen);
+        mController.onResume();
+        verify(mPreference, never()).setOnAddClickListener(notNull());
+    }
+
+    @Test
+    public void addButton_oneSubscriptionSingleSimMode_noAddClickListener() {
+        final SubscriptionInfo sub1 = mock(SubscriptionInfo.class);
+        SubscriptionUtil.setAvailableSubscriptionsForTesting(Arrays.asList(sub1));
+        mController.displayPreference(mPreferenceScreen);
+        mController.onResume();
+        verify(mPreference, never()).setOnAddClickListener(notNull());
+    }
+
+    @Test
+    public void addButton_noSubscriptionsMultiSimMode_hasAddClickListenerAndPrefDisabled() {
+        when(mTelephonyManager.getMultiSimConfiguration()).thenReturn(DSDS);
+        mController.displayPreference(mPreferenceScreen);
+        mController.onResume();
+        assertThat(mPreference.isEnabled()).isFalse();
+        verify(mPreference, never()).setOnAddClickListener(isNull());
+        verify(mPreference).setOnAddClickListener(notNull());
+    }
+
+    @Test
+    public void addButton_oneSubscriptionMultiSimMode_hasAddClickListener() {
+        when(mTelephonyManager.getMultiSimConfiguration()).thenReturn(DSDS);
+        final SubscriptionInfo sub1 = mock(SubscriptionInfo.class);
+        SubscriptionUtil.setAvailableSubscriptionsForTesting(Arrays.asList(sub1));
+        mController.displayPreference(mPreferenceScreen);
+        mController.onResume();
+        verify(mPreference, never()).setOnAddClickListener(isNull());
+        verify(mPreference).setOnAddClickListener(notNull());
+    }
+
+    @Test
+    public void addButton_twoSubscriptionsMultiSimMode_hasAddClickListener() {
+        when(mTelephonyManager.getMultiSimConfiguration()).thenReturn(DSDS);
+        final SubscriptionInfo sub1 = mock(SubscriptionInfo.class);
+        final SubscriptionInfo sub2 = mock(SubscriptionInfo.class);
+        SubscriptionUtil.setAvailableSubscriptionsForTesting(Arrays.asList(sub1, sub2));
+        mController.displayPreference(mPreferenceScreen);
+        mController.onResume();
+        verify(mPreference, never()).setOnAddClickListener(isNull());
+        verify(mPreference).setOnAddClickListener(notNull());
+    }
 }
diff --git a/tests/robotests/src/com/android/settings/widget/AddPreferenceTest.java b/tests/robotests/src/com/android/settings/widget/AddPreferenceTest.java
new file mode 100644
index 0000000..3fccb34
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/widget/AddPreferenceTest.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.widget;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.view.View;
+
+import com.android.settings.R;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+
+import androidx.preference.PreferenceViewHolder;
+
+@RunWith(RobolectricTestRunner.class)
+public class AddPreferenceTest {
+
+    private Context mContext;
+    private PreferenceViewHolder mViewHolder;
+    private View mWidgetFrame;
+    private View mAddWidget;
+    private AddPreference mPreference;
+
+    @Before
+    public void setUp() {
+        mContext = RuntimeEnvironment.application;
+        mPreference = new AddPreference(mContext, null);
+
+        final View view = spy(View.inflate(mContext, mPreference.getLayoutResource(), null));
+        mViewHolder = PreferenceViewHolder.createInstanceForTests(view);
+        mWidgetFrame = view.findViewById(android.R.id.widget_frame);
+        mAddWidget = spy(View.inflate(mContext, mPreference.getSecondTargetResId(), null));
+        when(mViewHolder.findViewById(mPreference.getAddWidgetResId())).thenReturn(mAddWidget);
+    }
+
+    @Test
+    public void onBindViewHolder_noListener_addButtonNotVisible() {
+        mPreference.onBindViewHolder(mViewHolder);
+        assertThat(mPreference.shouldHideSecondTarget()).isTrue();
+        assertThat(mWidgetFrame.getVisibility()).isEqualTo(View.GONE);
+    }
+
+    @Test
+    public void onBindViewHolder_hasListener_addButtonVisible() {
+        mPreference.setOnAddClickListener(p -> {});
+        mPreference.onBindViewHolder(mViewHolder);
+        assertThat(mPreference.shouldHideSecondTarget()).isFalse();
+        assertThat(mWidgetFrame.getVisibility()).isEqualTo(View.VISIBLE);
+    }
+
+    @Test
+    public void setOnAddClickListener_listenerAddedAfterBinding_addButtonBecomesVisible() {
+        mPreference.onBindViewHolder(mViewHolder);
+        assertThat(mPreference.shouldHideSecondTarget()).isTrue();
+        assertThat(mWidgetFrame.getVisibility()).isEqualTo(View.GONE);
+
+        mPreference.setOnAddClickListener(p -> {});
+        assertThat(mPreference.shouldHideSecondTarget()).isFalse();
+        assertThat(mWidgetFrame.getVisibility()).isEqualTo(View.VISIBLE);
+    }
+
+    @Test
+    public void setOnAddClickListener_listenerRemovedAfterBinding_addButtonNotVisible() {
+        mPreference.setOnAddClickListener(p -> {});
+
+        mPreference.onBindViewHolder(mViewHolder);
+        assertThat(mPreference.shouldHideSecondTarget()).isFalse();
+        assertThat(mWidgetFrame.getVisibility()).isEqualTo(View.VISIBLE);
+
+        mPreference.setOnAddClickListener(null);
+        assertThat(mPreference.shouldHideSecondTarget()).isTrue();
+        assertThat(mWidgetFrame.getVisibility()).isEqualTo(View.GONE);
+    }
+
+    @Test
+    public void setOnAddClickListener_listenerAddedAndRemovedAfterBinding_addButtonNotVisible() {
+        mPreference.onBindViewHolder(mViewHolder);
+        assertThat(mPreference.shouldHideSecondTarget()).isTrue();
+        assertThat(mWidgetFrame.getVisibility()).isEqualTo(View.GONE);
+
+        mPreference.setOnAddClickListener(p -> {});
+        assertThat(mPreference.shouldHideSecondTarget()).isFalse();
+        assertThat(mWidgetFrame.getVisibility()).isEqualTo(View.VISIBLE);
+
+        mPreference.setOnAddClickListener(null);
+        assertThat(mPreference.shouldHideSecondTarget()).isTrue();
+        assertThat(mWidgetFrame.getVisibility()).isEqualTo(View.GONE);
+    }
+
+    @Test
+    public void onClick_noListener_noCrash() {
+        mPreference.onBindViewHolder(mViewHolder);
+        // should be no crash here
+        mPreference.onClick(mAddWidget);
+    }
+
+    @Test
+    public void onClick_hasListenerBeforeBind_firesCorrectly() {
+        final AddPreference.OnAddClickListener listener = mock(
+                AddPreference.OnAddClickListener.class);
+        mPreference.setOnAddClickListener(listener);
+
+        mPreference.onBindViewHolder(mViewHolder);
+        assertThat(mWidgetFrame.getVisibility()).isEqualTo(View.VISIBLE);
+
+        mPreference.onClick(mAddWidget);
+        verify(listener).onAddClick(eq(mPreference));
+    }
+
+    @Test
+    public void onClick_listenerAddedAfterBind_firesCorrectly() {
+        mPreference.onBindViewHolder(mViewHolder);
+        assertThat(mWidgetFrame.getVisibility()).isEqualTo(View.GONE);
+
+        final AddPreference.OnAddClickListener listener = mock(
+                AddPreference.OnAddClickListener.class);
+        mPreference.setOnAddClickListener(listener);
+        assertThat(mWidgetFrame.getVisibility()).isEqualTo(View.VISIBLE);
+
+        mPreference.onClick(mAddWidget);
+        verify(listener).onAddClick(eq(mPreference));
+    }
+}