Merge "Add entry animation to Settings Panels." into qt-dev
This commit is contained in:
@@ -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>
|
@@ -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"/>
|
@@ -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>
|
||||
|
@@ -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
|
||||
|
@@ -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 */);
|
||||
})
|
||||
);
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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
|
||||
|
@@ -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 =
|
||||
|
@@ -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();
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user