Add LiveData support for Slices

Switch SliceView to be a static rendering that just has a setSlice.
Also make SliceView a LiveData.Observer, and provide LiveData
implementation for listening to Slice changes.

Test: slices sample app
Bug: 68378561
Change-Id: Ib451c6e26a3af0f5335596fb70658f55eee639f3
diff --git a/samples/SupportSliceDemos/src/main/java/com/example/androidx/slice/demos/SliceBrowser.java b/samples/SupportSliceDemos/src/main/java/com/example/androidx/slice/demos/SliceBrowser.java
index 8986630..a4f28c1 100644
--- a/samples/SupportSliceDemos/src/main/java/com/example/androidx/slice/demos/SliceBrowser.java
+++ b/samples/SupportSliceDemos/src/main/java/com/example/androidx/slice/demos/SliceBrowser.java
@@ -16,7 +16,8 @@
 
 package com.example.androidx.slice.demos;
 
-import android.app.Activity;
+import android.app.slice.Slice;
+import android.arch.lifecycle.LiveData;
 import android.content.ContentResolver;
 import android.content.pm.ActivityInfo;
 import android.content.pm.PackageInfo;
@@ -27,6 +28,8 @@
 import android.os.Bundle;
 import android.provider.BaseColumns;
 import android.support.annotation.RequiresApi;
+import android.support.v7.app.AppCompatActivity;
+import android.support.v7.widget.Toolbar;
 import android.util.ArrayMap;
 import android.util.Log;
 import android.view.Menu;
@@ -36,12 +39,12 @@
 import android.widget.CursorAdapter;
 import android.widget.SearchView;
 import android.widget.SimpleCursorAdapter;
-import android.widget.Toolbar;
 
 import java.util.ArrayList;
 import java.util.Comparator;
 import java.util.List;
 
+import androidx.app.slice.widget.SliceLiveData;
 import androidx.app.slice.widget.SliceView;
 
 /**
@@ -49,7 +52,7 @@
  * then displayed in the selected mode with SliceView.
  */
 @RequiresApi(api = 28)
