1 /*
2  * Copyright 2018 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 androidx.slice.render;
18 
19 import static android.view.View.MeasureSpec.makeMeasureSpec;
20 
21 import android.app.Activity;
22 import android.app.ProgressDialog;
23 import android.graphics.Bitmap;
24 import android.graphics.Canvas;
25 import android.net.Uri;
26 import android.os.Handler;
27 import android.util.Log;
28 import android.util.TypedValue;
29 import android.view.LayoutInflater;
30 import android.view.View;
31 import android.view.ViewGroup;
32 
33 import androidx.recyclerview.widget.RecyclerView;
34 import androidx.slice.Slice;
35 import androidx.slice.SliceProvider;
36 import androidx.slice.SliceUtils;
37 import androidx.slice.view.test.R;
38 import androidx.slice.widget.SliceLiveData;
39 import androidx.slice.widget.SliceView;
40 
41 import java.io.ByteArrayInputStream;
42 import java.io.ByteArrayOutputStream;
43 import java.io.File;
44 import java.io.FileOutputStream;
45 import java.util.concurrent.CountDownLatch;
46 import java.util.concurrent.ExecutorService;
47 import java.util.concurrent.Executors;
48 
49 public class SliceRenderer {
50 
51     private static final String TAG = "SliceRenderer";
52     public static final String SCREENSHOT_DIR = "slice-screenshots";
53 
54     private static final int MAX_CONCURRENT = 5;
55 
56     private static File sScreenshotDirectory;
57 
58     private final Object mRenderLock = new Object();
59 
60     private final Activity mContext;
61     private final View mLayout;
62     private final SliceView mSV1;
63     private final SliceView mSV2;
64     private final SliceView mSV3;
65     private final ViewGroup mParent;
66     private final Handler mHandler;
67     private final SliceCreator mSliceCreator;
68     private CountDownLatch mDoneLatch;
69 
SliceRenderer(Activity context)70     public SliceRenderer(Activity context) {
71         mContext = context;
72         mParent = new ViewGroup(mContext) {
73             @Override
74             protected void onLayout(boolean changed, int l, int t, int r, int b) {
75                 int width = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1000,
76                         mContext.getResources().getDisplayMetrics());
77                 int height = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 330,
78                         mContext.getResources().getDisplayMetrics());
79                 mLayout.measure(makeMeasureSpec(width, View.MeasureSpec.EXACTLY),
80                         makeMeasureSpec(height, View.MeasureSpec.EXACTLY));
81                 mLayout.layout(0, 0, width, height);
82             }
83 
84             @Override
85             protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
86                 return false;
87             }
88         };
89         mLayout = LayoutInflater.from(context).inflate(R.layout.render_layout, null);
90         mSV1 = mLayout.findViewById(R.id.sv1);
91         mSV1.setMode(SliceView.MODE_SHORTCUT);
92         mSV2 = mLayout.findViewById(R.id.sv2);
93         mSV2.setMode(SliceView.MODE_SMALL);
94         mSV3 = mLayout.findViewById(R.id.sv3);
95         mSV3.setMode(SliceView.MODE_LARGE);
96         disableAnims(mLayout);
97         mHandler = new Handler();
98         ((ViewGroup) mContext.getWindow().getDecorView()).addView(mParent);
99         mParent.addView(mLayout);
100         SliceProvider.setSpecs(SliceLiveData.SUPPORTED_SPECS);
101         mSliceCreator = new SliceCreator(mContext);
102     }
103 
disableAnims(View view)104     private void disableAnims(View view) {
105         if (view instanceof RecyclerView) {
106             ((RecyclerView) view).setItemAnimator(null);
107         }
108         if (view instanceof ViewGroup) {
109             ViewGroup viewGroup = (ViewGroup) view;
110             for (int i = 0; i < viewGroup.getChildCount(); i++) {
111                 disableAnims(viewGroup.getChildAt(i));
112             }
113         }
114     }
115 
116 
getScreenshotDirectory()117     private File getScreenshotDirectory() {
118         if (sScreenshotDirectory == null) {
119             File storage = mContext.getFilesDir();
120             sScreenshotDirectory = new File(storage, SCREENSHOT_DIR);
121             if (!sScreenshotDirectory.exists()) {
122                 if (!sScreenshotDirectory.mkdirs()) {
123                     throw new RuntimeException(
124                             "Failed to create a screenshot directory.");
125                 }
126             }
127         }
128         return sScreenshotDirectory;
129     }
130 
131 
doRender()132     private void doRender() {
133         final File output = getScreenshotDirectory();
134         if (!output.exists()) {
135             output.mkdir();
136         }
137         mDoneLatch = new CountDownLatch(SliceCreator.URI_PATHS.length * 2 + 2);
138 
139         ExecutorService executor = Executors.newFixedThreadPool(5);
140         for (final String slice : SliceCreator.URI_PATHS) {
141             final Slice s = mSliceCreator.onBindSlice(SliceCreator.getUri(slice, mContext));
142 
143             executor.execute(new Runnable() {
144                 @Override
145                 public void run() {
146                     doRender(slice, s, new File(output, String.format("%s.png", slice)),
147                             true /* scrollable */);
148                 }
149             });
150             final Slice serialized = serAndUnSer(s);
151             executor.execute(new Runnable() {
152                 @Override
153                 public void run() {
154                     doRender(slice + "-ser", serialized, new File(output, String.format(
155                             "%s-serialized.png", slice)), true /* scrollable */);
156                 }
157             });
158             if (slice.equals("wifi") || slice.equals("wifi2")) {
159                 // Test scrolling
160                 executor.execute(new Runnable() {
161                     @Override
162                     public void run() {
163                         doRender(slice + "-ns", s, new File(output, String.format(
164                                 "%s-no-scroll.png", slice)), false /* scrollable */);
165                     }
166                 });
167             }
168         }
169         try {
170             mDoneLatch.await();
171         } catch (InterruptedException e) {
172         }
173         Log.d(TAG, "Wrote render to " + output.getAbsolutePath());
174         mContext.runOnUiThread(new Runnable() {
175             @Override
176             public void run() {
177                 ((ViewGroup) mParent.getParent()).removeView(mParent);
178             }
179         });
180     }
181 
serAndUnSer(Slice s)182     private Slice serAndUnSer(Slice s) {
183         try {
184             ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
185             SliceUtils.serializeSlice(s, mContext, outputStream, "UTF-8",
186                     new SliceUtils.SerializeOptions()
187                             .setImageMode(SliceUtils.SerializeOptions.MODE_CONVERT)
188                             .setActionMode(SliceUtils.SerializeOptions.MODE_CONVERT));
189 
190             byte[] bytes = outputStream.toByteArray();
191             Log.d(TAG, "Serialized: " + new String(bytes));
192             ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes);
193             return SliceUtils.parseSlice(mContext, inputStream, "UTF-8",
194                     new SliceUtils.SliceActionListener() {
195                         @Override
196                         public void onSliceAction(Uri actionUri) { }
197                     });
198         } catch (Exception e) {
199             throw new RuntimeException(e);
200         }
201     }
202 
doRender(final String slice, final Slice s, final File file, final boolean scrollable)203     private void doRender(final String slice, final Slice s, final File file,
204             final boolean scrollable) {
205         Log.d(TAG, "Rendering " + slice + " to " + file.getAbsolutePath());
206 
207         try {
208             final CountDownLatch l = new CountDownLatch(1);
209             final Bitmap[] b = new Bitmap[1];
210             synchronized (mRenderLock) {
211                 mContext.runOnUiThread(new Runnable() {
212                     @Override
213                     public void run() {
214                         mSV1.setSlice(s);
215                         mSV2.setSlice(s);
216                         mSV3.setSlice(s);
217                         mSV3.setScrollable(scrollable);
218                         mSV1.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
219                             @Override
220                             public void onLayoutChange(View v, int left, int top, int right,
221                                     int bottom,
222                                     int oldLeft, int oldTop, int oldRight, int oldBottom) {
223                                 mSV1.removeOnLayoutChangeListener(this);
224                                 mSV1.postDelayed(new Runnable() {
225                                     @Override
226                                     public void run() {
227                                         Log.d(TAG, "Drawing " + slice);
228                                         b[0] = Bitmap.createBitmap(mLayout.getMeasuredWidth(),
229                                                 mLayout.getMeasuredHeight(),
230                                                 Bitmap.Config.ARGB_8888);
231 
232                                         mLayout.draw(new Canvas(b[0]));
233                                         l.countDown();
234                                     }
235                                 }, 10);
236                             }
237                         });
238                     }
239                 });
240                 l.await();
241             }
242             doCompress(slice, b[0], new FileOutputStream(file));
243         } catch (Exception e) {
244             throw new RuntimeException(e);
245         }
246     }
247 
doCompress(final String slice, final Bitmap b, final FileOutputStream s)248     private void doCompress(final String slice, final Bitmap b, final FileOutputStream s) {
249         Log.d(TAG, "Compressing " + slice);
250         if (!b.compress(Bitmap.CompressFormat.PNG, 100, s)) {
251             throw new RuntimeException("Unable to compress");
252         }
253 
254         b.recycle();
255         mDoneLatch.countDown();
256         Log.d(TAG, "Done " + slice);
257     }
258 
renderAll(final Runnable runnable)259     public void renderAll(final Runnable runnable) {
260         final ProgressDialog dialog = ProgressDialog.show(mContext, null, "Rendering...");
261         new Thread(new Runnable() {
262             @Override
263             public void run() {
264                 doRender();
265                 mContext.runOnUiThread(new Runnable() {
266                     @Override
267                     public void run() {
268                         dialog.dismiss();
269                         runnable.run();
270                     }
271                 });
272             }
273         }).start();
274     }
275 }
276