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 17 package com.google.snippet.ranging; 18 19 import android.app.UiAutomation; 20 import android.content.Context; 21 import android.net.ConnectivityManager; 22 import android.ranging.RangingCapabilities; 23 import android.ranging.RangingData; 24 import android.ranging.RangingDevice; 25 import android.ranging.RangingManager; 26 import android.ranging.RangingManager.RangingTechnology; 27 import android.ranging.RangingPreference; 28 import android.ranging.RangingSession; 29 import android.util.Log; 30 31 import androidx.annotation.NonNull; 32 import androidx.test.platform.app.InstrumentationRegistry; 33 34 import com.google.android.mobly.snippet.Snippet; 35 import com.google.android.mobly.snippet.event.EventCache; 36 import com.google.android.mobly.snippet.event.SnippetEvent; 37 import com.google.android.mobly.snippet.rpc.AsyncRpc; 38 import com.google.android.mobly.snippet.rpc.Rpc; 39 40 import java.lang.reflect.Method; 41 import java.util.Map; 42 import java.util.concurrent.ConcurrentHashMap; 43 import java.util.concurrent.ConcurrentMap; 44 import java.util.concurrent.Executor; 45 import java.util.concurrent.Executors; 46 47 public class RangingSnippet implements Snippet { 48 private static final String TAG = "GenericRangingSnippet"; 49 50 private final Context mContext; 51 private final RangingManager mRangingManager; 52 private final Executor mExecutor = Executors.newSingleThreadExecutor(); 53 private final EventCache mEventCache = EventCache.getInstance(); 54 private final ConnectivityManager mConnectivityManager; 55 private final ConcurrentMap<String, RangingSessionInfo> mSessions; 56 private final ConcurrentMap<Integer, Integer> mTechnologyAvailability; 57 RangingSnippet()58 public RangingSnippet() { 59 mContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); 60 mConnectivityManager = mContext.getSystemService(ConnectivityManager.class); 61 mRangingManager = mContext.getSystemService(RangingManager.class); 62 63 mSessions = new ConcurrentHashMap<>(); 64 mTechnologyAvailability = new ConcurrentHashMap<>(); 65 mRangingManager.registerCapabilitiesCallback(mExecutor, new AvailabilityListener()); 66 } 67 adoptShellPermission()68 private void adoptShellPermission() { 69 UiAutomation uia = InstrumentationRegistry.getInstrumentation().getUiAutomation(); 70 uia.adoptShellPermissionIdentity(); 71 try { 72 Class<?> cls = Class.forName("android.app.UiAutomation"); 73 Method destroyMethod = cls.getDeclaredMethod("destroy"); 74 destroyMethod.invoke(uia); 75 } catch (ReflectiveOperationException e) { 76 throw new IllegalStateException("Failed to cleanup ui automation", e); 77 } 78 } 79 dropShellPermission()80 private void dropShellPermission() throws Throwable { 81 UiAutomation uia = InstrumentationRegistry.getInstrumentation().getUiAutomation(); 82 uia.dropShellPermissionIdentity(); 83 try { 84 Class<?> cls = Class.forName("android.app.UiAutomation"); 85 Method destroyMethod = cls.getDeclaredMethod("destroy"); 86 destroyMethod.invoke(uia); 87 } catch (ReflectiveOperationException e) { 88 throw new IllegalStateException("Failed to cleanup ui automation", e); 89 } 90 } 91 92 private enum Event { 93 OPENED, 94 OPEN_FAILED, 95 STARTED, 96 DATA, 97 STOPPED, 98 CLOSED 99 } 100 101 private class RangingSessionCallback implements RangingSession.Callback { 102 103 private final String mCallbackId; 104 RangingSessionCallback(String callbackId)105 RangingSessionCallback(String callbackId) { 106 mCallbackId = callbackId; 107 } 108 109 @Override onOpened()110 public void onOpened() { 111 Log.d(TAG, "onOpened"); 112 mEventCache.postEvent(new SnippetEvent(mCallbackId, Event.OPENED.toString())); 113 } 114 115 @Override onOpenFailed(@eason int reason)116 public void onOpenFailed(@Reason int reason) { 117 Log.d(TAG, "onOpenFailed"); 118 mEventCache.postEvent(new SnippetEvent(mCallbackId, Event.OPEN_FAILED.toString())); 119 } 120 121 @Override onStarted(@onNull RangingDevice peer, @RangingTechnology int technology)122 public void onStarted(@NonNull RangingDevice peer, @RangingTechnology int technology) { 123 Log.d(TAG, "onStarted"); 124 SnippetEvent event = new SnippetEvent(mCallbackId, Event.STARTED.toString()); 125 event.getData().putString("peer", peer.getUuid().toString()); 126 event.getData().putInt("technology", technology); 127 mEventCache.postEvent(event); 128 } 129 130 @Override onResults(@onNull RangingDevice peer, @NonNull RangingData data)131 public void onResults(@NonNull RangingDevice peer, @NonNull RangingData data) { 132 Log.d(TAG, "onData"); 133 SnippetEvent event = new SnippetEvent(mCallbackId, Event.DATA.toString()); 134 event.getData().putString("peer", peer.getUuid().toString()); 135 event.getData().putInt("technology", data.getRangingTechnology()); 136 mEventCache.postEvent(event); 137 } 138 139 @Override onStopped(@onNull RangingDevice peer, @RangingTechnology int technology)140 public void onStopped(@NonNull RangingDevice peer, @RangingTechnology int technology) { 141 Log.d(TAG, "onStopped"); 142 SnippetEvent event = new SnippetEvent(mCallbackId, Event.STOPPED.toString()); 143 event.getData().putString("peer", peer.getUuid().toString()); 144 event.getData().putInt("technology", technology); 145 mEventCache.postEvent(event); 146 } 147 148 @Override onClosed(@eason int reason)149 public void onClosed(@Reason int reason) { 150 Log.d(TAG, "onClosed"); 151 mEventCache.postEvent(new SnippetEvent(mCallbackId, Event.CLOSED.toString())); 152 } 153 } 154 155 private static class RangingSessionInfo { 156 private final RangingSession mSession; 157 private final RangingSessionCallback mCallback; 158 RangingSessionInfo(RangingSession session, RangingSessionCallback callback)159 RangingSessionInfo(RangingSession session, RangingSessionCallback callback) { 160 mSession = session; 161 mCallback = callback; 162 } 163 getSession()164 public RangingSession getSession() { 165 return mSession; 166 } 167 getCallback()168 public RangingSessionCallback getCallback() { 169 return mCallback; 170 } 171 } 172 173 private class AvailabilityListener implements RangingManager.RangingCapabilitiesCallback { 174 @Override onRangingCapabilities(@onNull RangingCapabilities capabilities)175 public void onRangingCapabilities(@NonNull RangingCapabilities capabilities) { 176 Map<Integer, Integer> availabilities = capabilities.getTechnologyAvailability(); 177 mTechnologyAvailability.putAll(availabilities); 178 } 179 } 180 181 182 @AsyncRpc(description = "Start a ranging session") startRanging( String callbackId, String sessionHandle, RangingPreference preference )183 public void startRanging( 184 String callbackId, String sessionHandle, RangingPreference preference 185 ) { 186 RangingSessionCallback callback = new RangingSessionCallback(callbackId); 187 RangingSession session = mRangingManager.createRangingSession(mExecutor, callback); 188 mSessions.put(sessionHandle, new RangingSessionInfo(session, callback)); 189 session.start(preference); 190 } 191 192 @AsyncRpc(description = "Stop a ranging session") stopRanging(String unused, String sessionHandle)193 public void stopRanging(String unused, String sessionHandle) { 194 RangingSessionInfo sessionInfo = mSessions.get(sessionHandle); 195 if (sessionInfo != null) { 196 sessionInfo.getSession().stop(); 197 mSessions.remove(sessionHandle); 198 } 199 } 200 201 @Rpc(description = "Check whether the provided ranging technology is enabled") isTechnologyEnabled(int technology)202 public boolean isTechnologyEnabled(int technology) { 203 Integer availability = mTechnologyAvailability.get(technology); 204 return availability != null 205 && availability == RangingCapabilities.ENABLED; 206 } 207 208 @Rpc(description = "Check whether the provided ranging technology is supported") isTechnologySupported(int technology)209 public boolean isTechnologySupported(int technology) { 210 Integer availability = mTechnologyAvailability.get(technology); 211 return availability != null 212 && availability != RangingCapabilities.NOT_SUPPORTED; 213 } 214 215 @Rpc(description = "Set airplane mode") setAirplaneMode(boolean enabled)216 public void setAirplaneMode(boolean enabled) throws Throwable { 217 runWithShellPermission(() -> mConnectivityManager.setAirplaneMode(enabled)); 218 } 219 220 @Rpc(description = "Log info level message to device logcat") logInfo(String message)221 public void logInfo(String message) { 222 Log.i(TAG, message); 223 } 224 runWithShellPermission(Runnable action)225 public void runWithShellPermission(Runnable action) throws Throwable { 226 adoptShellPermission(); 227 try { 228 action.run(); 229 } finally { 230 dropShellPermission(); 231 } 232 } 233 } 234