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