New keyguard slice data structure

Using androidx package and new API, also splitting content into
multiple views for better animation support.

Bug: 64155983
Test: visual, see data from provider propagate to AoD
Change-Id: I74b5511d582e7ec1f6ffe5dbc5595f54b9ccb202
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSliceView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSliceView.java
index cb3d59c..b9bf80d 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSliceView.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSliceView.java
@@ -17,38 +17,56 @@
 package com.android.keyguard;
 
 import android.app.PendingIntent;
-import android.app.slice.Slice;
-import android.app.slice.SliceItem;
-import android.app.slice.SliceQuery;
+import android.arch.lifecycle.LiveData;
+import android.arch.lifecycle.Observer;
 import android.content.Context;
-import android.database.ContentObserver;
+import android.graphics.Canvas;
 import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.drawable.Drawable;
 import android.net.Uri;
-import android.os.Handler;
+import android.provider.Settings;
 import android.util.AttributeSet;
+import android.util.Log;
+import android.view.View;
+import android.widget.Button;
 import android.widget.LinearLayout;
 import android.widget.TextView;
 
 import com.android.internal.graphics.ColorUtils;
+import com.android.settingslib.Utils;
+import com.android.systemui.Dependency;
 import com.android.systemui.R;
 import com.android.systemui.keyguard.KeyguardSliceProvider;
+import com.android.systemui.tuner.TunerService;
 
-import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.function.Consumer;
+
+import androidx.app.slice.Slice;
+import androidx.app.slice.SliceItem;
+import androidx.app.slice.core.SliceQuery;
+import androidx.app.slice.widget.SliceLiveData;
 
 /**
  * View visible under the clock on the lock screen and AoD.
  */
