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