Merge "Update SettingsPanel UI"

This commit is contained in:
TreeHugger Robot
2018-12-01 06:51:53 +00:00
committed by Android (Google) Code Review
14 changed files with 569 additions and 35 deletions

View File

@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2018 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
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:paddingBottom="24dp"
android:paddingTop="18dp"
android:textColor="?android:attr/colorPrimary"
android:textSize="20sp"/>
<include layout="@layout/horizontal_divider"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/panel_parent_layout"
android:scrollbars="vertical"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"/>
<include layout="@layout/panel_buttons"/>
</LinearLayout>

View File

@@ -0,0 +1,45 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2018 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
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="14dp"
android:paddingBottom="14dp"
android:orientation="horizontal">
<Button
android:id="@+id/see_more"
style="@android:style/Widget.DeviceDefault.Button.Borderless.Colored"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
android:text="@string/see_more"/>
<Space
android:layout_weight="1"
android:layout_width="0dp"
android:layout_height="match_parent" />
<Button
android:id="@+id/done"
style="@android:style/Widget.DeviceDefault.Button.Borderless.Colored"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="20dp"
android:text="@string/done"/>
</LinearLayout>

View File

@@ -14,13 +14,30 @@
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License limitations under the License
--> -->
<!-- Note: There is a landscape version of this layout. -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/panel_parent_layout" android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/title"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingLeft="24dp" android:gravity="center"
android:paddingRight="24dp" android:paddingBottom="24dp"
android:paddingBottom="8dp" android:paddingTop="18dp"
android:layout_margin="4dp" android:textColor="?android:attr/colorPrimary"
android:orientation="vertical"> android:textSize="20sp"/>
<include layout="@layout/horizontal_divider"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/panel_parent_layout"
android:scrollbars="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<include layout="@layout/panel_buttons"/>
</LinearLayout> </LinearLayout>

View File

@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2018 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.
-->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<androidx.slice.widget.SliceView
android:id="@+id/slice_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="20dp"
android:paddingEnd="20dp" />
<include layout="@layout/horizontal_divider"/>
</LinearLayout>

View File

@@ -197,6 +197,9 @@
<style name="Theme.BottomDialog" parent="@*android:style/Theme.DeviceDefault.Settings.Dialog"> <style name="Theme.BottomDialog" parent="@*android:style/Theme.DeviceDefault.Settings.Dialog">
<item name="android:windowBackground">@drawable/settings_panel_background</item> <item name="android:windowBackground">@drawable/settings_panel_background</item>
<item name="android:dividerHorizontal">@*android:drawable/list_divider_material</item>
<item name="android:windowNoTitle">true</item>
<item name="android:fontFamily">@*android:string/config_headlineFontFamilyMedium</item>
</style> </style>
</resources> </resources>

View File

@@ -19,6 +19,7 @@ package com.android.settings.panel;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.net.Uri; import android.net.Uri;
import android.provider.Settings;
import com.android.settings.R; import com.android.settings.R;
import com.android.settings.slices.CustomSliceRegistry; import com.android.settings.slices.CustomSliceRegistry;
@@ -60,6 +61,6 @@ public class InternetConnectivityPanel implements PanelContent {
@Override @Override
public Intent getSeeMoreIntent() { public Intent getSeeMoreIntent() {
return null; return new Intent(Settings.ACTION_WIRELESS_SETTINGS);
} }
} }

View File