-public class KeyguardSliceView extends LinearLayout {
+public class KeyguardSliceView extends LinearLayout implements View.OnClickListener,
+        Observer<Slice>, TunerService.Tunable {
 
-    private final Uri mKeyguardSliceUri;
+    private static final String TAG = "KeyguardSliceView";
+    private final HashMap<View, PendingIntent> mClickActions;
+    private Uri mKeyguardSliceUri;
     private TextView mTitle;
-    private TextView mText;
-    private Slice mSlice;
-    private PendingIntent mSliceAction;
+    private LinearLayout mRow;
     private int mTextColor;
     private float mDarkAmount = 0;
 
-    private final ContentObserver mObserver;
+    private LiveData<Slice> mLiveData;
+    private int mIconSize;
+    private Consumer<Boolean> mListener;
+    private boolean mHasHeader;
 
     public KeyguardSliceView(Context context) {
         this(context, null, 0);
@@ -60,16 +78,20 @@
 
     public KeyguardSliceView(Context context, AttributeSet attrs, int defStyle) {
         super(context, attrs, defStyle);
-        mObserver = new KeyguardSliceObserver(new Handler());
-        mKeyguardSliceUri = Uri.parse(KeyguardSliceProvider.KEYGUARD_SLICE_URI);;
+
+        TunerService tunerService = Dependency.get(TunerService.class);
+        tunerService.addTunable(this, Settings.Secure.KEYGUARD_SLICE_URI);
+
+        mClickActions = new HashMap<>();
     }
 
     @Override
     protected void onFinishInflate() {
         super.onFinishInflate();
         mTitle = findViewById(R.id.title);
-        mText = findViewById(R.id.text);
-        mTextColor = mTitle.getCurrentTextColor();
+        mRow = findViewById(R.id.row);
+        mTextColor = Utils.getColorAttr(mContext, R.attr.wallpaperTextColor);
+        mIconSize = (int) mContext.getResources().getDimension(R.dimen.widget_icon_size);
     }
 
     @Override
@@ -77,57 +99,103 @@
         super.onAttachedToWindow();
 
         // Set initial content
-        showSlice(Slice.bindSlice(getContext().getContentResolver(), mKeyguardSliceUri,
-                Collections.emptyList()));
+        showSlice(Slice.bindSlice(getContext(), mKeyguardSliceUri));
 
         // Make sure we always have the most current slice
-        getContext().getContentResolver().registerContentObserver(mKeyguardSliceUri,
-                false /* notifyDescendants */, mObserver);
+        mLiveData.observeForever(this);
     }
 
     @Override
     protected void onDetachedFromWindow() {
         super.onDetachedFromWindow();
 
-        getContext().getContentResolver().unregisterContentObserver(mObserver);
+        mLiveData.removeObserver(this);
     }
 
     private void showSlice(Slice slice) {
-        // Items will be wrapped into an action when they have tap targets.
-        SliceItem actionSlice = SliceQuery.find(slice, SliceItem.FORMAT_ACTION);
-        if (actionSlice != null) {
-            mSlice = actionSlice.getSlice();
-            mSliceAction = actionSlice.getAction();
-        } else {
-            mSlice = slice;
-            mSliceAction = null;
-        }
 
-        if (mSlice == null) {
-            setVisibility(GONE);
-            return;
-        }
+        // Main area
+        SliceItem mainItem = SliceQuery.find(slice, android.app.slice.SliceItem.FORMAT_SLICE,
+                null /* hints */, new String[]{android.app.slice.Slice.HINT_LIST_ITEM});
+        mHasHeader = mainItem != null;
 
-        SliceItem title = SliceQuery.find(mSlice, SliceItem.FORMAT_TEXT, Slice.HINT_TITLE, null);
-        if (title == null) {
+        List<SliceItem> subItems = SliceQuery.findAll(slice,
+                android.app.slice.SliceItem.FORMAT_SLICE,
+                new String[]{android.app.slice.Slice.HINT_LIST_ITEM},
+                null /* nonHints */);
+
+        if (!mHasHeader) {
             mTitle.setVisibility(GONE);
         } else {
             mTitle.setVisibility(VISIBLE);
-            mTitle.setText(title.getText());
+            SliceItem mainTitle = SliceQuery.find(mainItem.getSlice(),
+                    android.app.slice.SliceItem.FORMAT_TEXT,
+                    new String[]{android.app.slice.Slice.HINT_TITLE},
+                    null /* nonHints */);
+            mTitle.setText(mainTitle.getText());
         }
 
-        SliceItem text = SliceQuery.find(mSlice, SliceItem.FORMAT_TEXT, null, Slice.HINT_TITLE);
-        if (text == null) {
-            mText.setVisibility(GONE);
-        } else {
-            mText.setVisibility(VISIBLE);
-            mText.setText(text.getText());
+        mClickActions.clear();
+        final int subItemsCount = subItems.size();
+
+        for (int i = 0; i < subItemsCount; i++) {
+            SliceItem item = subItems.get(i);
+            final Uri itemTag = item.getSlice().getUri();
+            // Try to reuse the view if already exists in the layout
+            KeyguardSliceButton button = mRow.findViewWithTag(itemTag);
+            if (button == null) {
+                button = new KeyguardSliceButton(mContext);
+                button.setTextColor(mTextColor);
+                button.setTag(itemTag);
+            } else {
+                mRow.removeView(button);
+            }
+            button.setHasDivider(i < subItemsCount - 1);
+            mRow.addView(button, i);
+
+            PendingIntent pendingIntent;
+            try {
+                pendingIntent = item.getAction();
+            } catch (RuntimeException e) {
+                Log.w(TAG, "Cannot retrieve action from keyguard slice", e);
+                pendingIntent = null;
+            }
+            mClickActions.put(button, pendingIntent);
+
+            SliceItem title = SliceQuery.find(item.getSlice(),
+                    android.app.slice.SliceItem.FORMAT_TEXT,
+                    new String[]{android.app.slice.Slice.HINT_TITLE},
+                    null /* nonHints */);
+            button.setText(title.getText());
+
+            Drawable iconDrawable = null;
+            SliceItem icon = SliceQuery.find(item.getSlice(),
+                    android.app.slice.SliceItem.FORMAT_IMAGE);
+            if (icon != null) {
+                iconDrawable = icon.getIcon().loadDrawable(mContext);
+                final int width = (int) (iconDrawable.getIntrinsicWidth()
+                        / (float) iconDrawable.getIntrinsicHeight() * mIconSize);
+                iconDrawable.setBounds(0, 0, Math.max(width, 1), mIconSize);
+            }
+            button.setCompoundDrawablesRelative(iconDrawable, null, null, null);
+            button.setOnClickListener(this);
         }
 
-        final int visibility = title == null && text == null ? GONE : VISIBLE;
+        // Removing old views
+        for (int i = 0; i < mRow.getChildCount(); i++) {
+            View child = mRow.getChildAt(i);
+            if (!mClickActions.containsKey(child)) {
+                mRow.removeView(child);
+                i--;
+            }
+        }
+
+        final int visibility = mHasHeader || subItemsCount > 0 ? VISIBLE : GONE;
         if (visibility != getVisibility()) {
             setVisibility(visibility);
         }
+
+        mListener.accept(mHasHeader);
     }
 
     public void setDark(float darkAmount) {
@@ -135,30 +203,113 @@
         updateTextColors();
     }
 
-    public void setTextColor(int textColor) {
-        mTextColor = textColor;
-    }
-
     private void updateTextColors() {
         final int blendedColor = ColorUtils.blendARGB(mTextColor, Color.WHITE, mDarkAmount);
         mTitle.setTextColor(blendedColor);
-        mText.setTextColor(blendedColor);
+        int childCount = mRow.getChildCount();
+        for (int i = 0; i < childCount; i++) {
+            View v = mRow.getChildAt(i);
+            if (v instanceof Button) {
+                ((Button) v).setTextColor(blendedColor);
+            }
+        }
     }
 
-    private class KeyguardSliceObserver extends ContentObserver {
-        KeyguardSliceObserver(Handler handler) {
-            super(handler);
+    @Override
+    public void onClick(View v) {
+        final PendingIntent action = mClickActions.get(v);
+        if (action != null) {
+            try {
+                action.send();
+            } catch (PendingIntent.CanceledException e) {
+                Log.i(TAG, "Pending intent cancelled, nothing to launch", e);
+            }
+        }
+    }
+
+    public void setListener(Consumer<Boolean> listener) {
+        mListener = listener;
+    }
+
+    public boolean hasHeader() {
+        return mHasHeader;
+    }
+
+    /**
+     * LiveData observer lifecycle.
+     * @param slice the new slice content.
+     */
+    @Override
+    public void onChanged(Slice slice) {
+        showSlice(slice);
+    }
+
+    @Override
+    public void onTuningChanged(String key, String newValue) {
+        setupUri(newValue);
+    }
+
+    public void setupUri(String uriString) {
+        if (uriString == null) {
+            uriString = KeyguardSliceProvider.KEYGUARD_SLICE_URI;
+        }
+
+        boolean wasObserving = false;
+        if (mLiveData != null && mLiveData.hasActiveObservers()) {
+            wasObserving = true;
+            mLiveData.removeObserver(this);
+        }
+
+        mKeyguardSliceUri = Uri.parse(uriString);
+        mLiveData = SliceLiveData.fromUri(mContext, mKeyguardSliceUri);
+
+        if (wasObserving) {
+            mLiveData.observeForever(this);
+            showSlice(Slice.bindSlice(getContext(), mKeyguardSliceUri));
+        }
+    }
+
+    /**
+     * Representation of an item that appears under the clock on main keyguard message.
+     * Shows optional separator.
+     */
+    private class KeyguardSliceButton extends Button {
+
+        private final Paint mPaint;
+        private boolean mHasDivider;
+
+        public KeyguardSliceButton(Context context) {
+            super(context, null /* attrs */,
+                    com.android.keyguard.R.style.TextAppearance_Keyguard_Secondary);
+            mPaint = new Paint();
+            mPaint.setStyle(Paint.Style.STROKE);
+            float dividerWidth = context.getResources()
+                    .getDimension(R.dimen.widget_separator_thickness);
+            mPaint.setStrokeWidth(dividerWidth);
+            int horizontalPadding = (int) context.getResources()
+                    .getDimension(R.dimen.widget_horizontal_padding);
+            setPadding(horizontalPadding, 0, horizontalPadding, 0);
+            setCompoundDrawablePadding((int) context.getResources()
+                    .getDimension(R.dimen.widget_icon_padding));
+        }
+
+        public void setHasDivider(boolean hasDivider) {
+            mHasDivider = hasDivider;
         }
 
         @Override
-        public void onChange(boolean selfChange) {
-            this.onChange(selfChange, null);
+        public void setTextColor(int color) {
+            super.setTextColor(color);
+            mPaint.setColor(color);
         }
 
         @Override
-        public void onChange(boolean selfChange, Uri uri) {
-            showSlice(Slice.bindSlice(getContext().getContentResolver(), mKeyguardSliceUri,
-                    Collections.emptyList()));
+        protected void onDraw(Canvas canvas) {
+            super.onDraw(canvas);
+            if (mHasDivider) {
+                final int lineX = getLayoutDirection() == LAYOUT_DIRECTION_RTL ? 0 : getWidth();
+                canvas.drawLine(lineX, 0, lineX, getHeight(), mPaint);
+            }
         }
     }
 }