limitations under the License
-->
-<!-- Note: There is a landscape version of this layout. -->
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:orientation="vertical">
+<FrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/panel_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="@drawable/settings_panel_background" >
- <TextView
- android:id="@+id/panel_title"
+ <LinearLayout
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"/>
+ android:orientation="vertical">
- <include layout="@layout/panel_slice_list"/>
+ <TextView
+ android:id="@+id/panel_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"/>
- <LinearLayout
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:orientation="horizontal"
- android:paddingTop="8dp"
- android:paddingBottom="8dp">
+ <include layout="@layout/horizontal_divider"/>
+
+ <!-- Note: There is a landscape version of panel_slice_list which supports scrolling. -->
+ <include layout="@layout/panel_slice_list"/>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:paddingTop="8dp"
+ android:paddingBottom="8dp">
- <Button
- android:id="@+id/see_more"
- style="@android:style/Widget.DeviceDefault.Button.Borderless.Colored"
- android:layout_width="wrap_content"
- android:layout_height="48dp"
- android:layout_marginStart="12dp"
- android:text="@string/see_more"/>
+ <Button
+ android:id="@+id/see_more"
+ style="@android:style/Widget.DeviceDefault.Button.Borderless.Colored"
+ android:layout_width="wrap_content"
+ android:layout_height="48dp"
+ android:layout_marginStart="12dp"
+ android:text="@string/see_more"/>
- <Space
- android:layout_weight="1"
- android:layout_width="0dp"
- android:layout_height="match_parent" />
+ <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="48dp"
- android:layout_marginEnd="12dp"
- android:text="@string/done"/>
+ <Button
+ android:id="@+id/done"
+ style="@android:style/Widget.DeviceDefault.Button.Borderless.Colored"
+ android:layout_width="wrap_content"
+ android:layout_height="48dp"
+ android:layout_marginEnd="12dp"
+ android:text="@string/done"/>
+ </LinearLayout>
</LinearLayout>
-</LinearLayout>
\ No newline at end of file
+</FrameLayout>
\ No newline at end of file
-->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/main_content"
- android:layout_height="match_parent"
- android:layout_width="match_parent"
- android:animateLayoutChanges="true"/>
\ No newline at end of file
+ android:layout_height="wrap_content"
+ android:layout_width="match_parent"/>
\ No newline at end of file
<!-- Note that Dialog themes do not set list dividers -->
<style name="Theme.BottomDialog" parent="@*android:style/Theme.DeviceDefault.Settings.Dialog">
- <item name="android:windowBackground">@drawable/settings_panel_background</item>
+ <item name="android:windowBackground">@null</item>
<item name="android:dividerHorizontal">@*android:drawable/list_divider_material</item>
<item name="android:windowNoTitle">true</item>
<item name="android:listDivider">@*android:drawable/list_divider_material</item>
package com.android.settings.panel;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.animation.ValueAnimator;
import android.app.settings.SettingsEnums;
-import android.content.Context;
+import android.net.Uri;
import android.os.Bundle;
+import android.os.Handler;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
+import android.view.ViewTreeObserver;
+import android.view.animation.DecelerateInterpolator;
+import android.view.animation.Interpolator;
import android.widget.Button;
import android.widget.TextView;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
+import androidx.lifecycle.LiveData;
+import androidx.slice.Slice;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
+import androidx.slice.SliceMetadata;
+import androidx.slice.widget.SliceLiveData;
import com.android.internal.annotations.VisibleForTesting;
import com.android.settings.R;
import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
import com.google.android.setupdesign.DividerItemDecoration;
+import java.util.ArrayList;
+import java.util.List;
+
public class PanelFragment extends Fragment {
private static final String TAG = "PanelFragment";
+ /**
+ * Duration of the animation entering or exiting the screen, in milliseconds.
+ */
+ private static final int DURATION_ANIMATE_PANEL_MS = 250;
+
+ /**
+ * Duration of timeout waiting for Slice data to bind, in milliseconds.
+ */
+ private static final int DURATION_SLICE_BINDING_TIMEOUT_MS = 250;
+
+ private View mLayoutView;
private TextView mTitleView;
private Button mSeeMoreButton;
private Button mDoneButton;
private MetricsFeatureProvider mMetricsProvider;
private String mPanelClosedKey;
+ private final List<LiveData<Slice>> mSliceLiveData = new ArrayList<>();
+
@VisibleForTesting
- PanelSlicesAdapter mAdapter;
+ PanelSlicesLoaderCountdownLatch mPanelSlicesLoaderCountdownLatch;
+
+ private ViewTreeObserver.OnPreDrawListener mOnPreDrawListener = () -> {
+ return false;
+ };
+
+ private final ViewTreeObserver.OnGlobalLayoutListener mOnGlobalLayoutListener =
+ new ViewTreeObserver.OnGlobalLayoutListener() {
+ @Override
+ public void onGlobalLayout() {
+ animateIn();
+ if (mPanelSlices != null) {
+ mPanelSlices.getViewTreeObserver().removeOnGlobalLayoutListener(this);
+ }
+ }
+ };
+
+ private PanelSlicesAdapter mAdapter;
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
final FragmentActivity activity = getActivity();
- final View view = inflater.inflate(R.layout.panel_layout, container, false);
- mPanelSlices = view.findViewById(R.id.panel_parent_layout);
- mSeeMoreButton = view.findViewById(R.id.see_more);
- mDoneButton = view.findViewById(R.id.done);
- mTitleView = view.findViewById(R.id.panel_title);
+ mLayoutView = inflater.inflate(R.layout.panel_layout, container, false);
+
+ mPanelSlices = mLayoutView.findViewById(R.id.panel_parent_layout);
+ mSeeMoreButton = mLayoutView.findViewById(R.id.see_more);
+ mDoneButton = mLayoutView.findViewById(R.id.done);
+ mTitleView = mLayoutView.findViewById(R.id.panel_title);
final Bundle arguments = getArguments();
final String panelType =
.getPanel(activity, panelType, mediaPackageName);
mMetricsProvider = FeatureFactory.getFactory(activity).getMetricsFeatureProvider();
+
+ mPanelSlices.setLayoutManager(new LinearLayoutManager((activity)));
+
+ // Add predraw listener to remove the animation and while we wait for Slices to load.
+ mLayoutView.getViewTreeObserver().addOnPreDrawListener(mOnPreDrawListener);
+
+ // Start loading Slices. When finished, the Panel will animate in.
+ loadAllSlices();
+
+ mTitleView.setText(mPanel.getTitle());
+ mSeeMoreButton.setOnClickListener(getSeeMoreListener());
+ mDoneButton.setOnClickListener(getCloseListener());
+
+ // If getSeeMoreIntent() is null, hide the mSeeMoreButton.
+ if (mPanel.getSeeMoreIntent() == null) {
+ mSeeMoreButton.setVisibility(View.GONE);
+ }
+
// Log panel opened.
mMetricsProvider.action(
0 /* attribution */,
callingPackageName,
0 /* value */);
- mAdapter = new PanelSlicesAdapter(this, mPanel);
+ return mLayoutView;
+ }
- mPanelSlices.setHasFixedSize(true);
- mPanelSlices.setLayoutManager(new LinearLayoutManager((activity)));
- mPanelSlices.setAdapter(mAdapter);
+ private void loadAllSlices() {
+ mSliceLiveData.clear();
+ final List<Uri> sliceUris = mPanel.getSlices();
+ mPanelSlicesLoaderCountdownLatch = new PanelSlicesLoaderCountdownLatch(sliceUris.size());
- DividerItemDecoration itemDecoration = new DividerItemDecoration(getActivity());
- itemDecoration.setDividerCondition(DividerItemDecoration.DIVIDER_CONDITION_BOTH);
- mPanelSlices.addItemDecoration(itemDecoration);
+ for (Uri uri : sliceUris) {
+ final LiveData<Slice> sliceLiveData = SliceLiveData.fromUri(getActivity(), uri);
- mTitleView.setText(mPanel.getTitle());
+ // Add slice first to make it in order. Will remove it later if there's an error.
+ mSliceLiveData.add(sliceLiveData);
- mSeeMoreButton.setOnClickListener(getSeeMoreListener());
- mDoneButton.setOnClickListener(getCloseListener());
+ sliceLiveData.observe(getViewLifecycleOwner(), slice -> {
+ // If the Slice has already loaded, do nothing.
+ if (mPanelSlicesLoaderCountdownLatch.isSliceLoaded(uri)) {
+ return;
+ }
- //If getSeeMoreIntent() is null, hide the mSeeMoreButton.
- if (mPanel.getSeeMoreIntent() == null) {
- mSeeMoreButton.setVisibility(View.GONE);
+ /**
+ * Watching for the {@link Slice} to load.
+ * <p>
+ * If the Slice comes back {@code null} or with the Error attribute, remove the
+ * Slice data from the list, and mark the Slice as loaded.
+ * <p>
+ * If the Slice has come back fully loaded, then mark the Slice as loaded. No
+ * other actions required since we already have the Slice data in the list.
+ * <p>
+ * If the Slice does not match the above condition, we will still want to mark
+ * it as loaded after 250ms timeout to avoid delay showing up the panel for
+ * too long. Since we are still having the Slice data in the list, the Slice
+ * will show up later once it is loaded.
+ */
+ final SliceMetadata metadata = SliceMetadata.from(getActivity(), slice);
+ if (slice == null || metadata.isErrorSlice()) {
+ mSliceLiveData.remove(sliceLiveData);
+ mPanelSlicesLoaderCountdownLatch.markSliceLoaded(uri);
+ } else if (metadata.getLoadingState() == SliceMetadata.LOADED_ALL) {
+ mPanelSlicesLoaderCountdownLatch.markSliceLoaded(uri);
+ } else {
+ Handler handler = new Handler();
+ handler.postDelayed(() -> {
+ mPanelSlicesLoaderCountdownLatch.markSliceLoaded(uri);
+ loadPanelWhenReady();
+ }, DURATION_SLICE_BINDING_TIMEOUT_MS);
+ }
+
+ loadPanelWhenReady();
+ });
}
+ }
+
+ /**
+ * When all of the Slices have loaded for the first time, then we can setup the
+ * {@link RecyclerView}.
+ * <p>
+ * When the Recyclerview has been laid out, we can begin the animation with the
+ * {@link mOnGlobalLayoutListener}, which calls {@link #animateIn()}.
+ */
+ private void loadPanelWhenReady() {
+ if (mPanelSlicesLoaderCountdownLatch.isPanelReadyToLoad()) {
+ mAdapter = new PanelSlicesAdapter(
+ this, mSliceLiveData, mPanel.getMetricsCategory());
+ mPanelSlices.setAdapter(mAdapter);
+ mPanelSlices.getViewTreeObserver()
+ .addOnGlobalLayoutListener(mOnGlobalLayoutListener);
+
+ DividerItemDecoration itemDecoration = new DividerItemDecoration(getActivity());
+ itemDecoration
+ .setDividerCondition(DividerItemDecoration.DIVIDER_CONDITION_BOTH);
+ mPanelSlices.addItemDecoration(itemDecoration);
+ }
+ }
+
+ /**
+ * Animate a Panel onto the screen.
+ * <p>
+ * Takes the entire panel and animates in from behind the navigation bar.
+ * <p>
+ * Relies on the Panel being having a fixed height to begin the animation.
+ */
+ private void animateIn() {
+ final View panelContent = mLayoutView.findViewById(R.id.panel_container);
+ final AnimatorSet animatorSet = buildAnimatorSet(mLayoutView, panelContent.getHeight(),
+ 0.0f, new DecelerateInterpolator());
+ final ValueAnimator animator = new ValueAnimator();
+ animator.setFloatValues(0.0f, 1.0f);
+ animatorSet.play(animator);
+ animatorSet.start();
+ // Remove the predraw listeners on the Panel.
+ mLayoutView.getViewTreeObserver().removeOnPreDrawListener(mOnPreDrawListener);
+ }
- return view;
+ /**
+ * Build an {@link AnimatorSet} to bring the Panel, {@param parentView}in our out of the screen,
+ * based on the positional parameters {@param startY}, {@param endY} and at the rate set by the
+ * {@param interpolator}.
+ */
+ @NonNull
+ private static AnimatorSet buildAnimatorSet(@NonNull View parentView, float startY, float endY,
+ @NonNull Interpolator interpolator) {
+ final View sheet = parentView.findViewById(R.id.panel_container);
+ final AnimatorSet animatorSet = new AnimatorSet();
+ animatorSet.setDuration(DURATION_ANIMATE_PANEL_MS);
+ animatorSet.setInterpolator(interpolator);
+ animatorSet.playTogether(ObjectAnimator.ofFloat(sheet, View.TRANSLATION_Y, startY, endY));
+ return animatorSet;
}
@Override
import android.app.settings.SettingsEnums;
import android.content.Context;
-import android.net.Uri;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
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 com.android.settings.overlay.FeatureFactory;
import com.google.android.setupdesign.DividerItemDecoration;
+import java.util.ArrayList;
import java.util.List;
/**
public class PanelSlicesAdapter
extends RecyclerView.Adapter<PanelSlicesAdapter.SliceRowViewHolder> {
- private final List<Uri> mSliceUris;
+ private final List<LiveData<Slice>> mSliceLiveData;
+ private final int mMetricsCategory;
private final PanelFragment mPanelFragment;
- private final PanelContent mPanelContent;
- public PanelSlicesAdapter(PanelFragment fragment, PanelContent panel) {
+ public PanelSlicesAdapter(
+ PanelFragment fragment, List<LiveData<Slice>> sliceLiveData, int metricsCategory) {
mPanelFragment = fragment;
- mSliceUris = panel.getSlices();
- mPanelContent = panel;
+ mSliceLiveData = new ArrayList<>(sliceLiveData);
+ mMetricsCategory = metricsCategory;
}
@NonNull
final LayoutInflater inflater = LayoutInflater.from(context);
final View view = inflater.inflate(R.layout.panel_slice_row, viewGroup, false);
- return new SliceRowViewHolder(view, mPanelContent);
+ return new SliceRowViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull SliceRowViewHolder sliceRowViewHolder, int position) {
- sliceRowViewHolder.onBind(mPanelFragment, mSliceUris.get(position));
+ sliceRowViewHolder.onBind(mSliceLiveData.get(position));
}
@Override
public int getItemCount() {
- return mSliceUris.size();
+ return mSliceLiveData.size();
}
@VisibleForTesting
- List<Uri> getData() {
- return mSliceUris;
+ List<LiveData<Slice>> getData() {
+ return mSliceLiveData;
}
/**
* ViewHolder for binding Slices to SliceViews.
*/
- public static class SliceRowViewHolder extends RecyclerView.ViewHolder
+ public class SliceRowViewHolder extends RecyclerView.ViewHolder
implements DividerItemDecoration.DividedViewHolder {
- private final PanelContent mPanelContent;
-
private boolean mDividerAllowedAbove = true;
@VisibleForTesting
- LiveData<Slice> sliceLiveData;
-
- @VisibleForTesting
final SliceView sliceView;
- public SliceRowViewHolder(View view, PanelContent panelContent) {
+ public SliceRowViewHolder(View view) {
super(view);
sliceView = view.findViewById(R.id.slice_view);
sliceView.setMode(SliceView.MODE_LARGE);
sliceView.showTitleItems(true);
- mPanelContent = panelContent;
}
- public void onBind(PanelFragment fragment, Uri sliceUri) {
- final Context context = sliceView.getContext();
- sliceLiveData = SliceLiveData.fromUri(context, sliceUri);
- sliceLiveData.observe(fragment.getViewLifecycleOwner(), sliceView);
+ public void onBind(LiveData<Slice> sliceLiveData) {
+ sliceLiveData.observe(mPanelFragment.getViewLifecycleOwner(), sliceView);
// Do not show the divider above media devices switcher slice per request
- if (sliceUri.equals(MEDIA_OUTPUT_INDICATOR_SLICE_URI)) {
+ final Slice slice = sliceLiveData.getValue();
+ if (slice != null && slice.getUri().equals(MEDIA_OUTPUT_INDICATOR_SLICE_URI)) {
mDividerAllowedAbove = false;
}
// Log Panel interaction
sliceView.setOnSliceActionListener(
((eventInfo, sliceItem) -> {
- FeatureFactory.getFactory(context)
+ FeatureFactory.getFactory(sliceView.getContext())
.getMetricsFeatureProvider()
.action(0 /* attribution */,
SettingsEnums.ACTION_PANEL_INTERACTION,
- mPanelContent.getMetricsCategory(),
- sliceUri.toString() /* log key */,
+ mMetricsCategory,
+ sliceLiveData.toString() /* log key */,
eventInfo.actionType /* value */);
})
);
--- /dev/null
+/*
+ * 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.panel;
+
+import android.net.Uri;
+
+import androidx.slice.Slice;
+
+import java.util.HashSet;
+import java.util.Set;
+import java.util.concurrent.CountDownLatch;
+
+/**
+ * Helper class to isolate the work tracking all of the {@link Slice Slices} being loaded.
+ * <p>
+ * Uses a {@link CountDownLatch} and a {@link Set} of Slices to track how many
+ * Slices have been loaded. A Slice can only be counted as being loaded a single time, even
+ * when they get updated later.
+ * <p>
+ * To use the class, pass the number of expected Slices to load into the constructor. For
+ * every Slice that loads, call {@link #markSliceLoaded(Uri)} with the corresponding
+ * {@link Uri}. Then check if all of the Slices have loaded with
+ * {@link #isPanelReadyToLoad()}, which will return {@code true} the first time after all
+ * Slices have loaded.
+ */
+public class PanelSlicesLoaderCountdownLatch {
+ private final Set<Uri> mLoadedSlices;
+ private final CountDownLatch mCountDownLatch;
+ private boolean slicesReadyToLoad = false;
+
+ public PanelSlicesLoaderCountdownLatch(int countdownSize) {
+ mLoadedSlices = new HashSet<>();
+ mCountDownLatch = new CountDownLatch(countdownSize);
+ }
+
+ /**
+ * Checks if the {@param sliceUri} has been loaded: if not, then decrement the countdown
+ * latch, and if so, then do nothing.
+ */
+ public void markSliceLoaded(Uri sliceUri) {
+ if (mLoadedSlices.contains(sliceUri)) {
+ return;
+ }
+ mLoadedSlices.add(sliceUri);
+ mCountDownLatch.countDown();
+ }
+
+ /**
+ * @return {@code true} if the Slice has already been loaded.
+ */
+ public boolean isSliceLoaded(Uri uri) {
+ return mLoadedSlices.contains(uri);
+ }
+
+ /**
+ * @return {@code true} when all Slices have loaded, and the Panel has not yet been loaded.
+ */
+ public boolean isPanelReadyToLoad() {
+ /**
+ * Use {@link slicesReadyToLoad} to track whether or not the Panel has been loaded. We
+ * only want to animate the Panel a single time.
+ */
+ if ((mCountDownLatch.getCount() == 0) && !slicesReadyToLoad) {
+ slicesReadyToLoad = true;
+ return true;
+ }
+ return false;
+ }
+}
\ No newline at end of file
import android.app.settings.SettingsEnums;
import android.content.Context;
+import android.net.Uri;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
}
@Test
- public void onCreateView_adapterGetsDataset() {
+ public void onCreateView_countdownLatch_setup() {
mPanelFragment.onCreateView(LayoutInflater.from(mContext),
new LinearLayout(mContext), null);
- PanelSlicesAdapter adapter = mPanelFragment.mAdapter;
+ PanelSlicesLoaderCountdownLatch countdownLatch =
+ mPanelFragment.mPanelSlicesLoaderCountdownLatch;
+ for (Uri sliecUri: mFakePanelContent.getSlices()) {
+ countdownLatch.markSliceLoaded(sliecUri);
+ }
- assertThat(adapter.getData()).containsAllIn(mFakePanelContent.getSlices());
+ assertThat(countdownLatch.isPanelReadyToLoad()).isTrue();
}
@Test
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.when;
-import android.app.settings.SettingsEnums;
import android.content.Context;
-import android.content.Intent;
import android.net.Uri;
import android.view.ViewGroup;
import android.widget.FrameLayout;
+import androidx.lifecycle.LiveData;
+import androidx.slice.Slice;
+
import com.android.settings.R;
+import com.android.settings.slices.CustomSliceRegistry;
import com.android.settings.testutils.FakeFeatureFactory;
import org.junit.Before;
import org.junit.runner.RunWith;
-import org.robolectric.Robolectric;
-
import org.junit.Test;
-
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.Robolectric;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.android.controller.ActivityController;
-import java.util.Arrays;
+import java.util.ArrayList;
import java.util.List;
@RunWith(RobolectricTestRunner.class)
public class PanelSlicesAdapterTest {
+ private static final Uri DATA_URI = CustomSliceRegistry.DATA_USAGE_SLICE_URI;
+
private Context mContext;
private PanelFragment mPanelFragment;
- private FakePanelContent mFakePanelContent;
- private FakeFeatureFactory mFakeFeatureFactory;
private PanelFeatureProvider mPanelFeatureProvider;
+ private FakeFeatureFactory mFakeFeatureFactory;
+ private FakePanelContent mFakePanelContent;
+ private List<LiveData<Slice>> mData = new ArrayList<>();
+
+ @Mock
+ private LiveData<Slice> mLiveData;
+
+ private Slice mSlice;
@Before
public void setUp() {
+ MockitoAnnotations.initMocks(this);
mContext = RuntimeEnvironment.application;
mPanelFeatureProvider = spy(new PanelFeatureProviderImpl());
.get()
.getSupportFragmentManager()
.findFragmentById(R.id.main_content));
+
+ }
+
+ private void constructTestLiveData(Uri uri) {
+ // Create a slice to return for the LiveData
+ mSlice = spy(new Slice());
+ doReturn(uri).when(mSlice).getUri();
+ when(mLiveData.getValue()).thenReturn(mSlice);
+ mData.add(mLiveData);
}
@Test
public void onCreateViewHolder_returnsSliceRowViewHolder() {
+ constructTestLiveData(DATA_URI);
final PanelSlicesAdapter adapter =
- new PanelSlicesAdapter(mPanelFragment, mFakePanelContent);
+ new PanelSlicesAdapter(mPanelFragment, mData, 0 /* metrics category */);
final ViewGroup view = new FrameLayout(mContext);
final PanelSlicesAdapter.SliceRowViewHolder viewHolder =
adapter.onCreateViewHolder(view, 0);
}
@Test
- public void onBindViewHolder_bindsSlice() {
- final PanelSlicesAdapter adapter =
- new PanelSlicesAdapter(mPanelFragment, mFakePanelContent);
- final int position = 0;
- final ViewGroup view = new FrameLayout(mContext);
- final PanelSlicesAdapter.SliceRowViewHolder viewHolder =
- adapter.onCreateViewHolder(view, 0 /* view type*/);
-
- adapter.onBindViewHolder(viewHolder, position);
-
- assertThat(viewHolder.sliceLiveData).isNotNull();
- }
-
- @Test
public void nonMediaOutputIndicatorSlice_shouldAllowDividerAboveAndBelow() {
+ constructTestLiveData(DATA_URI);
final PanelSlicesAdapter adapter =
- new PanelSlicesAdapter(mPanelFragment, mFakePanelContent);
+ new PanelSlicesAdapter(mPanelFragment, mData, 0 /* metrics category */);
final int position = 0;
final ViewGroup view = new FrameLayout(mContext);
final PanelSlicesAdapter.SliceRowViewHolder viewHolder =
@Test
public void mediaOutputIndicatorSlice_shouldNotAllowDividerAbove() {
- PanelContent mediaOutputIndicatorSlicePanelContent = new PanelContent() {
- @Override
- public CharSequence getTitle() {
- return "title";
- }
-
- @Override
- public List<Uri> getSlices() {
- return Arrays.asList(
- MEDIA_OUTPUT_INDICATOR_SLICE_URI
- );
- }
-
- @Override
- public Intent getSeeMoreIntent() {
- return new Intent();
- }
-
- @Override
- public int getMetricsCategory() {
- return SettingsEnums.TESTING;
- }
- };
+ constructTestLiveData(MEDIA_OUTPUT_INDICATOR_SLICE_URI);
final PanelSlicesAdapter adapter =
- new PanelSlicesAdapter(mPanelFragment, mediaOutputIndicatorSlicePanelContent);
+ new PanelSlicesAdapter(mPanelFragment, mData, 0 /* metrics category */);
final int position = 0;
final ViewGroup view = new FrameLayout(mContext);
final PanelSlicesAdapter.SliceRowViewHolder viewHolder =
--- /dev/null
+/*
+ * 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.panel;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.Context;
+import android.net.Uri;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+
+@RunWith(RobolectricTestRunner.class)
+public class PanelSlicesLoaderCountdownLatchTest {
+
+ private Context mContext;
+ private PanelSlicesLoaderCountdownLatch mSliceCountdownLatch;
+
+ private static final Uri[] URIS = new Uri[] {
+ Uri.parse("content://testUri"),
+ Uri.parse("content://wowUri"),
+ Uri.parse("content://boxTurtle")
+ };
+
+ @Before
+ public void setUp() {
+ mContext = RuntimeEnvironment.application;
+ mSliceCountdownLatch = new PanelSlicesLoaderCountdownLatch(URIS.length);
+ }
+
+
+ @Test
+ public void hasLoaded_newObject_returnsFalse() {
+ assertThat(mSliceCountdownLatch.isSliceLoaded(URIS[0])).isFalse();
+ }
+
+ @Test
+ public void hasLoaded_markSliceLoaded_returnsTrue() {
+ mSliceCountdownLatch.markSliceLoaded(URIS[0]);
+
+ assertThat(mSliceCountdownLatch.isSliceLoaded(URIS[0])).isTrue();
+ }
+
+ @Test
+ public void markSliceLoaded_onlyCountsDownUniqueUris() {
+ for (int i = 0; i < URIS.length; i++) {
+ mSliceCountdownLatch.markSliceLoaded(URIS[0]);
+ }
+
+ assertThat(mSliceCountdownLatch.isPanelReadyToLoad()).isFalse();
+ }
+
+ @Test
+ public void areSlicesReadyToLoad_allSlicesLoaded_returnsTrue() {
+ for (int i = 0; i < URIS.length; i++) {
+ mSliceCountdownLatch.markSliceLoaded(URIS[i]);
+ }
+
+ assertThat(mSliceCountdownLatch.isPanelReadyToLoad()).isTrue();
+ }
+
+ @Test
+ public void areSlicesReadyToLoad_onlyReturnsTrueOnce() {
+ for (int i = 0; i < URIS.length; i++) {
+ mSliceCountdownLatch.markSliceLoaded(URIS[i]);
+ }
+
+ // Verify that it returns true once
+ assertThat(mSliceCountdownLatch.isPanelReadyToLoad()).isTrue();
+ // Verify the second call returns false without external state change
+ assertThat(mSliceCountdownLatch.isPanelReadyToLoad()).isFalse();
+ }
+}
\ No newline at end of file