1 /*
2  * Copyright (C) 2015 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 package com.example.android.leanback;
18 
19 import android.app.Activity;
20 import android.graphics.Bitmap;
21 import android.graphics.drawable.BitmapDrawable;
22 import android.graphics.drawable.Drawable;
23 import android.os.Handler;
24 import android.util.Log;
25 import android.view.View;
26 
27 import androidx.core.content.ContextCompat;
28 import androidx.leanback.app.BackgroundManager;
29 
30 /**
31  * App uses BackgroundHelper for each Activity, it wraps BackgroundManager and provides:
32  * 1. AsyncTask to load bitmap in background thread.
33  * 2. Using a BitmapCache to cache loaded bitmaps.
34  */
35 public class BackgroundHelper {
36 
37     private static final String TAG = "BackgroundHelper";
38     private static final boolean DEBUG = false;
39     private static final boolean ENABLED = true;
40 
41     // Background delay serves to avoid kicking off expensive bitmap loading
42     // in case multiple backgrounds are set in quick succession.
43     private static final int SET_BACKGROUND_DELAY_MS = 100;
44 
45     /**
46      * An very simple example of BitmapCache.
47      */
48     public static class BitmapCache {
49         Bitmap mLastBitmap;
50         Object mLastToken;
51 
52         // Singleton BitmapCache shared by multiple activities/backgroundHelper.
53         static BitmapCache sInstance = new BitmapCache();
54 
BitmapCache()55         private BitmapCache() {
56         }
57 
58         /**
59          * Get cached bitmap by token, returns null if missing cache.
60          */
61         @SuppressWarnings("ObjectToString")
getCache(Object token)62         public Bitmap getCache(Object token) {
63             if (token == null ? mLastToken == null : token.equals(mLastToken)) {
64                 if (DEBUG) Log.v(TAG, "hitCache token:" + token + " " + mLastBitmap);
65                 return mLastBitmap;
66             }
67             return null;
68         }
69 
70         /**
71          * Add cached bitmap.
72          */
73         @SuppressWarnings("ObjectToString")
putCache(Object token, Bitmap bitmap)74         public void putCache(Object token, Bitmap bitmap) {
75             if (DEBUG) Log.v(TAG, "putCache token:" + token + " " + bitmap);
76             mLastToken = token;
77             mLastBitmap = bitmap;
78         }
79 
80         /**
81          * Add singleton of BitmapCache shared across activities.
82          */
getInstance()83         public static BitmapCache getInstance() {
84             return sInstance;
85         }
86     }
87 
88     /**
89      * Callback class to perform task after bitmap is loaded.
90      */
91     public abstract static class BitmapLoadCallback {
92         /**
93          * Called when Bitmap is loaded.
94          */
onBitmapLoaded(Bitmap bitmap)95         public abstract void onBitmapLoaded(Bitmap bitmap);
96     }
97 
98     static class Request {
99         Object mImageToken;
100         Bitmap mResult;
101 
Request(Object imageToken)102         Request(Object imageToken) {
103             mImageToken = imageToken;
104         }
105     }
106 
BackgroundHelper(Activity activity)107     public BackgroundHelper(Activity activity) {
108         if (DEBUG && !ENABLED) Log.v(TAG, "BackgroundHelper: disabled");
109         mActivity = activity;
110     }
111 
112     class LoadBackgroundRunnable implements Runnable {
113         Request mRequest;
114 
LoadBackgroundRunnable(Object imageToken)115         LoadBackgroundRunnable(Object imageToken) {
116             mRequest = new Request(imageToken);
117         }
118 
119         @Override
run()120         public void run() {
121             if (DEBUG) Log.v(TAG, "Executing task");
122             new LoadBitmapIntoBackgroundManagerTask().execute(mRequest);
123             mRunnable = null;
124         }
125     }
126 
127     @SuppressWarnings("deprecation") /* AsyncTask */
128     class LoadBitmapTaskBase extends android.os.AsyncTask<Request, Object, Request> {
129         @Override
doInBackground(Request... params)130         protected Request doInBackground(Request... params) {
131             boolean cancelled = isCancelled();
132             if (DEBUG) Log.v(TAG, "doInBackground cancelled " + cancelled);
133             Request request = params[0];
134             if (!cancelled) {
135                 request.mResult = loadBitmap(request.mImageToken);
136             }
137             return request;
138         }
139 
140         @Override
onPostExecute(Request request)141         protected void onPostExecute(Request request) {
142             if (DEBUG) Log.v(TAG, "onPostExecute");
143             BitmapCache.getInstance().putCache(request.mImageToken, request.mResult);
144         }
145 
146         @Override
onCancelled(Request request)147         protected void onCancelled(Request request) {
148             if (DEBUG) Log.v(TAG, "onCancelled");
149         }
150 
loadBitmap(Object imageToken)151         private Bitmap loadBitmap(Object imageToken) {
152             if (imageToken instanceof Integer) {
153                 final int resourceId = (Integer) imageToken;
154                 if (DEBUG) Log.v(TAG, "load resourceId " + resourceId);
155                 Drawable drawable = ContextCompat.getDrawable(mActivity, resourceId);
156                 if (drawable instanceof BitmapDrawable) {
157                     return ((BitmapDrawable) drawable).getBitmap();
158                 }
159             }
160             return null;
161         }
162     }
163 
164     class LoadBitmapIntoBackgroundManagerTask extends LoadBitmapTaskBase {
165         @Override
onPostExecute(Request request)166         protected void onPostExecute(Request request) {
167             super.onPostExecute(request);
168             mBackgroundManager.setBitmap(request.mResult);
169         }
170     }
171 
172     class LoadBitmapCallbackTask extends LoadBitmapTaskBase {
173         BitmapLoadCallback mCallback;
174 
LoadBitmapCallbackTask(BitmapLoadCallback callback)175         LoadBitmapCallbackTask(BitmapLoadCallback callback) {
176             mCallback = callback;
177         }
178 
179         @Override
onPostExecute(Request request)180         protected void onPostExecute(Request request) {
181             super.onPostExecute(request);
182             if (mCallback != null) {
183                 mCallback.onBitmapLoaded(request.mResult);
184             }
185         }
186     }
187 
188     final Activity mActivity;
189     BackgroundManager mBackgroundManager;
190     LoadBackgroundRunnable mRunnable;
191 
192     // Allocate a dedicated handler because there may be no view available
193     // when setBackground is invoked.
194     static Handler sHandler = new Handler();
195 
createBackgroundManagerIfNeeded()196     void createBackgroundManagerIfNeeded() {
197         if (mBackgroundManager == null) {
198             mBackgroundManager = BackgroundManager.getInstance(mActivity);
199         }
200     }
201 
202     /**
203      * Attach BackgroundManager to activity window.
204      */
attachToWindow()205     public void attachToWindow() {
206         if (!ENABLED) {
207             return;
208         }
209         if (DEBUG) Log.v(TAG, "attachToWindow " + mActivity);
210         createBackgroundManagerIfNeeded();
211         mBackgroundManager.attach(mActivity.getWindow());
212     }
213 
214     /**
215      * Attach BackgroundManager to a view inside activity.
216      */
attachToView(View backgroundView)217     public void attachToView(View backgroundView) {
218         if (!ENABLED) {
219             return;
220         }
221         if (DEBUG) Log.v(TAG, "attachToView " + mActivity + " " + backgroundView);
222         createBackgroundManagerIfNeeded();
223         mBackgroundManager.attachToView(backgroundView);
224     }
225 
226     /**
227      * Sets a background bitmap. It will look up the cache first if missing, an AsyncTask will
228      * will be launched to load the bitmap.
229      */
setBackground(Object imageToken)230     public void setBackground(Object imageToken) {
231         if (!ENABLED) {
232             return;
233         }
234         if (DEBUG) Log.v(TAG, "set imageToken " + imageToken + " to " + mActivity);
235         createBackgroundManagerIfNeeded();
236         if (imageToken == null) {
237             mBackgroundManager.setDrawable(null);
238             return;
239         }
240         Bitmap cachedBitmap = BitmapCache.getInstance().getCache(imageToken);
241         if (cachedBitmap != null) {
242             mBackgroundManager.setBitmap(cachedBitmap);
243             return;
244         }
245         if (mRunnable != null) {
246             sHandler.removeCallbacks(mRunnable);
247         }
248         mRunnable = new LoadBackgroundRunnable(imageToken);
249         sHandler.postDelayed(mRunnable, SET_BACKGROUND_DELAY_MS);
250     }
251 
252     /**
253      * Clear Drawable.
254      */
clearDrawable()255     public void clearDrawable() {
256         if (!ENABLED) {
257             return;
258         }
259         if (DEBUG) Log.v(TAG, "clearDrawable to " + mActivity);
260         createBackgroundManagerIfNeeded();
261         mBackgroundManager.clearDrawable();
262     }
263 
264     /**
265      * Directly sets a Drawable as background.
266      */
setDrawable(Drawable drawable)267     public void setDrawable(Drawable drawable) {
268         if (!ENABLED) {
269             return;
270         }
271         if (DEBUG) Log.v(TAG, "setDrawable " + drawable + " to " + mActivity);
272         createBackgroundManagerIfNeeded();
273         mBackgroundManager.setDrawable(drawable);
274     }
275 
276     /**
277      * Load bitmap in background and pass result to BitmapLoadCallback.
278      */
loadBitmap(Object imageToken, BitmapLoadCallback callback)279     public void loadBitmap(Object imageToken, BitmapLoadCallback callback) {
280         Bitmap cachedBitmap = BitmapCache.getInstance().getCache(imageToken);
281         if (cachedBitmap != null) {
282             if (callback != null) {
283                 callback.onBitmapLoaded(cachedBitmap);
284                 return;
285             }
286         }
287         new LoadBitmapCallbackTask(callback).execute(new Request(imageToken));
288     }
289 }
290