-public class SliceBrowser extends Activity {
+public class SliceBrowser extends AppCompatActivity {
 
     private static final String TAG = "SlicePresenter";
 
@@ -61,6 +64,7 @@
     private SearchView mSearchView;
     private SimpleCursorAdapter mAdapter;
     private SubMenu mTypeMenu;
+    private LiveData<Slice> mSliceLiveData;
 
     @Override
     public void onCreate(Bundle savedInstanceState) {
@@ -68,7 +72,7 @@
         setContentView(R.layout.activity_layout);
 
         Toolbar toolbar = findViewById(R.id.search_toolbar);
-        setActionBar(toolbar);
+        setSupportActionBar(toolbar);
 
         // Shows the slice
         mContainer = findViewById(R.id.slice_preview);
@@ -155,6 +159,7 @@
 
     @Override
     protected void onSaveInstanceState(Bundle outState) {
+        super.onSaveInstanceState(outState);
         outState.putInt("SELECTED_MODE", mSelectedMode);
         outState.putString("SELECTED_QUERY", mSearchView.getQuery().toString());
     }
@@ -184,10 +189,14 @@
         if (ContentResolver.SCHEME_CONTENT.equals(uri.getScheme())) {
             SliceView v = new SliceView(getApplicationContext());
             v.setTag(uri);
+            if (mSliceLiveData != null) {
+                mSliceLiveData.removeObservers(this);
+            }
             mContainer.removeAllViews();
             mContainer.addView(v);
+            mSliceLiveData = SliceLiveData.fromUri(this, uri);
             v.setMode(mSelectedMode);
-            v.setSlice(uri);
+            mSliceLiveData.observe(this, v);
         } else {
             Log.w(TAG, "Invalid uri, skipping slice: " + uri);
         }
diff --git a/samples/SupportSliceDemos/src/main/res/layout/activity_layout.xml b/samples/SupportSliceDemos/src/main/res/layout/activity_layout.xml
index 833414c..6a087a3 100644
--- a/samples/SupportSliceDemos/src/main/res/layout/activity_layout.xml
+++ b/samples/SupportSliceDemos/src/main/res/layout/activity_layout.xml
@@ -35,7 +35,7 @@
             app:cardCornerRadius="2dp"
             app:cardBackgroundColor="?android:attr/colorBackground"
             app:cardElevation="2dp">
-            <Toolbar
+            <android.support.v7.widget.Toolbar
                 android:id="@+id/search_toolbar"
                 android:layout_width="match_parent"
                 android:layout_height="48dp"
@@ -51,7 +51,7 @@
                     android:imeOptions="actionSearch|flagNoExtractUi"
                     android:queryHint="content://..."
                     android:searchIcon="@null"/>
-            </Toolbar>
+            </android.support.v7.widget.Toolbar>
         </android.support.v7.widget.CardView>
     </FrameLayout>
 
diff --git a/slices/view/build.gradle b/slices/view/build.gradle
index 72f0064..9ce7167 100644
--- a/slices/view/build.gradle
+++ b/slices/view/build.gradle
@@ -21,6 +21,7 @@
 dependencies {
     implementation project(":slices-core")
     implementation libs.support.recyclerview, libs.support_exclude_config
+    implementation project(':lifecycle:extensions')
 }
 
 android {
diff --git a/slices/view/src/main/java/androidx/app/slice/widget/SliceLiveData.java b/slices/view/src/main/java/androidx/app/slice/widget/SliceLiveData.java
new file mode 100644
index 0000000..eaaef50
--- /dev/null
+++ b/slices/view/src/main/java/androidx/app/slice/widget/SliceLiveData.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2017 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 androidx.app.slice.widget;
+
+import android.app.slice.Slice;
+import android.arch.lifecycle.LiveData;
+import android.content.Context;
+import android.database.ContentObserver;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Handler;
+
+/**
+ * Class with factory methods for creating LiveData that observes slices.
+ *
+ * @see #fromUri(Context, Uri)
+ * @see LiveData
+ */
+public final class SliceLiveData {
+
+    /**
+     * Produces an {@link LiveData} that tracks a Slice for a given Uri. To use
+     * this method your app must have the permission to the slice Uri or hold
+     * {@link android.Manifest.permission#BIND_SLICE}).
+     */
+    public static LiveData<Slice> fromUri(Context context, Uri uri) {
+        return new SliceLiveDataImpl(context.getApplicationContext(), uri);
+    }
+
+    private static class SliceLiveDataImpl extends LiveData<Slice> {
+        private final Uri mUri;
+        private final Context mContext;
+
+        private SliceLiveDataImpl(Context context, Uri uri) {
+            super();
+            mContext = context;
+            mUri = uri;
+            // TODO: Check if uri points at a Slice?
+        }
+
+        @Override
+        protected void onActive() {
+            AsyncTask.execute(this::updateSlice);
+            mContext.getContentResolver().registerContentObserver(mUri, false, mObserver);
+        }
+
+        @Override
+        protected void onInactive() {
+            mContext.getContentResolver().unregisterContentObserver(mObserver);
+        }
+
+        private void updateSlice() {
+            postValue(Slice.bindSlice(mContext.getContentResolver(), mUri));
+        }
+
+        private final ContentObserver mObserver = new ContentObserver(new Handler()) {
+            @Override
+            public void onChange(boolean selfChange) {
+                AsyncTask.execute(SliceLiveDataImpl.this::updateSlice);
+            }
+        };
+    }
+}
diff --git a/slices/view/src/main/java/androidx/app/slice/widget/SliceView.java b/slices/view/src/main/java/androidx/app/slice/widget/SliceView.java
index c35dc2c..6e12dba 100644
--- a/slices/view/src/main/java/androidx/app/slice/widget/SliceView.java
+++ b/slices/view/src/main/java/androidx/app/slice/widget/SliceView.java
@@ -18,19 +18,14 @@
 
 import android.app.slice.Slice;
 import android.app.slice.SliceItem;
+import android.arch.lifecycle.Observer;
 import android.content.ContentResolver;
 import android.content.Context;
-import android.content.Intent;
-import android.database.ContentObserver;
 import android.graphics.drawable.ColorDrawable;
 import android.net.Uri;
-import android.os.Handler;
-import android.os.Looper;
 import android.support.annotation.IntDef;
-import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
 import android.support.annotation.RestrictTo;
-import android.support.v4.util.Preconditions;
 import android.util.AttributeSet;
 import android.util.Log;
 import android.view.View;
@@ -62,20 +57,17 @@
  * {@link Slice#HINT_TITLE} would be placed in the title position of a template. A slice annotated
  * with {@link Slice#HINT_LIST} would present the child items of that slice in a list.
  * <p>
- * SliceView can be provided a slice via a uri {@link #setSlice(Uri)} in which case a content
- * observer will be set for that uri and the view will update if there are any changes to the slice.
- * To use this the app must have a special permission to bind to the slice (see
- * {@link android.Manifest.permission#BIND_SLICE}).
- * <p>
  * Example usage:
  *
  * <pre class="prettyprint">
  * SliceView v = new SliceView(getContext());
  * v.setMode(desiredMode);
- * v.setSlice(sliceUri);
+ * LiveData<Slice> liveData = SliceLiveData.fromUri(sliceUri);
+ * liveData.observe(lifecycleOwner, v);
  * </pre>
+ * @see SliceLiveData
  */
-public class SliceView extends ViewGroup {
+public class SliceView extends ViewGroup implements Observer<Slice> {
 
     private static final String TAG = "SliceView";
 
@@ -136,7 +128,6 @@
     private Slice mCurrentSlice;
     private boolean mShowActions = true;
     private boolean mIsScrollable;
-    private SliceObserver mObserver;
     private final int mShortcutSize;
 
     public SliceView(Context context) {
@@ -153,7 +144,6 @@
 
     public SliceView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
         super(context, attrs, defStyleAttr, defStyleRes);
-        mObserver = new SliceObserver(new Handler(Looper.getMainLooper()));
         mActions = new ActionRow(getContext(), true);
         mActions.setBackground(new ColorDrawable(0xffeeeeee));
         mCurrentView = new LargeTemplateView(getContext());
@@ -192,81 +182,23 @@
         }
     }
 
-    /**
-     * @hide
-     */
-    @RestrictTo(RestrictTo.Scope.LIBRARY)
-    public void showSlice(Intent intent) {
-        // TODO
-    }
-
-    /**
-     * Populates this view with the {@link Slice} associated with the provided {@link Uri}. To use
-     * this method your app must have the permission
-     * {@link android.Manifest.permission#BIND_SLICE}).
-     * <p>
-     * Setting a slice differs from {@link #showSlice(Slice)} because it will ensure the view is
-     * updated when the slice identified by the provided URI changes. The lifecycle of this observer
-     * is handled by SliceView in {@link #onAttachedToWindow()} and {@link #onDetachedFromWindow()}.
-     * To unregister this observer outside of that you can call {@link #clearSlice}.
-     *
-     * @return true if the a slice was found for the provided uri.
-     * @see #clearSlice
-     */
-    public boolean setSlice(@NonNull Uri sliceUri) {
-        Preconditions.checkNotNull(sliceUri,
-                "Uri cannot be null, to remove the slice use clearSlice()");
-        if (sliceUri == null) {
-            clearSlice();
-            return false;
-        }
-        validate(sliceUri);
-        Slice s = Slice.bindSlice(getContext().getContentResolver(), sliceUri);
-        if (s != null) {
-            if (mObserver != null) {
-                getContext().getContentResolver().unregisterContentObserver(mObserver);
-            }
-            mObserver = new SliceObserver(new Handler(Looper.getMainLooper()));
-            if (isAttachedToWindow()) {
-                registerSlice(sliceUri);
-            }
-            mCurrentSlice = s;
-            reinflate();
-        }
-        return s != null;
+    @Override
+    public void onChanged(@Nullable Slice slice) {
+        setSlice(slice);
     }
 
     /**
      * Populates this view to the provided {@link Slice}.
-     * <p>
-     * This does not register a content observer on the URI that the slice is backed by so it will
-     * not update if the content changes. To have the view update when the content changes use
-     * {@link #setSlice(Uri)} instead. Unlike {@link #setSlice(Uri)}, this method does not require
-     * any special permissions.
+     *
+     * This will not update automatically if the slice content changes, for live
+     * content see {@link SliceLiveData}.
      */
-    public void showSlice(@NonNull Slice slice) {
-        Preconditions.checkNotNull(slice,
-                "Slice cannot be null, to remove the slice use clearSlice()");
-        clearSlice();
+    public void setSlice(@Nullable Slice slice) {
         mCurrentSlice = slice;
         reinflate();
     }
 
     /**
-     * Unregisters the change observer that is set when using {@link #setSlice}. Normally this is
-     * done automatically during {@link #onDetachedFromWindow()}.
-     * <p>
-     * It is safe to call this method multiple times.
-     */
-    public void clearSlice() {
-        mCurrentSlice = null;
-        if (mObserver != null) {
-            getContext().getContentResolver().unregisterContentObserver(mObserver);
-            mObserver = null;
-        }
-    }
-
-    /**
      * Set the mode this view should present in.
      */
     public void setMode(@SliceMode int mode) {
@@ -324,29 +256,6 @@
         return new LargeTemplateView(getContext());
     }
 
-    @Override
-    protected void onAttachedToWindow() {
-        super.onAttachedToWindow();
-        registerSlice(mCurrentSlice != null ? mCurrentSlice.getUri() : null);
-    }
-
-    @Override
-    protected void onDetachedFromWindow() {
-        super.onDetachedFromWindow();
-        if (mObserver != null) {
-            getContext().getContentResolver().unregisterContentObserver(mObserver);
-            mObserver = null;
-        }
-    }
-
-    private void registerSlice(Uri sliceUri) {
-        if (sliceUri == null || mObserver == null) {
-            return;
-        }
-        getContext().getContentResolver().registerContentObserver(sliceUri,
-                false /* notifyForDescendants */, mObserver);
-    }
-
     private void reinflate() {
         if (mCurrentSlice == null) {
             return;
@@ -400,22 +309,4 @@
             throw new RuntimeException("Invalid uri " + sliceUri);
         }
     }
-
-    private class SliceObserver extends ContentObserver {
-        SliceObserver(Handler handler) {
-            super(handler);
-        }
-
-        @Override
-        public void onChange(boolean selfChange) {
-            this.onChange(selfChange, null);
-        }
-
-        @Override
-        public void onChange(boolean selfChange, Uri uri) {
-            Slice s = Slice.bindSlice(getContext().getContentResolver(), uri);
-            mCurrentSlice = s;
-            reinflate();
-        }
-    }
 }