1 /*
2  * Copyright (C) 2020 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;
18 
19 import static android.telecom.cts.TestUtils.WAIT_FOR_STATE_CHANGE_TIMEOUT_MS;
20 
21 import static com.android.compatibility.common.util.ShellIdentityUtils.invokeMethodWithShellPermissionsNoReturn;
22 
23 import android.app.AppOpsManager;
24 import android.content.ComponentName;
25 import android.content.Context;
26 import android.content.Intent;
27 import android.content.ServiceConnection;
28 import android.content.pm.PackageManager;
29 import android.net.Uri;
30 import android.os.Bundle;
31 import android.os.IBinder;
32 import android.os.RemoteException;
33 import android.telecom.Call;
34 import android.telecom.TelecomManager;
35 import android.telecom.cts.thirdptyincallservice.CtsThirdPartyInCallService;
36 import android.telecom.cts.thirdptyincallservice.CtsThirdPartyInCallServiceControl;
37 import android.telecom.cts.thirdptyincallservice.ICtsThirdPartyInCallServiceControl;
38 import android.util.Log;
39 
40 import com.android.compatibility.common.util.ApiTest;
41 
42 import java.util.concurrent.CountDownLatch;
43 import java.util.concurrent.TimeUnit;
44 
45 public class ThirdPartyInCallServiceAppOpsPermissionTest extends BaseTelecomTestWithMockServices {
46 
47     private static final String TAG = ThirdPartyInCallServiceAppOpsPermissionTest
48             .class.getSimpleName();
49     private static final String THIRD_PARITY_PACKAGE_NAME = CtsThirdPartyInCallService
50             .class.getPackage().getName();
51     private static final Uri TEST_URI = Uri.parse("tel:555-TEST");
52     private static final String TEST_KEY = "woowoo";
53     private static final String TEST_VALUE = "yay";
54     private Context mContext;
55     private AppOpsManager mAppOpsManager;
56     private PackageManager mPackageManager;
57     private TelecomManager mTelecomManager;
58     ICtsThirdPartyInCallServiceControl mICtsThirdPartyInCallServiceControl;
59     private boolean mExpectedTearDownBindingStatus;
60 
61     @Override
setUp()62     public void setUp() throws Exception {
63         super.setUp();
64         if (!mShouldTestTelecom) {
65             return;
66         }
67         mContext = getInstrumentation().getContext();
68         mAppOpsManager = mContext.getSystemService(AppOpsManager.class);
69         mPackageManager = mContext.getPackageManager();
70         mTelecomManager = mContext.getSystemService(TelecomManager.class);
71         setupConnectionService(null, FLAG_REGISTER | FLAG_ENABLE);
72         setUpControl();
73         mICtsThirdPartyInCallServiceControl.resetLatchForServiceBound(false);
74         mExpectedTearDownBindingStatus = true;
75     }
76 
77     @Override
tearDown()78     public void tearDown() throws Exception {
79         resetRemoteCalls();
80         super.tearDown();
81         if (mShouldTestTelecom && mExpectedTearDownBindingStatus) {
82             // A bind status of false (unbound) requires tearDown() to run to tear down the
83             // ConnectionService. If tearDown generates an exception, this assert will also fail,
84             // so it is safe after tearDown.
85             assertBindStatus(/* true: bind, false: unbind */false, /* expected result */true);
86         }
87     }
88 
testWithoutAppOpsPermission()89     public void testWithoutAppOpsPermission() throws Exception {
90         if (!mShouldTestTelecom) {
91             return;
92         }
93         setInCallServiceAppOpsPermission(false);
94         int previousCallCount = mICtsThirdPartyInCallServiceControl.getLocalCallCount();
95         addAndVerifyNewIncomingCall(TEST_URI, null);
96         assertBindStatus(/* true: bind, false: unbind */true, /* expected result */false);
97         assertCallCount(previousCallCount);
98         // Third Party InCallService hasn't been bound yet, unbound latch can be null when tearDown.
99         mExpectedTearDownBindingStatus = false;
100     }
101 
testWithAppOpsPermission()102     public void testWithAppOpsPermission() throws Exception {
103         if (!mShouldTestTelecom) {
104             return;
105         }
106         // Grant App Ops Permission
107         setInCallServiceAppOpsPermission(true);
108 
109         int previousCallCount = mICtsThirdPartyInCallServiceControl.getLocalCallCount();
110         addAndVerifyNewIncomingCall(TEST_URI, null);
111         assertBindStatus(/* true: bind, false: unbind */true, /* expected result */true);
112         assertCallCount(previousCallCount + 1);
113         mICtsThirdPartyInCallServiceControl.resetLatchForServiceBound(true);
114 
115         // Revoke App Ops Permission
116         setInCallServiceAppOpsPermission(false);
117     }
118 
119     /**
120      * Verifies that {@link android.telecom.Call#putExtras(Bundle)} changes made in one
121      * {@link android.telecom.InCallService} instance will be seen in other running
122      * {@link android.telecom.InCallService} instances.
123      * @throws Exception
124      */
125     @ApiTest(apis = {"android.telecom.Call#putExtras",
126             "android.telecom.Call.Callback#onDetailsChanged"})
testExtrasPropagation()127     public void testExtrasPropagation() throws Exception {
128         if (!mShouldTestTelecom) {
129             return;
130         }
131         // Grant App Ops Permission
132         setInCallServiceAppOpsPermission(true);
133         try {
134             // Make a new call.
135             int previousCallCount = mICtsThirdPartyInCallServiceControl.getLocalCallCount();
136             addAndVerifyNewIncomingCall(TEST_URI, null);
137             assertBindStatus(/* true: bind, false: unbind */true, /* expected result */true);
138             assertCallCount(previousCallCount + 1);
139             android.telecom.Call call = mInCallCallbacks.getService().getLastCall();
140 
141             // Make it active
142             final MockConnection connection = verifyConnectionForIncomingCall();
143             connection.setActive();
144             assertCallState(call, Call.STATE_ACTIVE);
145 
146             // Prime the controlled other ICS to expect some known extras.
147             mICtsThirdPartyInCallServiceControl.setExpectedExtra(TEST_KEY, TEST_VALUE);
148 
149             // From the main ICS in the CTS test runner, we'll add an extra.
150             Bundle newExtras = new Bundle();
151             newExtras.putString(TEST_KEY, TEST_VALUE);
152             call.putExtras(newExtras);
153 
154             // Now wait for the other ICS to have received that extra.
155             assertTrue(mICtsThirdPartyInCallServiceControl.waitUntilExpectedExtrasReceived());
156         } finally {
157             mICtsThirdPartyInCallServiceControl.resetLatchForServiceBound(true);
158             // Revoke App Ops Permission
159             setInCallServiceAppOpsPermission(false);
160         }
161     }
162 
163     /**
164      *
165      * @param bind: check the status of InCallService bind latches.
166      *             Values: true (bound latch), false (unbound latch).
167      * @param success: whether the latch should have counted down.
168      */
assertBindStatus(boolean bind, boolean success)169     private void assertBindStatus(boolean bind, boolean success) {
170         waitUntilConditionIsTrueOrTimeout(new Condition() {
171             @Override
172             public Object expected() {
173                 return success;
174             }
175 
176             @Override
177             public Object actual() {
178                 try {
179                     return mICtsThirdPartyInCallServiceControl.checkBindStatus(bind);
180                 } catch (RemoteException re) {
181                     Log.e(TAG, "Remote exception when checking bind status: " + re);
182                     return false;
183                 }
184             }
185         }, WAIT_FOR_STATE_CHANGE_TIMEOUT_MS, "Unable to " + (bind ? "Bind" : "Unbind")
186                 + " third party in call service");
187     }
188 
resetRemoteCalls()189     private void resetRemoteCalls() {
190         if (mICtsThirdPartyInCallServiceControl != null) {
191             try {
192                 mICtsThirdPartyInCallServiceControl.resetCalls();
193             } catch (Exception e) {
194                 Log.w(TAG, "resetRemoteCalls ran into an exception: " + e);
195             }
196         }
197     }
198 
assertCallCount(int expected)199     private void assertCallCount(int expected) {
200         waitUntilConditionIsTrueOrTimeout(new Condition() {
201             @Override
202             public Object expected() {
203                 return expected;
204             }
205 
206             @Override
207             public Object actual() {
208                 try {
209                     return mICtsThirdPartyInCallServiceControl.getLocalCallCount();
210                 } catch (RemoteException re) {
211                     Log.e(TAG, "Remote exception when getting local call count: " + re);
212                     return -1;
213                 }
214             }
215          }, WAIT_FOR_STATE_CHANGE_TIMEOUT_MS,
216                 "Failed to match localCallCount and expected: " + expected);
217     }
218 
setUpControl()219     private void setUpControl() throws InterruptedException {
220         Intent bindIntent = new Intent(CtsThirdPartyInCallServiceControl.CONTROL_INTERFACE_ACTION);
221         // mContext is android.telecom.cts, which doesn't include thirdptyincallservice.
222         ComponentName controlComponentName =
223               ComponentName.createRelative(
224                     CtsThirdPartyInCallServiceControl.class.getPackage().getName(),
225                             CtsThirdPartyInCallServiceControl.class.getName());
226 
227         bindIntent.setComponent(controlComponentName);
228         final CountDownLatch bindLatch = new CountDownLatch(1);
229         boolean success = mContext.bindService(bindIntent, new ServiceConnection() {
230             @Override
231             public void onServiceConnected(ComponentName name, IBinder service) {
232                 Log.i(TAG, "Service Connected: " + name);
233                 mICtsThirdPartyInCallServiceControl =
234                         ICtsThirdPartyInCallServiceControl.Stub.asInterface(service);
235                 bindLatch.countDown();
236             }
237 
238             @Override
239             public void onServiceDisconnected(ComponentName name) {
240                 mICtsThirdPartyInCallServiceControl = null;
241             }
242         }, Context.BIND_AUTO_CREATE);
243         if (!success) {
244             fail("Failed to get control interface -- bind error");
245         }
246         bindLatch.await(WAIT_FOR_STATE_CHANGE_TIMEOUT_MS, TimeUnit.MILLISECONDS);
247     }
248 
setInCallServiceAppOpsPermission(boolean allow)249     private void setInCallServiceAppOpsPermission(boolean allow)
250             throws PackageManager.NameNotFoundException {
251         int uid = mPackageManager.getApplicationInfo(THIRD_PARITY_PACKAGE_NAME, 0).uid;
252         invokeMethodWithShellPermissionsNoReturn(mAppOpsManager,
253               (appOpsMan) -> appOpsMan.setUidMode(AppOpsManager.OPSTR_MANAGE_ONGOING_CALLS,
254                       uid, allow ? AppOpsManager.MODE_ALLOWED : AppOpsManager.opToDefaultMode(
255                               AppOpsManager.OPSTR_MANAGE_ONGOING_CALLS)));
256     }
257 }
258