/* * Copyright (C) 2018 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.homepage.contextualcards.slices; import static android.app.slice.Slice.HINT_ERROR; import android.content.ContentResolver; import android.content.Context; import android.net.Uri; import android.os.Bundle; import android.util.ArrayMap; import android.util.ArraySet; import android.util.Log; import android.view.View; import android.widget.Button; import androidx.annotation.LayoutRes; import androidx.annotation.VisibleForTesting; import androidx.core.view.AccessibilityDelegateCompat; import androidx.core.view.ViewCompat; import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; import androidx.lifecycle.Lifecycle; import androidx.lifecycle.LifecycleObserver; import androidx.lifecycle.LifecycleOwner; import androidx.lifecycle.LiveData; import androidx.lifecycle.OnLifecycleEvent; import androidx.recyclerview.widget.RecyclerView; import androidx.slice.Slice; import androidx.slice.widget.SliceLiveData; import com.android.settings.R; import com.android.settings.homepage.contextualcards.CardContentProvider; import com.android.settings.homepage.contextualcards.ContextualCard; import com.android.settings.homepage.contextualcards.ContextualCardRenderer; import com.android.settings.homepage.contextualcards.ControllerRendererPool; import com.android.settings.homepage.contextualcards.slices.SliceFullCardRendererHelper.SliceViewHolder; import com.android.settingslib.utils.ThreadUtils; import java.util.Map; import java.util.Set; /** * Card renderer for {@link ContextualCard} built as slice full card or slice half card. */ public class SliceContextualCardRenderer implements ContextualCardRenderer, LifecycleObserver { public static final int VIEW_TYPE_FULL_WIDTH = R.layout.contextual_slice_full_tile; public static final int VIEW_TYPE_HALF_WIDTH = R.layout.contextual_slice_half_tile; public static final int VIEW_TYPE_STICKY = R.layout.contextual_slice_sticky_tile; private static final String TAG = "SliceCardRenderer"; @VisibleForTesting final Map> mSliceLiveDataMap; @VisibleForTesting final Set mFlippedCardSet; private final Context mContext; private final LifecycleOwner mLifecycleOwner; private final ControllerRendererPool mControllerRendererPool; private final SliceFullCardRendererHelper mFullCardHelper; private final SliceHalfCardRendererHelper mHalfCardHelper; public SliceContextualCardRenderer(Context context, LifecycleOwner lifecycleOwner, ControllerRendererPool controllerRendererPool) { mContext = context; mLifecycleOwner = lifecycleOwner; mSliceLiveDataMap = new ArrayMap<>(); mControllerRendererPool = controllerRendererPool; mFlippedCardSet = new ArraySet<>(); mLifecycleOwner.getLifecycle().addObserver(this); mFullCardHelper = new SliceFullCardRendererHelper(context); mHalfCardHelper = new SliceHalfCardRendererHelper(context); } @Override public RecyclerView.ViewHolder createViewHolder(View view, @LayoutRes int viewType) { if (viewType == VIEW_TYPE_HALF_WIDTH) { return mHalfCardHelper.createViewHolder(view); } return mFullCardHelper.createViewHolder(view); } @Override public void bindView(RecyclerView.ViewHolder holder, ContextualCard card) { final Uri uri = card.getSliceUri(); if (!ContentResolver.SCHEME_CONTENT.equals(uri.getScheme())) { Log.w(TAG, "Invalid uri, skipping slice: " + uri); return; } // Show cached slice first before slice binding completed to avoid jank. if (holder.getItemViewType() != VIEW_TYPE_HALF_WIDTH) { ((SliceViewHolder) holder).sliceView.setSlice(card.getSlice()); } LiveData sliceLiveData = mSliceLiveDataMap.get(uri); if (sliceLiveData == null) { sliceLiveData = SliceLiveData.fromUri(mContext, uri, (int type, Throwable source) -> { // onSliceError doesn't handle error Slices. Log.w(TAG, "Slice may be null. uri = " + uri + ", error = " + type); ThreadUtils.postOnMainThread( () -> mSliceLiveDataMap.get(uri).removeObservers(mLifecycleOwner)); mContext.getContentResolver() .notifyChange(CardContentProvider.REFRESH_CARD_URI, null); }); mSliceLiveDataMap.put(uri, sliceLiveData); } final View swipeBackground = holder.itemView.findViewById(R.id.dismissal_swipe_background); sliceLiveData.removeObservers(mLifecycleOwner); // set the background to GONE in case the holder is reused. if (swipeBackground != null) { swipeBackground.setVisibility(View.GONE); } sliceLiveData.observe(mLifecycleOwner, slice -> { if (slice == null) { // The logic handling this case is in OnErrorListener. Adding this check is to // prevent from NPE when it calls .hasHint(). return; } if (slice.hasHint(HINT_ERROR)) { Log.w(TAG, "Slice has HINT_ERROR, skipping rendering. uri=" + slice.getUri()); mSliceLiveDataMap.get(slice.getUri()).removeObservers(mLifecycleOwner); mContext.getContentResolver().notifyChange(CardContentProvider.REFRESH_CARD_URI, null); return; } if (holder.getItemViewType() == VIEW_TYPE_HALF_WIDTH) { mHalfCardHelper.bindView(holder, card, slice); } else { mFullCardHelper.bindView(holder, card, slice); } if (swipeBackground != null) { swipeBackground.setVisibility(View.VISIBLE); } }); if (holder.getItemViewType() != VIEW_TYPE_STICKY) { initDismissalActions(holder, card); if (card.isPendingDismiss()) { showDismissalView(holder); mFlippedCardSet.add(holder); } } } private void initDismissalActions(RecyclerView.ViewHolder holder, ContextualCard card) { final Button btnKeep = holder.itemView.findViewById(R.id.keep); btnKeep.setOnClickListener(v -> { mFlippedCardSet.remove(holder); resetCardView(holder); }); final Button btnRemove = holder.itemView.findViewById(R.id.remove); btnRemove.setOnClickListener(v -> { mControllerRendererPool.getController(mContext, card.getCardType()).onDismissed(card); mFlippedCardSet.remove(holder); resetCardView(holder); mSliceLiveDataMap.get(card.getSliceUri()).removeObservers(mLifecycleOwner); }); ViewCompat.setAccessibilityDelegate(getInitialView(holder), new AccessibilityDelegateCompat() { @Override public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) { super.onInitializeAccessibilityNodeInfo(host, info); info.addAction(AccessibilityNodeInfoCompat.ACTION_DISMISS); info.setDismissable(true); } @Override public boolean performAccessibilityAction(View host, int action, Bundle args) { if (action == AccessibilityNodeInfoCompat.ACTION_DISMISS) { mControllerRendererPool.getController(mContext, card.getCardType()).onDismissed(card); } return super.performAccessibilityAction(host, action, args); } }); } @OnLifecycleEvent(Lifecycle.Event.ON_STOP) public void onStop() { mFlippedCardSet.forEach(holder -> resetCardView(holder)); mFlippedCardSet.clear(); } private void resetCardView(RecyclerView.ViewHolder holder) { holder.itemView.findViewById(R.id.dismissal_view).setVisibility(View.GONE); getInitialView(holder).setVisibility(View.VISIBLE); } private void showDismissalView(RecyclerView.ViewHolder holder) { holder.itemView.findViewById(R.id.dismissal_view).setVisibility(View.VISIBLE); getInitialView(holder).setVisibility(View.INVISIBLE); } private View getInitialView(RecyclerView.ViewHolder viewHolder) { if (viewHolder.getItemViewType() == VIEW_TYPE_HALF_WIDTH) { return ((SliceHalfCardRendererHelper.HalfCardViewHolder) viewHolder).content; } return ((SliceFullCardRendererHelper.SliceViewHolder) viewHolder).sliceView; } }