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