1 /* 2 * Copyright (C) 2023 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 android.telecom.cts.apps; 18 19 import static org.junit.Assert.fail; 20 21 import android.content.ComponentName; 22 import android.content.Context; 23 import android.content.Intent; 24 import android.content.ServiceConnection; 25 import android.os.IBinder; 26 import android.os.SystemClock; 27 import android.util.Log; 28 import android.util.Pair; 29 30 import java.util.HashMap; 31 import java.util.Map; 32 import java.util.concurrent.CompletableFuture; 33 import java.util.concurrent.TimeUnit; 34 35 public class BindUtils { 36 private static final String TAG = BindUtils.class.getSimpleName(); 37 private static final Map<TelecomTestApp, Pair<TelecomAppServiceConnection, AppControlWrapper>> 38 sTelecomAppToService = new HashMap<>(); 39 hasBoundTestApp()40 public static boolean hasBoundTestApp() { 41 // If the sTelecomAppToService is NOT empty, that means that an app has not unbound yet 42 return !sTelecomAppToService.isEmpty(); 43 } 44 printBoundTestApps()45 public static void printBoundTestApps() { 46 for (TelecomTestApp testAppName : sTelecomAppToService.keySet()) { 47 Log.i(TAG, String.format("printBoundTestApps: [%s] is currently bound", testAppName)); 48 49 } 50 } 51 52 private static class TelecomAppServiceConnection implements ServiceConnection { 53 private static final String TAG = TelecomAppServiceConnection.class.getSimpleName(); 54 private final CompletableFuture<IAppControl> mFuture; 55 TelecomAppServiceConnection(CompletableFuture<IAppControl> future)56 TelecomAppServiceConnection(CompletableFuture<IAppControl> future) { 57 mFuture = future; 58 } 59 60 @Override onServiceConnected(ComponentName name, IBinder service)61 public void onServiceConnected(ComponentName name, IBinder service) { 62 Log.i(TAG, String.format("onServiceConnected: ComponentName=[%s]", name)); 63 mFuture.complete(IAppControl.Stub.asInterface(service)); 64 } 65 66 @Override onServiceDisconnected(ComponentName name)67 public void onServiceDisconnected(ComponentName name) { 68 Log.i(TAG, String.format("onServiceDisconnected: ComponentName=[%s]", name)); 69 mFuture.complete(null); 70 } 71 } 72 73 /** 74 * This helper method handles the binding process for a given test app. It ensures the 75 * that the test app signals that onBind is finished executing before returning control to the 76 * test. 77 */ bindToApp(Context context, TelecomTestApp appName)78 public AppControlWrapper bindToApp(Context context, TelecomTestApp appName) 79 throws Exception { 80 // if a test app is already bound, return the existing binding 81 if (sTelecomAppToService.containsKey(appName)) { 82 return sTelecomAppToService.get(appName).second; 83 } 84 final AppControlWrapper appControl = waitOnBindForApp(context, appName); 85 // For debugging purposes, it is good to know how much time for an app to signal it bound! 86 Log.i(TAG, String.format("bindToApp: wait for %s to signal onBind is complete ", appName)); 87 long startTimeMillis = SystemClock.elapsedRealtime(); 88 WaitUntil.waitUntilConditionIsTrueOrTimeout(new Condition() { 89 @Override 90 public Object expected() { 91 return true; 92 } 93 94 @Override 95 public Object actual() { 96 return appControl.isBound(); 97 } 98 }, 99 WaitUntil.DEFAULT_TIMEOUT_MS, 100 "Timed out waiting for isBound to return <TRUE> for [" + appName + "]"); 101 long elapsed = SystemClock.elapsedRealtime() - startTimeMillis; 102 Log.i(TAG, String.format("bindToApp: %s took %d milliseconds signal it was bound", 103 appName, elapsed)); 104 return appControl; 105 } 106 107 /** 108 * This helper method handles the unbinding process for a given test app. It ensures the 109 * that the test app signals that onUnbind is finished executing before returning control to the 110 * test. 111 */ unbindFromApp(Context context, AppControlWrapper appControl)112 public void unbindFromApp(Context context, AppControlWrapper appControl) { 113 TelecomTestApp name = appControl.getTelecomApps(); 114 Log.i(TAG, String.format("unbindFromApplication: applicationName=[%s]", name)); 115 if (!sTelecomAppToService.containsKey(name)) { 116 Log.i(TAG, String.format("cannot find the service binder for application=[%s]", name)); 117 return; 118 } 119 try { 120 TelecomAppServiceConnection serviceConnection = sTelecomAppToService.get(name).first; 121 context.unbindService(serviceConnection); 122 long startTimeMillis = SystemClock.elapsedRealtime(); 123 Log.i(TAG, String.format("unbindFromApp: wait for %s to signal onUnbind is complete ", 124 name)); 125 WaitUntil.waitUntilConditionIsTrueOrTimeout(new Condition() { 126 @Override 127 public Object expected() { 128 return false; 129 } 130 131 @Override 132 public Object actual() { 133 return appControl.isBound(); 134 } 135 }, 136 WaitUntil.DEFAULT_TIMEOUT_MS, 137 "Timed out waiting for isBound to return <FALSE>"); 138 long elapsedMs = SystemClock.elapsedRealtime() - startTimeMillis; 139 Log.i(TAG, String.format("unbindFromApp: %s took %d milliseconds to signal it was " 140 + "unbound", name, elapsedMs)); 141 } catch (Exception e) { 142 // Note: Do not throw the UnBind Exception! Otherwise, the test will fail with an unbind 143 // error instead of a potential underlying cause. 144 Log.e(TAG, String.format("unbindFromApplication: app=[%s], e=[%s]", name, e)); 145 } 146 finally { 147 sTelecomAppToService.remove(name); 148 } 149 } 150 createBindIntentForApplication(TelecomTestApp application)151 private Intent createBindIntentForApplication(TelecomTestApp application) throws Exception { 152 Intent bindIntent = new Intent(getBindActionFromApplicationName(application)); 153 bindIntent.setPackage(getPackageNameFromApplicationName(application)); 154 return bindIntent; 155 } 156 getBindActionFromApplicationName(TelecomTestApp app)157 private String getBindActionFromApplicationName(TelecomTestApp app) throws Exception { 158 switch (app) { 159 case TransactionalVoipAppMain, TransactionalVoipAppClone -> { 160 return TelecomTestApp.T_CONTROL_INTERFACE_ACTION; 161 } 162 case ConnectionServiceVoipAppMain, ConnectionServiceVoipAppClone -> { 163 return TelecomTestApp.VOIP_CS_CONTROL_INTERFACE_ACTION; 164 } 165 case ManagedConnectionServiceApp, ManagedConnectionServiceAppClone -> { 166 return TelecomTestApp.CONTROL_INTERFACE_ACTION; 167 } 168 } 169 throw new Exception( 170 String.format("%s doesn't have a <CONTROL_INTERFACE> mapping." + app)); 171 } 172 getPackageNameFromApplicationName(TelecomTestApp app)173 private static String getPackageNameFromApplicationName(TelecomTestApp app) throws Exception { 174 switch (app) { 175 case TransactionalVoipAppMain -> { 176 return TelecomTestApp.TRANSACTIONAL_PACKAGE_NAME; 177 } 178 case TransactionalVoipAppClone -> { 179 return TelecomTestApp.TRANSACTIONAL_CLONE_PACKAGE_NAME; 180 } 181 case ConnectionServiceVoipAppMain -> { 182 return TelecomTestApp.SELF_MANAGED_CS_MAIN_PACKAGE_NAME; 183 } 184 case ConnectionServiceVoipAppClone -> { 185 return TelecomTestApp.SELF_MANAGED_CS_CLONE_PACKAGE_NAME; 186 } 187 case ManagedConnectionServiceApp -> { 188 return TelecomTestApp.MANAGED_PACKAGE_NAME; 189 } 190 case ManagedConnectionServiceAppClone -> { 191 return TelecomTestApp.MANAGED_CLONE_PACKAGE_NAME; 192 } 193 } 194 throw new Exception( 195 String.format("%s doesn't have a <PACKAGE_NAME> mapping.", app)); 196 } 197 waitOnBindForApp(Context context, TelecomTestApp appName)198 private AppControlWrapper waitOnBindForApp(Context context, TelecomTestApp appName) 199 throws Exception { 200 TelecomAppServiceConnection serviceConnection; 201 if (sTelecomAppToService.containsKey(appName)) { 202 Log.i(TAG, String.format("returning cached service binder for application=[%s]", 203 appName)); 204 return sTelecomAppToService.get(appName).second; 205 } else { 206 CompletableFuture<IAppControl> f = new CompletableFuture<>(); 207 serviceConnection = new TelecomAppServiceConnection(f); 208 Log.i(TAG, String.format("waitOnBindForApp: requesting bind to %s", appName)); 209 long startTimeMillis = SystemClock.elapsedRealtime(); 210 boolean success = context.bindService(createBindIntentForApplication(appName), 211 serviceConnection, Context.BIND_AUTO_CREATE); 212 long elapsedMs = SystemClock.elapsedRealtime() - startTimeMillis; 213 Log.i(TAG, String.format("waitOnBindForApp: finished bind to %s in %d milliseconds", 214 appName, elapsedMs)); 215 if (!success) { 216 fail("Failed to get control interface -- bind error"); 217 } 218 IAppControl iAppControl = f.get(WaitUntil.DEFAULT_TIMEOUT_MS, TimeUnit.MILLISECONDS); 219 AppControlWrapper wrapper = new AppControlWrapper(iAppControl, appName); 220 sTelecomAppToService.put(appName, new Pair<>(serviceConnection, wrapper)); 221 return wrapper; 222 } 223 } 224 } 225