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