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