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
17package androidx.slice.render;
18
19import static android.view.View.MeasureSpec.makeMeasureSpec;
20
21import android.app.Activity;
22import android.app.ProgressDialog;
23import android.graphics.Bitmap;
24import android.graphics.Canvas;
25import android.net.Uri;
26import android.os.Handler;
27import android.util.Log;
28import android.util.TypedValue;
29import android.view.LayoutInflater;
30import android.view.View;
31import android.view.ViewGroup;
32
33import androidx.recyclerview.widget.RecyclerView;
34import androidx.slice.Slice;
35import androidx.slice.SliceProvider;
36import androidx.slice.SliceUtils;
37import androidx.slice.view.test.R;
38import androidx.slice.widget.SliceLiveData;
39import androidx.slice.widget.SliceView;
40
41import java.io.ByteArrayInputStream;
42import java.io.ByteArrayOutputStream;
43import java.io.File;
44import java.io.FileOutputStream;
45import java.util.concurrent.CountDownLatch;
46import java.util.concurrent.ExecutorService;
47import java.util.concurrent.Executors;
48
49public 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
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
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
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
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
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
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
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
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