1 /* 2 * Copyright (C) 2024 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.systemui.screenshot.scroll; 17 18 import android.annotation.AnyThread; 19 import android.graphics.Bitmap; 20 import android.graphics.HardwareRenderer; 21 import android.graphics.RecordingCanvas; 22 import android.graphics.Rect; 23 import android.graphics.Region; 24 import android.graphics.RenderNode; 25 import android.graphics.drawable.Drawable; 26 import android.os.Handler; 27 28 import androidx.annotation.UiThread; 29 30 import com.android.internal.util.CallbackRegistry; 31 import com.android.internal.util.CallbackRegistry.NotifierCallback; 32 33 import java.util.ArrayList; 34 import java.util.Iterator; 35 import java.util.List; 36 37 import javax.inject.Inject; 38 39 /** 40 * Owns a series of partial screen captures (tiles). 41 * <p> 42 * To display on-screen, use {@link #getDrawable()}. 43 */ 44 @UiThread 45 class ImageTileSet { 46 47 private static final String TAG = "ImageTileSet"; 48 49 private CallbackRegistry<OnContentChangedListener, ImageTileSet, Rect> mContentListeners; 50 51 @Inject ImageTileSet(@iThread Handler handler)52 ImageTileSet(@UiThread Handler handler) { 53 mHandler = handler; 54 } 55 56 interface OnContentChangedListener { 57 /** 58 * Mark as dirty and rebuild display list. 59 */ onContentChanged()60 void onContentChanged(); 61 } 62 63 private final List<ImageTile> mTiles = new ArrayList<>(); 64 private final Region mRegion = new Region(); 65 private final Handler mHandler; 66 addOnContentChangedListener(OnContentChangedListener listener)67 void addOnContentChangedListener(OnContentChangedListener listener) { 68 if (mContentListeners == null) { 69 mContentListeners = new CallbackRegistry<>( 70 new NotifierCallback<OnContentChangedListener, ImageTileSet, Rect>() { 71 @Override 72 public void onNotifyCallback(OnContentChangedListener callback, 73 ImageTileSet sender, 74 int arg, Rect newBounds) { 75 callback.onContentChanged(); 76 } 77 }); 78 } 79 mContentListeners.add(listener); 80 } 81 82 @AnyThread addTile(ImageTile tile)83 void addTile(ImageTile tile) { 84 if (!mHandler.getLooper().isCurrentThread()) { 85 mHandler.post(() -> addTile(tile)); 86 return; 87 } 88 mTiles.add(tile); 89 mRegion.op(tile.getLocation(), mRegion, Region.Op.UNION); 90 notifyContentChanged(); 91 } 92 notifyContentChanged()93 private void notifyContentChanged() { 94 if (mContentListeners != null) { 95 mContentListeners.notifyCallbacks(this, 0, null); 96 } 97 } 98 99 /** 100 * Returns a drawable to paint the combined contents of the tiles. Drawable dimensions are 101 * zero-based and map directly to {@link #getLeft()}, {@link #getTop()}, {@link #getRight()}, 102 * and {@link #getBottom()} which are dimensions relative to the capture start position 103 * (positive or negative). 104 * 105 * @return a drawable to display the image content 106 */ getDrawable()107 Drawable getDrawable() { 108 return new TiledImageDrawable(this); 109 } 110 isEmpty()111 boolean isEmpty() { 112 return mTiles.isEmpty(); 113 } 114 size()115 int size() { 116 return mTiles.size(); 117 } 118 119 /** 120 * @return the bounding rect around any gaps in the tiles. 121 */ getGaps()122 Rect getGaps() { 123 Region difference = new Region(); 124 difference.op(mRegion.getBounds(), mRegion, Region.Op.DIFFERENCE); 125 return difference.getBounds(); 126 } 127 get(int i)128 ImageTile get(int i) { 129 return mTiles.get(i); 130 } 131 toBitmap()132 Bitmap toBitmap() { 133 return toBitmap(new Rect(0, 0, getWidth(), getHeight())); 134 } 135 136 /** 137 * @param bounds Selected portion of the tile set's bounds (equivalent to tile bounds coord 138 * space). For example, to get the whole doc, use Rect(0, 0, getWidth(), 139 * getHeight()). 140 */ toBitmap(Rect bounds)141 Bitmap toBitmap(Rect bounds) { 142 if (mTiles.isEmpty()) { 143 return null; 144 } 145 final RenderNode output = new RenderNode("Bitmap Export"); 146 output.setPosition(0, 0, bounds.width(), bounds.height()); 147 RecordingCanvas canvas = output.beginRecording(); 148 Drawable drawable = getDrawable(); 149 drawable.setBounds(bounds); 150 drawable.draw(canvas); 151 output.endRecording(); 152 return HardwareRenderer.createHardwareBitmap(output, bounds.width(), bounds.height()); 153 } 154 getLeft()155 int getLeft() { 156 return mRegion.getBounds().left; 157 } 158 getTop()159 int getTop() { 160 return mRegion.getBounds().top; 161 } 162 getRight()163 int getRight() { 164 return mRegion.getBounds().right; 165 } 166 getBottom()167 int getBottom() { 168 return mRegion.getBounds().bottom; 169 } 170 getWidth()171 int getWidth() { 172 return mRegion.getBounds().width(); 173 } 174 getHeight()175 int getHeight() { 176 return mRegion.getBounds().height(); 177 } 178 clear()179 void clear() { 180 if (mTiles.isEmpty()) { 181 return; 182 } 183 mRegion.setEmpty(); 184 Iterator<ImageTile> i = mTiles.iterator(); 185 while (i.hasNext()) { 186 ImageTile next = i.next(); 187 next.close(); 188 i.remove(); 189 } 190 notifyContentChanged(); 191 } 192 } 193