1 /*
2  * Copyright (C) 2019 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 
18 package com.android.tv.twopanelsettings.slices;
19 
20 import android.content.Context;
21 import android.content.Intent;
22 import android.net.Uri;
23 import android.os.AsyncTask;
24 import android.util.Log;
25 
26 import androidx.annotation.MainThread;
27 import androidx.annotation.NonNull;
28 import androidx.annotation.RequiresApi;
29 import androidx.lifecycle.LiveData;
30 import androidx.lifecycle.MutableLiveData;
31 import androidx.slice.Slice;
32 import androidx.slice.SliceViewManager;
33 
34 import java.util.concurrent.atomic.AtomicBoolean;
35 
36 /**
37  * In TvSettings, if a preference item with corresponding slice first get focused, and regain focus
38  * later, we always receive a cache of the slice data first and then we get the real data from
39  * SliceProvider. If the cache and the real data is not consistent, user would clearly see the
40  * process of the cache data transforming to the real one. e.g, toggle state switching from false
41  * to true.
42  *
43  * This is because when a Lifecycle starts again, LiveData natively notify us about the change,
44  * which will trigger onChanged() in SliceFragment.
45  *
46  * To avoid this issue, we should ignore the lifecycle event, use SingleLiveEvent so we only get
47  * notified when the data is actually changed.
48  */
49 @RequiresApi(19)
50 public final class PreferenceSliceLiveData {
51     private static final String TAG = "SliceLiveData";
52 
53     /**
54      * Produces a {@link LiveData} that tracks a Slice for a given Uri. To use
55      * this method your app must have the permission to the slice Uri.
56      */
fromUri(@onNull Context context, @NonNull Uri uri)57     static @NonNull SliceLiveDataImpl fromUri(@NonNull Context context, @NonNull Uri uri) {
58         return new SliceLiveDataImpl(context.getApplicationContext(), uri);
59     }
60 
61     /**
62      * LiveData for slice used by TvSettings.
63      */
64     static class SliceLiveDataImpl extends MutableLiveData<Slice> {
65         final Intent mIntent;
66         final SliceViewManager mSliceViewManager;
67         Uri mUri;
68         final AtomicBoolean mUpdatePending = new AtomicBoolean(false);
SliceLiveDataImpl(Context context, Uri uri)69         SliceLiveDataImpl(Context context, Uri uri) {
70             super();
71             mSliceViewManager = SliceViewManager.getInstance(context);
72             mUri = uri;
73             mIntent = null;
74             // TODO: Check if uri points at a Slice?
75         }
76 
77         @Override
onActive()78         protected void onActive() {
79             AsyncTask.execute(mUpdateSlice);
80             if (mUri != null) {
81                 mSliceViewManager.registerSliceCallback(mUri, mSliceCallback);
82             }
83         }
84 
85         @Override
onInactive()86         protected void onInactive() {
87             if (mUri != null) {
88                 mSliceViewManager.unregisterSliceCallback(mUri, mSliceCallback);
89             }
90         }
91 
92         @Override
93         @MainThread
setValue(Slice slice)94         public void setValue(Slice slice) {
95             mUpdatePending.set(true);
96             super.setValue(slice);
97         }
98 
99         private final Runnable mUpdateSlice = new Runnable() {
100             @Override
101             public void run() {
102                 try {
103                     Slice s = mUri != null ? mSliceViewManager.bindSlice(mUri)
104                             : mSliceViewManager.bindSlice(mIntent);
105                     if (mUri == null && s != null) {
106                         mUri = s.getUri();
107                         mSliceViewManager.registerSliceCallback(mUri, mSliceCallback);
108                     }
109                     postValue(s);
110                 } catch (Exception e) {
111                     Log.e(TAG, "Error binding slice", e);
112                     postValue(null);
113                 }
114             }
115         };
116 
117         final SliceViewManager.SliceCallback mSliceCallback =
118                 new SliceViewManager.SliceCallback() {
119                     @Override
120                     public void onSliceUpdated(@NonNull Slice s) {
121                         postValue(s);
122                     }
123                 };
124     }
125 
PreferenceSliceLiveData()126     private PreferenceSliceLiveData() {
127     }
128 }
129