@@ -16,19 +16,20 @@
package com.android.settings.panel; package com.android.settings.panel;
import android.net.Uri; import android.content.Intent;
import android.os.Bundle; import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.LinearLayout; import android.widget.Button;
import android.widget.TextView;
import androidx.lifecycle.LiveData; import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.slice.Slice; import androidx.recyclerview.widget.RecyclerView;
import androidx.slice.widget.SliceLiveData;
import androidx.slice.widget.SliceView;
import com.android.internal.annotations.VisibleForTesting;
import com.android.settings.R; import com.android.settings.R;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
@@ -38,20 +39,24 @@ import androidx.fragment.app.FragmentActivity;
import com.android.settings.overlay.FeatureFactory; import com.android.settings.overlay.FeatureFactory;
import java.util.ArrayList;
import java.util.List;
public class PanelFragment extends Fragment { public class PanelFragment extends Fragment {
private static final String TAG = "PanelFragment"; private static final String TAG = "PanelFragment";
private List<SliceView> mSliceViewList; private TextView mTitleView;
private List<LiveData<Slice>> mSliceDataList; private Button mSeeMoreButton;
private LinearLayout mPanelLayout; private Button mDoneButton;
private RecyclerView mPanelSlices;
@VisibleForTesting
PanelSlicesAdapter mAdapter;
private View.OnClickListener mDoneButtonListener = (v) -> {
Log.d(TAG, "Closing dialog");
getActivity().finish();
};
public PanelFragment() { public PanelFragment() {
mSliceViewList = new ArrayList<>();
mSliceDataList = new ArrayList<>();
} }
@Nullable @Nullable
@@ -61,28 +66,37 @@ public class PanelFragment extends Fragment {
final FragmentActivity activity = getActivity(); final FragmentActivity activity = getActivity();
final View view = inflater.inflate(R.layout.panel_layout, container, false); final View view = inflater.inflate(R.layout.panel_layout, container, false);
mPanelLayout = view.findViewById(R.id.panel_parent_layout); mPanelSlices = view.findViewById(R.id.panel_parent_layout);
final Bundle arguments = getArguments(); mSeeMoreButton = view.findViewById(R.id.see_more);
mDoneButton = view.findViewById(R.id.done);
mTitleView = view.findViewById(R.id.title);
final Bundle arguments = getArguments();
final String panelType = arguments.getString(SettingsPanelActivity.KEY_PANEL_TYPE_ARGUMENT); final String panelType = arguments.getString(SettingsPanelActivity.KEY_PANEL_TYPE_ARGUMENT);
final PanelContent panel = FeatureFactory.getFactory(activity) final PanelContent panel = FeatureFactory.getFactory(activity)
.getPanelFeatureProvider() .getPanelFeatureProvider()
.getPanel(activity, panelType); .getPanel(activity, panelType);
activity.setTitle(panel.getTitle()); mAdapter = new PanelSlicesAdapter(this, panel.getSlices());
mPanelSlices.setHasFixedSize(true);
mPanelSlices.setLayoutManager(new LinearLayoutManager((activity)));
mPanelSlices.setAdapter(mAdapter);
for (Uri uri : panel.getSlices()) { mTitleView.setText(panel.getTitle());
final SliceView sliceView = new SliceView(activity);
mPanelLayout.addView(sliceView);
final LiveData<Slice> liveData = SliceLiveData.fromUri(activity, uri);
liveData.observe(this /* lifecycleOwner */, sliceView);
mSliceDataList.add(liveData); mSeeMoreButton.setOnClickListener(getSeeMoreListener(panel.getSeeMoreIntent()));
mSliceViewList.add(sliceView); mDoneButton.setOnClickListener(mDoneButtonListener);
}
return view; return view;
} }
private View.OnClickListener getSeeMoreListener(final Intent intent) {
return (v) -> {
final FragmentActivity activity = getActivity();
activity.startActivity(intent);
activity.finish();
};
}
} }

View File

@@ -0,0 +1,100 @@
/*
* Copyright (C) 2018 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.panel;
import android.content.Context;
import android.net.Uri;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import androidx.lifecycle.LiveData;
import androidx.recyclerview.widget.RecyclerView;
import androidx.slice.Slice;
import androidx.slice.widget.SliceLiveData;
import androidx.slice.widget.SliceView;
import com.android.settings.R;
import java.util.ArrayList;
import java.util.List;
/**
* RecyclerView adapter for Slices in Settings Panels.
*/
public class PanelSlicesAdapter
extends RecyclerView.Adapter<PanelSlicesAdapter.SliceRowViewHolder> {
private final List<Uri> mSliceUris;
private final PanelFragment mPanelFragment;
public PanelSlicesAdapter(PanelFragment fragment, List<Uri> sliceUris) {
mPanelFragment = fragment;
mSliceUris = new ArrayList<>(sliceUris);
}
@NonNull
@Override
public SliceRowViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int viewType) {
final Context context = viewGroup.getContext();
final LayoutInflater inflater = LayoutInflater.from(context);
final View view = inflater.inflate(R.layout.panel_slice_row, viewGroup, false);
return new SliceRowViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull SliceRowViewHolder sliceRowViewHolder, int position) {
sliceRowViewHolder.onBind(mPanelFragment, mSliceUris.get(position));
}
@Override
public int getItemCount() {
return mSliceUris.size();
}
@VisibleForTesting
List<Uri> getData() {
return mSliceUris;
}
/**
* ViewHolder for binding Slices to SliceViews.
*/
public static class SliceRowViewHolder extends RecyclerView.ViewHolder {
@VisibleForTesting
LiveData<Slice> sliceLiveData;
@VisibleForTesting
final SliceView sliceView;
public SliceRowViewHolder(View view) {
super(view);
sliceView = view.findViewById(R.id.slice_view);
sliceView.setMode(SliceView.MODE_LARGE);
}
public void onBind(PanelFragment fragment, Uri sliceUri) {
final Context context = sliceView.getContext();
sliceLiveData = SliceLiveData.fromUri(context, sliceUri);
sliceLiveData.observe(fragment.getViewLifecycleOwner(), sliceView);
}
}
}

