• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright 2017 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 package com.android.tv.twopanelsettings.slices.compat.widget;
17 
18 import android.app.PendingIntent;
19 import android.content.Context;
20 import android.content.Intent;
21 import android.net.Uri;
22 import android.os.Looper;
23 import android.os.StrictMode;
24 import android.util.Log;
25 import androidx.annotation.IntDef;
26 import androidx.annotation.NonNull;
27 import androidx.annotation.Nullable;
28 import androidx.collection.ArraySet;
29 import androidx.lifecycle.LiveData;
30 import com.android.tv.twopanelsettings.slices.compat.Slice;
31 import com.android.tv.twopanelsettings.slices.compat.SliceItem;
32 import com.android.tv.twopanelsettings.slices.compat.SliceMetadata;
33 import com.android.tv.twopanelsettings.slices.compat.SliceSpec;
34 import com.android.tv.twopanelsettings.slices.compat.SliceSpecs;
35 import com.android.tv.twopanelsettings.slices.compat.SliceStructure;
36 import com.android.tv.twopanelsettings.slices.compat.SliceUtils;
37 import com.android.tv.twopanelsettings.slices.compat.SliceViewManager;
38 import com.android.tv.twopanelsettings.slices.compat.core.SliceQuery;
39 import java.io.InputStream;
40 import java.lang.annotation.Retention;
41 import java.lang.annotation.RetentionPolicy;
42 import java.util.ArrayList;
43 import java.util.Arrays;
44 import java.util.List;
45 import java.util.Set;
46 
47 /**
48  * Class with factory methods for creating LiveData that observes slices.
49  *
50  * @see #fromUri(Context, Uri)
51  * @see LiveData
52  *     <p>Slice framework has been deprecated, it will not receive any updates moving forward. If
53  *     you are looking for a framework that handles communication across apps, consider using {@link
54  *     android.app.appsearch.AppSearchManager}.
55  */
56 // @Deprecated // Supported for TV
57 public final class SliceLiveData {
58   private static final String TAG = "SliceLiveData";
59 
60   /** */
61   // @RestrictTo(LIBRARY)
62   public static final SliceSpec OLD_BASIC = new SliceSpec("androidx.app.slice.BASIC", 1);
63 
64   /** */
65   // @RestrictTo(LIBRARY)
66   public static final SliceSpec OLD_LIST = new SliceSpec("androidx.app.slice.LIST", 1);
67 
68   /** */
69   // @RestrictTo(LIBRARY)
70   public static final Set<SliceSpec> SUPPORTED_SPECS =
71       new ArraySet<>(
72           Arrays.asList(
73               SliceSpecs.BASIC, SliceSpecs.LIST, SliceSpecs.LIST_V2, OLD_BASIC, OLD_LIST));
74 
75   /**
76    * Produces a {@link LiveData} that tracks a Slice for a given Uri. To use this method your app
77    * must have the permission to the slice Uri.
78    */
fromUri(@onNull Context context, @NonNull Uri uri)79   public static @NonNull LiveData<Slice> fromUri(@NonNull Context context, @NonNull Uri uri) {
80     return new SliceLiveDataImpl(context.getApplicationContext(), uri, null);
81   }
82 
83   /**
84    * Produces a {@link LiveData} that tracks a Slice for a given Uri. To use this method your app
85    * must have the permission to the slice Uri.
86    */
fromUri( @onNull Context context, @NonNull Uri uri, @Nullable OnErrorListener listener)87   public static @NonNull LiveData<Slice> fromUri(
88       @NonNull Context context, @NonNull Uri uri, @Nullable OnErrorListener listener) {
89     return new SliceLiveDataImpl(context.getApplicationContext(), uri, listener);
90   }
91 
92   /**
93    * Produces a {@link LiveData} that tracks a Slice for a given Intent. To use this method your app
94    * must have the permission to the slice Uri.
95    */
fromIntent( @onNull Context context, @NonNull Intent intent)96   public static @NonNull LiveData<Slice> fromIntent(
97       @NonNull Context context, @NonNull Intent intent) {
98     return new SliceLiveDataImpl(context.getApplicationContext(), intent, null);
99   }
100 
101   /**
102    * Produces a {@link LiveData} that tracks a Slice for a given Intent. To use this method your app
103    * must have the permission to the slice Uri.
104    */
fromIntent( @onNull Context context, @NonNull Intent intent, @Nullable OnErrorListener listener)105   public static @NonNull LiveData<Slice> fromIntent(
106       @NonNull Context context, @NonNull Intent intent, @Nullable OnErrorListener listener) {
107     return new SliceLiveDataImpl(context.getApplicationContext(), intent, listener);
108   }
109 
110   /**
111    * Produces a {@link LiveData} that tracks a Slice for a given InputStream. To use this method
112    * your app must have the permission to the slice Uri.
113    *
114    * <p>This will not ask the hosting app for a slice immediately, instead it will display the slice
115    * passed in through the input. When the user interacts with the slice, then the app will be
116    * started to obtain the current slice and trigger the user action.
117    */
fromStream( @onNull Context context, @NonNull InputStream input, OnErrorListener listener)118   public static @NonNull LiveData<Slice> fromStream(
119       @NonNull Context context, @NonNull InputStream input, OnErrorListener listener) {
120     return fromStream(context, SliceViewManager.getInstance(context), input, listener);
121   }
122 
123   /**
124    * Same as {@link #fromStream(Context, InputStream, OnErrorListener)} except returns as type
125    * {@link CachedSliceLiveData}.
126    */
fromCachedSlice( @onNull Context context, @NonNull InputStream input, OnErrorListener listener)127   public static @NonNull CachedSliceLiveData fromCachedSlice(
128       @NonNull Context context, @NonNull InputStream input, OnErrorListener listener) {
129     return fromStream(context, SliceViewManager.getInstance(context), input, listener);
130   }
131 
132   /** Version for testing */
133   // @RestrictTo(LIBRARY_GROUP_PREFIX)
134   @NonNull
fromStream( @onNull Context context, SliceViewManager manager, @NonNull InputStream input, OnErrorListener listener)135   public static CachedSliceLiveData fromStream(
136       @NonNull Context context,
137       SliceViewManager manager,
138       @NonNull InputStream input,
139       OnErrorListener listener) {
140     return new CachedSliceLiveData(context, manager, input, listener);
141   }
142 
143   /**
144    * Implementation of {@link LiveData}<Slice> that provides controls over how cached vs live slices
145    * work.
146    */
147   public static class CachedSliceLiveData extends LiveData<Slice> {
148     final SliceViewManager mSliceViewManager;
149     private final OnErrorListener mListener;
150     final Context mContext;
151     private InputStream mInput;
152     Uri mUri;
153     private boolean mActive;
154     List<Uri> mPendingUri = new ArrayList<>();
155     private boolean mLive;
156     SliceStructure mStructure;
157     List<Context> mPendingContext = new ArrayList<>();
158     List<Intent> mPendingIntent = new ArrayList<>();
159     private boolean mSliceCallbackRegistered;
160     private boolean mInitialSliceLoaded;
161 
CachedSliceLiveData( final Context context, final SliceViewManager manager, final InputStream input, final OnErrorListener listener)162     CachedSliceLiveData(
163         final Context context,
164         final SliceViewManager manager,
165         final InputStream input,
166         final OnErrorListener listener) {
167       super();
168       mContext = context;
169       mSliceViewManager = manager;
170       mUri = null;
171       mListener = listener;
172       mInput = input;
173     }
174 
175     /**
176      * Generally the InputStream are parsed asynchronously once the LiveData goes into the active
177      * state. When this is called, regardless of state, the slice will be read from the input stream
178      * and then the input stream's reference will be released when finished.
179      *
180      * <p>Calling parseStream() multiple times or after the stream has already been parsed
181      * asynchronously will have no effect.
182      */
parseStream()183     public void parseStream() {
184       loadInitialSlice();
185     }
186 
187     /**
188      * Moves this CachedSliceLiveData into a "live" state, causing the providing app to start up and
189      * provide an up to date version of the slice. After calling this method the slice will always
190      * be pinned as long as this LiveData is in the active state.
191      *
192      * <p>If the slice has already received a click or goLive() has already been called, then this
193      * method will have no effect.
194      *
195      * <p>Once goLive() has been called, there is no way to reverse it, this LiveData will then
196      * behave the same way as one created using {@link #fromUri(Context, Uri)}.
197      */
goLive()198     public void goLive() {
199       // Go live with no click.
200       goLive(null, null, null);
201     }
202 
203     /** */
204     // @RestrictTo(LIBRARY)
205     @SuppressWarnings("deprecation") /* AsyncTask */
loadInitialSlice()206     protected synchronized void loadInitialSlice() {
207       if (mInitialSliceLoaded) {
208         return;
209       }
210       try {
211         Slice s =
212             SliceUtils.parseSlice(
213                 mContext,
214                 mInput,
215                 "UTF-8",
216                 new SliceUtils.SliceActionListener() {
217                   @Override
218                   public void onSliceAction(Uri actionUri, Context context, Intent intent) {
219                     goLive(actionUri, context, intent);
220                   }
221                 });
222         mStructure = new SliceStructure(s);
223         mUri = s.getUri();
224         if (Looper.getMainLooper().getThread() == Thread.currentThread()) {
225           setValue(s);
226         } else {
227           postValue(s);
228         }
229       } catch (Exception e) {
230         mListener.onSliceError(OnErrorListener.ERROR_INVALID_INPUT, e);
231       }
232       mInput = null;
233       mInitialSliceLoaded = true;
234     }
235 
goLive(Uri actionUri, Context context, Intent intent)236     void goLive(Uri actionUri, Context context, Intent intent) {
237       mLive = true;
238       if (actionUri != null) {
239         mPendingUri.add(actionUri);
240         mPendingContext.add(context);
241         mPendingIntent.add(intent);
242       }
243       if (mActive && !mSliceCallbackRegistered) {
244         android.os.AsyncTask.execute(mUpdateSlice);
245         mSliceViewManager.registerSliceCallback(mUri, mSliceCallback);
246         mSliceCallbackRegistered = true;
247       }
248     }
249 
250     @Override
onActive()251     protected void onActive() {
252       mActive = true;
253       if (!mInitialSliceLoaded) {
254         android.os.AsyncTask.execute(
255             new Runnable() {
256               @Override
257               public void run() {
258                 loadInitialSlice();
259               }
260             });
261       }
262       if (mLive && !mSliceCallbackRegistered) {
263         android.os.AsyncTask.execute(mUpdateSlice);
264         mSliceViewManager.registerSliceCallback(mUri, mSliceCallback);
265         mSliceCallbackRegistered = true;
266       }
267     }
268 
269     @Override
onInactive()270     protected void onInactive() {
271       mActive = false;
272       if (mLive && mSliceCallbackRegistered) {
273         mSliceViewManager.unregisterSliceCallback(mUri, mSliceCallback);
274         mSliceCallbackRegistered = false;
275       }
276     }
277 
onSliceError(int error, Throwable t)278     void onSliceError(int error, Throwable t) {
279       mListener.onSliceError(error, t);
280       if (mLive) {
281         if (mSliceCallbackRegistered) {
282           mSliceViewManager.unregisterSliceCallback(mUri, mSliceCallback);
283           mSliceCallbackRegistered = false;
284         }
285         mLive = false;
286       }
287     }
288 
289     /** */
290     // @RestrictTo(LIBRARY)
updateSlice()291     protected void updateSlice() {
292       try {
293         Slice s = mSliceViewManager.bindSlice(mUri);
294         mSliceCallback.onSliceUpdated(s);
295       } catch (Exception e) {
296         mListener.onSliceError(OnErrorListener.ERROR_UNKNOWN, e);
297       }
298     }
299 
300     private final Runnable mUpdateSlice =
301         new Runnable() {
302           @Override
303           public void run() {
304             updateSlice();
305           }
306         };
307 
308     final SliceViewManager.SliceCallback mSliceCallback =
309         new SliceViewManager.SliceCallback() {
310           @Override
311           public void onSliceUpdated(@Nullable Slice s) {
312             if (!mPendingUri.isEmpty()) {
313               if (s == null) {
314                 onSliceError(OnErrorListener.ERROR_SLICE_NO_LONGER_PRESENT, null);
315                 return;
316               }
317               SliceStructure structure = new SliceStructure(s);
318               if (!mStructure.equals(structure)) {
319                 onSliceError(OnErrorListener.ERROR_STRUCTURE_CHANGED, null);
320                 return;
321               }
322               SliceMetadata metaData = SliceMetadata.from(mContext, s);
323               if (metaData.getLoadingState() == SliceMetadata.LOADED_ALL) {
324                 for (int i = 0; i < mPendingUri.size(); i++) {
325                   SliceItem item = SliceQuery.findItem(s, mPendingUri.get(i));
326                   if (item != null) {
327                     try {
328                       item.fireAction(mPendingContext.get(i), mPendingIntent.get(i));
329                     } catch (PendingIntent.CanceledException e) {
330                       onSliceError(OnErrorListener.ERROR_UNKNOWN, e);
331                       return;
332                     }
333                   } else {
334                     onSliceError(OnErrorListener.ERROR_UNKNOWN, new NullPointerException());
335                     return;
336                   }
337                 }
338                 mPendingUri.clear();
339                 mPendingContext.clear();
340                 mPendingIntent.clear();
341               }
342             }
343             postValue(s);
344           }
345         };
346   }
347 
348   private static class SliceLiveDataImpl extends LiveData<Slice> {
349     final Intent mIntent;
350     final SliceViewManager mSliceViewManager;
351     final OnErrorListener mListener;
352     Uri mUri;
353 
SliceLiveDataImpl(Context context, Uri uri, OnErrorListener listener)354     SliceLiveDataImpl(Context context, Uri uri, OnErrorListener listener) {
355       super();
356       mSliceViewManager = SliceViewManager.getInstance(context);
357       mUri = uri;
358       mIntent = null;
359       mListener = listener;
360     }
361 
SliceLiveDataImpl(Context context, Intent intent, OnErrorListener listener)362     SliceLiveDataImpl(Context context, Intent intent, OnErrorListener listener) {
363       super();
364       mSliceViewManager = SliceViewManager.getInstance(context);
365       mUri = null;
366       mIntent = intent;
367       mListener = listener;
368     }
369 
370     @Override
371     @SuppressWarnings("deprecation") /* AsyncTask */
onActive()372     protected void onActive() {
373       android.os.AsyncTask.execute(mUpdateSlice);
374       if (mUri != null) {
375         mSliceViewManager.registerSliceCallback(mUri, mSliceCallback);
376       }
377     }
378 
379     @Override
onInactive()380     protected void onInactive() {
381       if (mUri != null) {
382         mSliceViewManager.unregisterSliceCallback(mUri, mSliceCallback);
383       }
384     }
385 
386     private final Runnable mUpdateSlice =
387         new Runnable() {
388           @Override
389           public void run() {
390             StrictMode.ThreadPolicy oldPolicy = StrictMode.getThreadPolicy();
391             try {
392               // Prevent StrictMode from throwing when writing to disk.
393               StrictMode.setThreadPolicy(
394                   new StrictMode.ThreadPolicy
395                       .Builder(oldPolicy).permitDiskWrites().build());
396               Slice s =
397                   mUri != null
398                       ? mSliceViewManager.bindSlice(mUri)
399                       : mSliceViewManager.bindSlice(mIntent);
400               if (mUri == null && s != null) {
401                 mUri = s.getUri();
402                 mSliceViewManager.registerSliceCallback(mUri, mSliceCallback);
403               }
404               postValue(s);
405             } catch (IllegalArgumentException e) {
406               onSliceError(OnErrorListener.ERROR_INVALID_INPUT, e);
407               postValue(null);
408             } catch (Exception e) {
409               onSliceError(OnErrorListener.ERROR_UNKNOWN, e);
410               postValue(null);
411             } finally {
412               StrictMode.setThreadPolicy(oldPolicy);
413             }
414           }
415         };
416 
417     final SliceViewManager.SliceCallback mSliceCallback = value -> postValue(value);
418 
onSliceError(int error, Throwable t)419     void onSliceError(int error, Throwable t) {
420       if (mListener != null) {
421         mListener.onSliceError(error, t);
422         return;
423       }
424       Log.e(TAG, "Error binding slice", t);
425     }
426   }
427 
SliceLiveData()428   private SliceLiveData() {}
429 
430   /** Listener for errors when using {@link #fromStream(Context, InputStream, OnErrorListener)}. */
431   public interface OnErrorListener {
432     int ERROR_UNKNOWN = 0;
433     int ERROR_STRUCTURE_CHANGED = 1;
434     int ERROR_SLICE_NO_LONGER_PRESENT = 2;
435     int ERROR_INVALID_INPUT = 3;
436 
437     /** */
438     @IntDef({
439       ERROR_UNKNOWN,
440       ERROR_STRUCTURE_CHANGED,
441       ERROR_SLICE_NO_LONGER_PRESENT,
442       ERROR_INVALID_INPUT
443     })
444     @Retention(RetentionPolicy.SOURCE)
445     @interface ErrorType {}
446 
447     /** Called when an error occurs converting a serialized slice into a live slice. */
onSliceError(@rrorType int type, @Nullable Throwable source)448     void onSliceError(@ErrorType int type, @Nullable Throwable source);
449   }
450 }
451