• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2022 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 com.android.systemui.touch;
18 
19 import android.graphics.Rect;
20 import android.graphics.Region;
21 import android.util.Log;
22 import android.view.AttachedSurfaceControl;
23 import android.view.View;
24 import android.view.ViewGroup;
25 
26 import androidx.concurrent.futures.CallbackToFutureAdapter;
27 
28 import com.google.common.util.concurrent.ListenableFuture;
29 
30 import java.util.HashMap;
31 import java.util.HashSet;
32 import java.util.concurrent.Executor;
33 
34 /**
35  * {@link TouchInsetManager} handles setting the touchable inset regions for a given View. This
36  * is useful for passing through touch events for all but select areas.
37  */
38 public class TouchInsetManager {
39     private static final String TAG = "TouchInsetManager";
40     /**
41      * {@link TouchInsetSession} provides an individualized session with the
42      * {@link TouchInsetManager}, linking any action to the client.
43      */
44     public static class TouchInsetSession {
45         private final TouchInsetManager mManager;
46         private final HashSet<View> mTrackedViews;
47         private final Executor mExecutor;
48 
49         private final View.OnLayoutChangeListener mOnLayoutChangeListener =
50                 (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom)
51                         -> updateTouchRegions();
52 
53         private final View.OnAttachStateChangeListener mAttachListener =
54                 new View.OnAttachStateChangeListener() {
55                     @Override
56                     public void onViewAttachedToWindow(View v) {
57                         updateTouchRegions();
58                     }
59 
60                     @Override
61                     public void onViewDetachedFromWindow(View v) {
62                         updateTouchRegions();
63                     }
64                 };
65 
66         /**
67          * Default constructor
68          * @param manager The parent {@link TouchInsetManager} which will be affected by actions on
69          *                this session.
70          * @param executor An executor for marshalling operations.
71          */
TouchInsetSession(TouchInsetManager manager, Executor executor)72         TouchInsetSession(TouchInsetManager manager, Executor executor) {
73             mManager = manager;
74             mTrackedViews = new HashSet<>();
75             mExecutor = executor;
76         }
77 
78         /**
79          * Adds a descendant of the root view to be tracked.
80          * @param view {@link View} to be tracked.
81          */
addViewToTracking(View view)82         public void addViewToTracking(View view) {
83             mExecutor.execute(() -> {
84                 mTrackedViews.add(view);
85                 view.addOnAttachStateChangeListener(mAttachListener);
86                 view.addOnLayoutChangeListener(mOnLayoutChangeListener);
87                 updateTouchRegions();
88             });
89         }
90 
91         /**
92          * Removes a view from further tracking
93          * @param view {@link View} to be removed.
94          */
removeViewFromTracking(View view)95         public void removeViewFromTracking(View view) {
96             mExecutor.execute(() -> {
97                 mTrackedViews.remove(view);
98                 view.removeOnLayoutChangeListener(mOnLayoutChangeListener);
99                 view.removeOnAttachStateChangeListener(mAttachListener);
100                 updateTouchRegions();
101             });
102         }
103 
updateTouchRegions()104         private void updateTouchRegions() {
105             mExecutor.execute(() -> {
106                 final HashMap<AttachedSurfaceControl, Region> affectedSurfaces = new HashMap<>();
107                 if (mTrackedViews.isEmpty()) {
108                     return;
109                 }
110 
111                 mTrackedViews.stream().forEach(view -> {
112                     final AttachedSurfaceControl surface = view.getRootSurfaceControl();
113 
114                     // Detached views will not have a surface control.
115                     if (surface == null) {
116                         return;
117                     }
118 
119                     if (!affectedSurfaces.containsKey(surface)) {
120                         affectedSurfaces.put(surface, Region.obtain());
121                     }
122                     final Rect boundaries = new Rect();
123                     view.getDrawingRect(boundaries);
124                     ((ViewGroup) view.getRootView())
125                             .offsetDescendantRectToMyCoords(view, boundaries);
126                     affectedSurfaces.get(surface).op(boundaries, Region.Op.UNION);
127                 });
128                 mManager.setTouchRegions(this, affectedSurfaces);
129             });
130         }
131 
132         /**
133          * Removes all tracked views and updates insets accordingly.
134          */
clear()135         public void clear() {
136             mExecutor.execute(() -> {
137                 mManager.clearRegion(this);
138                 mTrackedViews.clear();
139             });
140         }
141     }
142 
143     private final HashMap<TouchInsetSession, HashMap<AttachedSurfaceControl, Region>>
144             mSessionRegions = new HashMap<>();
145     private final HashMap<AttachedSurfaceControl, Region> mLastAffectedSurfaces = new HashMap();
146     private final Executor mExecutor;
147 
148     /**
149      * Default constructor.
150      * @param executor An {@link Executor} to marshal all operations on.
151      */
TouchInsetManager(Executor executor)152     public TouchInsetManager(Executor executor) {
153         mExecutor = executor;
154     }
155 
156     /**
157      * Creates a new associated session.
158      */
createSession()159     public TouchInsetSession createSession() {
160         return new TouchInsetSession(this, mExecutor);
161     }
162 
163     /**
164      * Checks to see if the given point coordinates fall within an inset region.
165      */
checkWithinTouchRegion(int x, int y)166     public ListenableFuture<Boolean> checkWithinTouchRegion(int x, int y) {
167         return CallbackToFutureAdapter.getFuture(completer -> {
168             mExecutor.execute(() -> completer.set(
169                     mLastAffectedSurfaces.values().stream().anyMatch(
170                             region -> region.contains(x, y))));
171 
172             return "DreamOverlayTouchMonitor::checkWithinTouchRegion";
173         });
174     }
175 
updateTouchInsets()176     private void updateTouchInsets() {
177         // Get affected
178         final HashMap<AttachedSurfaceControl, Region> affectedSurfaces = new HashMap<>();
179         mSessionRegions.values().stream().forEach(regionMapping -> {
180             regionMapping.entrySet().stream().forEach(entry -> {
181                 final AttachedSurfaceControl surface = entry.getKey();
182 
183                 if (!affectedSurfaces.containsKey(surface)) {
184                     affectedSurfaces.put(surface, Region.obtain());
185                 }
186 
187                 affectedSurfaces.get(surface).op(entry.getValue(), Region.Op.UNION);
188             });
189         });
190 
191         affectedSurfaces.entrySet().stream().forEach(entry -> {
192             entry.getKey().setTouchableRegion(entry.getValue());
193         });
194 
195         mLastAffectedSurfaces.entrySet().forEach(entry -> {
196             final AttachedSurfaceControl surface = entry.getKey();
197             if (!affectedSurfaces.containsKey(surface)) {
198                 surface.setTouchableRegion(null);
199             }
200             entry.getValue().recycle();
201         });
202 
203         mLastAffectedSurfaces.clear();
204         mLastAffectedSurfaces.putAll(affectedSurfaces);
205     }
206 
setTouchRegions(TouchInsetSession session, HashMap<AttachedSurfaceControl, Region> regions)207     protected void setTouchRegions(TouchInsetSession session,
208             HashMap<AttachedSurfaceControl, Region> regions) {
209         mExecutor.execute(() -> {
210             recycleRegions(session);
211             mSessionRegions.put(session, regions);
212             updateTouchInsets();
213         });
214     }
215 
recycleRegions(TouchInsetSession session)216     private void recycleRegions(TouchInsetSession session) {
217         if (!mSessionRegions.containsKey(session)) {
218             Log.w(TAG,  "Removing a session with no regions:" + session);
219             return;
220         }
221 
222         for (Region region : mSessionRegions.get(session).values()) {
223             region.recycle();
224         }
225     }
226 
clearRegion(TouchInsetSession session)227     private void clearRegion(TouchInsetSession session) {
228         mExecutor.execute(() -> {
229             recycleRegions(session);
230             mSessionRegions.remove(session);
231             updateTouchInsets();
232         });
233     }
234 }
235