View File

@@ -0,0 +1,56 @@
/*
* Copyright (C) 2018 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.panel;
import static com.android.settings.slices.CustomSliceRegistry.WIFI_SLICE_URI;
import android.content.Intent;
import android.net.Uri;
import java.util.Arrays;
import java.util.List;
/**
* Fake PanelContent for testing.
*/
public class FakePanelContent implements PanelContent {
public static final String FAKE_KEY = "fake_key";
public static final CharSequence TITLE = "title";
public static final List<Uri> SLICE_URIS = Arrays.asList(
WIFI_SLICE_URI
);
public static final Intent INTENT = new Intent();
@Override
public CharSequence getTitle() {
return TITLE;
}
@Override
public List<Uri> getSlices() {
return SLICE_URIS;
}
@Override
public Intent getSeeMoreIntent() {
return INTENT;
}
}

View File

@@ -0,0 +1,34 @@
/*
* Copyright (C) 2018 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.panel;
import android.content.ComponentName;
import android.content.Intent;
public class FakeSettingsPanelActivity extends SettingsPanelActivity {
@Override
public ComponentName getCallingActivity() {
return new ComponentName("fake-package", "fake-class");
}
@Override
public Intent getIntent() {
final Intent intent = new Intent();
intent.putExtra(SettingsPanelActivity.EXTRA_PANEL_TYPE, FakePanelContent.FAKE_KEY);
return intent;
}
}

View File

@@ -49,4 +49,9 @@ public class InternetConnectivityPanelTest {
assertThat(uris).containsExactly(CustomSliceRegistry.WIFI_SLICE_URI, assertThat(uris).containsExactly(CustomSliceRegistry.WIFI_SLICE_URI,
CustomSliceRegistry.AIRPLANE_URI); CustomSliceRegistry.AIRPLANE_URI);
} }
@Test
public void getSeeMoreIntent_notNull() {
assertThat(mPanel.getSeeMoreIntent()).isNotNull();
}
} }

View File

@@ -0,0 +1,89 @@
/*
* Copyright (C) 2018 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.panel;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.spy;
import android.content.Context;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.widget.LinearLayout;
import com.android.settings.R;
import com.android.settings.testutils.FakeFeatureFactory;
import com.android.settings.testutils.SettingsRobolectricTestRunner;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.Robolectric;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.android.controller.ActivityController;
@RunWith(SettingsRobolectricTestRunner.class)
public class PanelFragmentTest {
private Context mContext;
private PanelFragment mPanelFragment;
private FakeFeatureFactory mFakeFeatureFactory;
private PanelFeatureProvider mPanelFeatureProvider;
private FakePanelContent mFakePanelContent;
private final String FAKE_EXTRA = "fake_extra";
@Before
public void setUp() {
mContext = RuntimeEnvironment.application;
mPanelFeatureProvider = spy(new PanelFeatureProviderImpl());
mFakeFeatureFactory = FakeFeatureFactory.setupForTest();
mFakeFeatureFactory.panelFeatureProvider = mPanelFeatureProvider;
mFakePanelContent = new FakePanelContent();
doReturn(mFakePanelContent).when(mPanelFeatureProvider).getPanel(any(), any());
ActivityController<FakeSettingsPanelActivity> activityController =
Robolectric.buildActivity(FakeSettingsPanelActivity.class);
activityController.setup();
mPanelFragment =
spy((PanelFragment)
activityController
.get()
.getSupportFragmentManager()
.findFragmentById(R.id.main_content));
}
@Test
public void onCreateView_adapterGetsDataset() {
final Bundle bundle = new Bundle();
bundle.putString(SettingsPanelActivity.KEY_PANEL_TYPE_ARGUMENT, FAKE_EXTRA);
doReturn(bundle).when(mPanelFragment).getArguments();
mPanelFragment.onCreateView(LayoutInflater.from(mContext),
new LinearLayout(mContext), null);
PanelSlicesAdapter adapter = mPanelFragment.mAdapter;
assertThat(adapter.getData()).containsAllIn(mFakePanelContent.getSlices());
}
}

View File

@@ -0,0 +1,97 @@
/*
* Copyright (C) 2018 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.panel;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.spy;
import android.content.Context;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import com.android.settings.R;
import com.android.settings.testutils.FakeFeatureFactory;
import com.android.settings.testutils.SettingsRobolectricTestRunner;
import org.junit.Before;
import org.junit.runner.RunWith;
import org.robolectric.Robolectric;
import org.junit.Test;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.android.controller.ActivityController;
@RunWith(SettingsRobolectricTestRunner.class)
public class PanelSlicesAdapterTest {
private Context mContext;
private PanelFragment mPanelFragment;
private FakePanelContent mFakePanelContent;
private FakeFeatureFactory mFakeFeatureFactory;
private PanelFeatureProvider mPanelFeatureProvider;
private PanelSlicesAdapter mAdapter;
@Before
public void setUp() {
mContext = RuntimeEnvironment.application;
final ActivityController<FakeSettingsPanelActivity> activityController =
Robolectric.buildActivity(FakeSettingsPanelActivity.class);
activityController.setup();
mPanelFragment =
spy((PanelFragment)
activityController
.get()
.getSupportFragmentManager()
.findFragmentById(R.id.main_content));
mPanelFeatureProvider = spy(new PanelFeatureProviderImpl());
mFakeFeatureFactory = FakeFeatureFactory.setupForTest();
mFakeFeatureFactory.panelFeatureProvider = mPanelFeatureProvider;
mFakePanelContent = new FakePanelContent();
doReturn(mFakePanelContent).when(mPanelFeatureProvider).getPanel(any(), any());
mAdapter = new PanelSlicesAdapter(mPanelFragment, mFakePanelContent.getSlices());
}
@Test
public void onCreateViewHolder_returnsSliceRowViewHolder() {
final ViewGroup view = new FrameLayout(mContext);
final PanelSlicesAdapter.SliceRowViewHolder viewHolder =
mAdapter.onCreateViewHolder(view, 0);
assertThat(viewHolder.sliceView).isNotNull();
}
@Test
public void onBindViewHolder_bindsSlice() {
final int position = 0;
final ViewGroup view = new FrameLayout(mContext);
final PanelSlicesAdapter.SliceRowViewHolder viewHolder =
mAdapter.onCreateViewHolder(view, 0 /* view type*/);
mAdapter.onBindViewHolder(viewHolder, position);
assertThat(viewHolder.sliceLiveData).isNotNull();
}
}

