Merge "Add entry animation to Settings Panels." into qt-dev

This commit is contained in:
Linda Tseng
2019-04-16 18:57:47 +00:00
committed by Android (Google) Code Review
9 changed files with 462 additions and 143 deletions

View File

@@ -15,50 +15,60 @@
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">
<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"/>
<include layout="@layout/panel_slice_list"/>
<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" >
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:paddingTop="8dp"
android:paddingBottom="8dp">
android:orientation="vertical">
<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"/>
<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"/>
<Space
android:layout_weight="1"
android:layout_width="0dp"
android:layout_height="match_parent" />
<include layout="@layout/horizontal_divider"/>
<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"/>
<!-- 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"/>
<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"/>
</LinearLayout>
</LinearLayout>
</LinearLayout>
</FrameLayout>

View File

@@ -15,6 +15,5 @@
-->
<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"/>
android:layout_height="wrap_content"
android:layout_width="match_parent"/>

View File

@@ -203,7 +203,7 @@
<!-- 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>

View File

@@ -16,13 +16,20 @@
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;
@@ -30,8 +37,12 @@ import androidx.annotation.NonNull;
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;
@@ -40,10 +51,24 @@ import com.android.settings.panel.PanelLoggingContract.PanelClosedKeys;
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;
@@ -53,20 +78,40 @@ public class PanelFragment extends Fragment {
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 =
@@ -82,6 +127,24 @@ public class PanelFragment extends Fragment {
.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 */,
@@ -90,27 +153,114 @@ public class PanelFragment extends Fragment {
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();
});
}
}
return view;
/**
* 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);
}
/**
* 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

View File

@@ -20,7 +20,6 @@ import static com.android.settings.slices.CustomSliceRegistry.MEDIA_OUTPUT_INDIC
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;
@@ -30,13 +29,13 @@ 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 com.android.settings.overlay.FeatureFactory;
import com.google.android.setupdesign.DividerItemDecoration;
import java.util.ArrayList;
import java.util.List;
/**
@@ -45,14 +44,15 @@ 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
@@ -62,67 +62,60 @@ public class PanelSlicesAdapter
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 */);
})
);

View File

@@ -0,0 +1,83 @@
/*
* 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;
}
}

View File

@@ -26,6 +26,7 @@ import static org.mockito.Mockito.verify;
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;
@@ -81,12 +82,16 @@ public class PanelFragmentTest {
}
@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

View File

@@ -23,41 +23,53 @@ 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 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());
@@ -76,12 +88,22 @@ public class PanelSlicesAdapterTest {
.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);
@@ -89,24 +111,11 @@ public class PanelSlicesAdapterTest {
assertThat(viewHolder.sliceView).isNotNull();
}
@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 =
@@ -120,32 +129,10 @@ public class PanelSlicesAdapterTest {
@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 =

View File

@@ -0,0 +1,92 @@
/*
* 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();
}
}