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.homepage.contextualcards.slices; 18 19 import static android.app.slice.Slice.HINT_ERROR; 20 21 import android.content.ContentResolver; 22 import android.content.Context; 23 import android.net.Uri; 24 import android.os.Bundle; 25 import android.util.ArrayMap; 26 import android.util.ArraySet; 27 import android.util.Log; 28 import android.view.View; 29 import android.widget.Button; 30 31 import androidx.annotation.LayoutRes; 32 import androidx.annotation.VisibleForTesting; 33 import androidx.core.view.AccessibilityDelegateCompat; 34 import androidx.core.view.ViewCompat; 35 import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; 36 import androidx.lifecycle.Lifecycle; 37 import androidx.lifecycle.LifecycleObserver; 38 import androidx.lifecycle.LifecycleOwner; 39 import androidx.lifecycle.LiveData; 40 import androidx.lifecycle.OnLifecycleEvent; 41 import androidx.recyclerview.widget.RecyclerView; 42 import androidx.slice.Slice; 43 import androidx.slice.widget.SliceLiveData; 44 45 import com.android.settings.R; 46 import com.android.settings.homepage.contextualcards.CardContentProvider; 47 import com.android.settings.homepage.contextualcards.ContextualCard; 48 import com.android.settings.homepage.contextualcards.ContextualCardRenderer; 49 import com.android.settings.homepage.contextualcards.ControllerRendererPool; 50 import com.android.settings.homepage.contextualcards.slices.SliceFullCardRendererHelper.SliceViewHolder; 51 import com.android.settingslib.utils.ThreadUtils; 52 53 import java.util.Map; 54 import java.util.Set; 55 56 /** 57 * Card renderer for {@link ContextualCard} built as slice full card or slice half card. 58 */ 59 public class SliceContextualCardRenderer implements ContextualCardRenderer, LifecycleObserver { 60 public static final int VIEW_TYPE_FULL_WIDTH = R.layout.contextual_slice_full_tile; 61 public static final int VIEW_TYPE_HALF_WIDTH = R.layout.contextual_slice_half_tile; 62 public static final int VIEW_TYPE_STICKY = R.layout.contextual_slice_sticky_tile; 63 64 private static final String TAG = "SliceCardRenderer"; 65 66 @VisibleForTesting 67 final Map<Uri, LiveData<Slice>> mSliceLiveDataMap; 68 @VisibleForTesting 69 final Set<RecyclerView.ViewHolder> mFlippedCardSet; 70 71 private final Context mContext; 72 private final LifecycleOwner mLifecycleOwner; 73 private final ControllerRendererPool mControllerRendererPool; 74 private final SliceFullCardRendererHelper mFullCardHelper; 75 private final SliceHalfCardRendererHelper mHalfCardHelper; 76 SliceContextualCardRenderer(Context context, LifecycleOwner lifecycleOwner, ControllerRendererPool controllerRendererPool)77 public SliceContextualCardRenderer(Context context, LifecycleOwner lifecycleOwner, 78 ControllerRendererPool controllerRendererPool) { 79 mContext = context; 80 mLifecycleOwner = lifecycleOwner; 81 mSliceLiveDataMap = new ArrayMap<>(); 82 mControllerRendererPool = controllerRendererPool; 83 mFlippedCardSet = new ArraySet<>(); 84 mLifecycleOwner.getLifecycle().addObserver(this); 85 mFullCardHelper = new SliceFullCardRendererHelper(context); 86 mHalfCardHelper = new SliceHalfCardRendererHelper(context); 87 } 88 89 @Override createViewHolder(View view, @LayoutRes int viewType)90 public RecyclerView.ViewHolder createViewHolder(View view, @LayoutRes int viewType) { 91 if (viewType == VIEW_TYPE_HALF_WIDTH) { 92 return mHalfCardHelper.createViewHolder(view); 93 } 94 return mFullCardHelper.createViewHolder(view); 95 } 96 97 @Override bindView(RecyclerView.ViewHolder holder, ContextualCard card)98 public void bindView(RecyclerView.ViewHolder holder, ContextualCard card) { 99 final Uri uri = card.getSliceUri(); 100 101 if (!ContentResolver.SCHEME_CONTENT.equals(uri.getScheme())) { 102 Log.w(TAG, "Invalid uri, skipping slice: " + uri); 103 return; 104 } 105 106 // Show cached slice first before slice binding completed to avoid jank. 107 if (holder.getItemViewType() != VIEW_TYPE_HALF_WIDTH) { 108 ((SliceViewHolder) holder).sliceView.setSlice(card.getSlice()); 109 } 110 111 LiveData<Slice> sliceLiveData = mSliceLiveDataMap.get(uri); 112 113 if (sliceLiveData == null) { 114 sliceLiveData = SliceLiveData.fromUri(mContext, uri, 115 (int type, Throwable source) -> { 116 // onSliceError doesn't handle error Slices. 117 Log.w(TAG, "Slice may be null. uri = " + uri + ", error = " + type); 118 ThreadUtils.postOnMainThread( 119 () -> mSliceLiveDataMap.get(uri).removeObservers(mLifecycleOwner)); 120 mContext.getContentResolver() 121 .notifyChange(CardContentProvider.REFRESH_CARD_URI, null); 122 }); 123 mSliceLiveDataMap.put(uri, sliceLiveData); 124 } 125 126 final View swipeBackground = holder.itemView.findViewById(R.id.dismissal_swipe_background); 127 sliceLiveData.removeObservers(mLifecycleOwner); 128 // set the background to GONE in case the holder is reused. 129 if (swipeBackground != null) { 130 swipeBackground.setVisibility(View.GONE); 131 } 132 sliceLiveData.observe(mLifecycleOwner, slice -> { 133 if (slice == null) { 134 // The logic handling this case is in OnErrorListener. Adding this check is to 135 // prevent from NPE when it calls .hasHint(). 136 return; 137 } 138 if (slice.hasHint(HINT_ERROR)) { 139 Log.w(TAG, "Slice has HINT_ERROR, skipping rendering. uri=" + slice.getUri()); 140 mSliceLiveDataMap.get(slice.getUri()).removeObservers(mLifecycleOwner); 141 mContext.getContentResolver().notifyChange(CardContentProvider.REFRESH_CARD_URI, 142 null); 143 return; 144 } 145 146 if (holder.getItemViewType() == VIEW_TYPE_HALF_WIDTH) { 147 mHalfCardHelper.bindView(holder, card, slice); 148 } else { 149 mFullCardHelper.bindView(holder, card, slice); 150 } 151 if (swipeBackground != null) { 152 swipeBackground.setVisibility(View.VISIBLE); 153 } 154 }); 155 156 if (holder.getItemViewType() != VIEW_TYPE_STICKY) { 157 initDismissalActions(holder, card); 158 159 if (card.isPendingDismiss()) { 160 showDismissalView(holder); 161 mFlippedCardSet.add(holder); 162 } 163 } 164 } 165 initDismissalActions(RecyclerView.ViewHolder holder, ContextualCard card)166 private void initDismissalActions(RecyclerView.ViewHolder holder, ContextualCard card) { 167 final Button btnKeep = holder.itemView.findViewById(R.id.keep); 168 btnKeep.setOnClickListener(v -> { 169 mFlippedCardSet.remove(holder); 170 resetCardView(holder); 171 }); 172 173 final Button btnRemove = holder.itemView.findViewById(R.id.remove); 174 btnRemove.setOnClickListener(v -> { 175 mControllerRendererPool.getController(mContext, card.getCardType()).onDismissed(card); 176 mFlippedCardSet.remove(holder); 177 resetCardView(holder); 178 mSliceLiveDataMap.get(card.getSliceUri()).removeObservers(mLifecycleOwner); 179 }); 180 181 ViewCompat.setAccessibilityDelegate(getInitialView(holder), 182 new AccessibilityDelegateCompat() { 183 @Override 184 public void onInitializeAccessibilityNodeInfo(View host, 185 AccessibilityNodeInfoCompat info) { 186 super.onInitializeAccessibilityNodeInfo(host, info); 187 info.addAction(AccessibilityNodeInfoCompat.ACTION_DISMISS); 188 info.setDismissable(true); 189 } 190 191 @Override 192 public boolean performAccessibilityAction(View host, int action, Bundle args) { 193 if (action == AccessibilityNodeInfoCompat.ACTION_DISMISS) { 194 mControllerRendererPool.getController(mContext, 195 card.getCardType()).onDismissed(card); 196 } 197 return super.performAccessibilityAction(host, action, args); 198 } 199 }); 200 201 } 202 203 @OnLifecycleEvent(Lifecycle.Event.ON_STOP) onStop()204 public void onStop() { 205 mFlippedCardSet.forEach(holder -> resetCardView(holder)); 206 mFlippedCardSet.clear(); 207 } 208 resetCardView(RecyclerView.ViewHolder holder)209 private void resetCardView(RecyclerView.ViewHolder holder) { 210 holder.itemView.findViewById(R.id.dismissal_view).setVisibility(View.GONE); 211 getInitialView(holder).setVisibility(View.VISIBLE); 212 } 213 showDismissalView(RecyclerView.ViewHolder holder)214 private void showDismissalView(RecyclerView.ViewHolder holder) { 215 holder.itemView.findViewById(R.id.dismissal_view).setVisibility(View.VISIBLE); 216 getInitialView(holder).setVisibility(View.INVISIBLE); 217 } 218 getInitialView(RecyclerView.ViewHolder viewHolder)219 private View getInitialView(RecyclerView.ViewHolder viewHolder) { 220 if (viewHolder.getItemViewType() == VIEW_TYPE_HALF_WIDTH) { 221 return ((SliceHalfCardRendererHelper.HalfCardViewHolder) viewHolder).content; 222 } 223 return ((SliceFullCardRendererHelper.SliceViewHolder) viewHolder).sliceView; 224 } 225 } 226