View File

@@ -63,9 +63,9 @@ public class FakeFeatureFactory extends FeatureFactory {
public final UserFeatureProvider userFeatureProvider; public final UserFeatureProvider userFeatureProvider;
public final AssistGestureFeatureProvider assistGestureFeatureProvider; public final AssistGestureFeatureProvider assistGestureFeatureProvider;
public final AccountFeatureProvider mAccountFeatureProvider; public final AccountFeatureProvider mAccountFeatureProvider;
public final PanelFeatureProvider mPanelFeatureProvider;
public final ContextualCardFeatureProvider mContextualCardFeatureProvider; public final ContextualCardFeatureProvider mContextualCardFeatureProvider;
public PanelFeatureProvider panelFeatureProvider;
public SlicesFeatureProvider slicesFeatureProvider; public SlicesFeatureProvider slicesFeatureProvider;
public SearchFeatureProvider searchFeatureProvider; public SearchFeatureProvider searchFeatureProvider;
@@ -106,8 +106,8 @@ public class FakeFeatureFactory extends FeatureFactory {
assistGestureFeatureProvider = mock(AssistGestureFeatureProvider.class); assistGestureFeatureProvider = mock(AssistGestureFeatureProvider.class);
slicesFeatureProvider = mock(SlicesFeatureProvider.class); slicesFeatureProvider = mock(SlicesFeatureProvider.class);
mAccountFeatureProvider = mock(AccountFeatureProvider.class); mAccountFeatureProvider = mock(AccountFeatureProvider.class);
mPanelFeatureProvider = mock(PanelFeatureProvider.class);
mContextualCardFeatureProvider = mock(ContextualCardFeatureProvider.class); mContextualCardFeatureProvider = mock(ContextualCardFeatureProvider.class);
panelFeatureProvider = mock(PanelFeatureProvider.class);
} }
@Override @Override
@@ -192,7 +192,7 @@ public class FakeFeatureFactory extends FeatureFactory {
@Override @Override
public PanelFeatureProvider getPanelFeatureProvider() { public PanelFeatureProvider getPanelFeatureProvider() {
return mPanelFeatureProvider; return panelFeatureProvider;
} }
public ContextualCardFeatureProvider getContextualCardFeatureProvider() { public ContextualCardFeatureProvider getContextualCardFeatureProvider() {