1 /* 2 * Copyright (C) 2018 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.settings.panel; 18 19 import static android.app.slice.Slice.HINT_ERROR; 20 import static android.app.slice.SliceItem.FORMAT_SLICE; 21 import static android.view.accessibility.AccessibilityNodeInfo.ACTION_CLICK; 22 23 import android.app.settings.SettingsEnums; 24 import android.content.Context; 25 import android.net.Uri; 26 import android.view.LayoutInflater; 27 import android.view.View; 28 import android.view.ViewGroup; 29 import android.view.accessibility.AccessibilityNodeInfo; 30 import android.widget.LinearLayout; 31 32 import androidx.annotation.NonNull; 33 import androidx.annotation.VisibleForTesting; 34 import androidx.lifecycle.LiveData; 35 import androidx.recyclerview.widget.RecyclerView; 36 import androidx.slice.Slice; 37 import androidx.slice.SliceItem; 38 import androidx.slice.widget.SliceView; 39 40 import com.android.settings.R; 41 import com.android.settings.overlay.FeatureFactory; 42 43 import com.google.android.setupdesign.DividerItemDecoration; 44 45 import java.util.ArrayList; 46 import java.util.List; 47 import java.util.Map; 48 49 /** 50 * RecyclerView adapter for Slices in Settings Panels. 51 */ 52 public class PanelSlicesAdapter 53 extends RecyclerView.Adapter<PanelSlicesAdapter.SliceRowViewHolder> { 54 55 /** 56 * Maximum number of slices allowed on the panel view. 57 */ 58 @VisibleForTesting 59 static final int MAX_NUM_OF_SLICES = 9; 60 61 private final List<LiveData<Slice>> mSliceLiveData; 62 private final int mMetricsCategory; 63 private final PanelFragment mPanelFragment; 64 PanelSlicesAdapter( PanelFragment fragment, Map<Uri, LiveData<Slice>> sliceLiveData, int metricsCategory)65 public PanelSlicesAdapter( 66 PanelFragment fragment, Map<Uri, LiveData<Slice>> sliceLiveData, int metricsCategory) { 67 mPanelFragment = fragment; 68 mSliceLiveData = new ArrayList<>(sliceLiveData.values()); 69 mMetricsCategory = metricsCategory; 70 } 71 72 @NonNull 73 @Override onCreateViewHolder(@onNull ViewGroup viewGroup, int viewType)74 public SliceRowViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int viewType) { 75 final Context context = viewGroup.getContext(); 76 final LayoutInflater inflater = LayoutInflater.from(context); 77 final View view; 78 if (viewType == PanelContent.VIEW_TYPE_SLIDER) { 79 view = inflater.inflate(R.layout.panel_slice_slider_row, viewGroup, false); 80 } else { 81 view = inflater.inflate(R.layout.panel_slice_row, viewGroup, false); 82 } 83 return new SliceRowViewHolder(view); 84 } 85 86 @Override onBindViewHolder(@onNull SliceRowViewHolder sliceRowViewHolder, int position)87 public void onBindViewHolder(@NonNull SliceRowViewHolder sliceRowViewHolder, int position) { 88 sliceRowViewHolder.onBind(mSliceLiveData.get(position).getValue()); 89 } 90 91 /** 92 * Return the number of available items in the adapter with max number of slices enforced. 93 */ 94 @Override getItemCount()95 public int getItemCount() { 96 return Math.min(mSliceLiveData.size(), MAX_NUM_OF_SLICES); 97 } 98 99 @Override getItemViewType(int position)100 public int getItemViewType(int position) { 101 return mPanelFragment.getPanelViewType(); 102 } 103 104 /** 105 * Return the available data from the adapter. If the number of Slices over the max number 106 * allowed, the list will only have the first MAX_NUM_OF_SLICES of slices. 107 */ 108 @VisibleForTesting getData()109 List<LiveData<Slice>> getData() { 110 return mSliceLiveData.subList(0, getItemCount()); 111 } 112 113 /** 114 * ViewHolder for binding Slices to SliceViews. 115 */ 116 public class SliceRowViewHolder extends RecyclerView.ViewHolder 117 implements DividerItemDecoration.DividedViewHolder { 118 119 private static final int ROW_VIEW_ID = androidx.slice.view.R.id.row_view; 120 private static final int ROW_VIEW_TAG = R.id.tag_row_view; 121 122 @VisibleForTesting 123 final SliceView sliceView; 124 @VisibleForTesting 125 final LinearLayout mSliceSliderLayout; 126 SliceRowViewHolder(View view)127 public SliceRowViewHolder(View view) { 128 super(view); 129 sliceView = view.findViewById(R.id.slice_view); 130 sliceView.setMode(SliceView.MODE_LARGE); 131 sliceView.setShowTitleItems(true); 132 sliceView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO); 133 mSliceSliderLayout = view.findViewById(R.id.slice_slider_layout); 134 } 135 136 /** 137 * Called when the view is displayed. 138 */ onBind(Slice slice)139 public void onBind(Slice slice) { 140 // Hides slice which reports with error hint or not contain any slice sub-item. 141 if (slice == null || !isValidSlice(slice)) { 142 updateActionLabel(); 143 sliceView.setVisibility(View.GONE); 144 return; 145 } else { 146 sliceView.setSlice(slice); 147 sliceView.setVisibility(View.VISIBLE); 148 } 149 150 // Add divider for the end icon 151 sliceView.setShowActionDividers(true); 152 153 // Log Panel interaction 154 sliceView.setOnSliceActionListener( 155 ((eventInfo, sliceItem) -> { 156 FeatureFactory.getFactory(sliceView.getContext()) 157 .getMetricsFeatureProvider() 158 .action(0 /* attribution */, 159 SettingsEnums.ACTION_PANEL_INTERACTION, 160 mMetricsCategory, 161 slice.getUri().getLastPathSegment() 162 /* log key */, 163 eventInfo.actionType /* value */); 164 }) 165 ); 166 updateActionLabel(); 167 } 168 169 /** 170 * Either set the action label if the row view is inflated into Slice, or set a listener to 171 * do so later when the row is available. 172 */ updateActionLabel()173 @VisibleForTesting void updateActionLabel() { 174 if (sliceView == null) { 175 return; 176 } 177 178 final LinearLayout llRow = sliceView.findViewById(ROW_VIEW_ID); 179 if (llRow != null) { 180 // Just set the label for the row. if is already laid out, there is no need for 181 // listening to future changes. 182 setActionLabel(llRow); 183 } else { // set the accessibility delegate when row_view is laid out 184 Object alreadyAddedListener = sliceView.getTag(ROW_VIEW_TAG); 185 if (alreadyAddedListener != null) { 186 return; 187 } 188 sliceView.setTag(ROW_VIEW_TAG, new Object()); 189 sliceView.addOnLayoutChangeListener(new View.OnLayoutChangeListener() { 190 @Override 191 public void onLayoutChange(View v, int left, int top, int right, int bottom, 192 int oldLeft, int oldTop, int oldRight, int oldBottom) { 193 LinearLayout row = sliceView.findViewById(ROW_VIEW_ID); 194 if (row != null) { 195 setActionLabel(row); 196 sliceView.removeOnLayoutChangeListener(this); 197 } 198 } 199 }); 200 } 201 } 202 203 /** 204 * Update the action label for TalkBack to be more specific 205 * @param view the RowView within the Slice 206 */ setActionLabel(View view)207 @VisibleForTesting void setActionLabel(View view) { 208 view.setAccessibilityDelegate(new View.AccessibilityDelegate() { 209 @Override 210 public void onInitializeAccessibilityNodeInfo(View host, 211 AccessibilityNodeInfo info) { 212 super.onInitializeAccessibilityNodeInfo(host, info); 213 214 AccessibilityNodeInfo.AccessibilityAction customClick = 215 new AccessibilityNodeInfo.AccessibilityAction(ACTION_CLICK, host 216 .getResources() 217 .getString(R.string.accessibility_action_label_panel_slice)); 218 info.addAction(customClick); 219 } 220 }); 221 } 222 isValidSlice(Slice slice)223 private boolean isValidSlice(Slice slice) { 224 if (slice.getHints().contains(HINT_ERROR)) { 225 return false; 226 } 227 for (SliceItem item : slice.getItems()) { 228 if (item.getFormat().equals(FORMAT_SLICE)) { 229 return true; 230 } 231 } 232 return false; 233 } 234 235 @Override isDividerAllowedAbove()236 public boolean isDividerAllowedAbove() { 237 return false; 238 } 239 240 @Override isDividerAllowedBelow()241 public boolean isDividerAllowedBelow() { 242 return false; 243 } 244 } 245 } 246