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