1 /*
2  * Copyright (C) 2017 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 package com.android.settings.applications;
17 
18 import static android.app.AppOpsManager.MODE_ALLOWED;
19 import static android.app.AppOpsManager.MODE_DEFAULT;
20 import static android.app.AppOpsManager.MODE_ERRORED;
21 import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK;
22 import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
23 
24 import static org.junit.Assert.assertEquals;
25 import static org.junit.Assert.assertNotNull;
26 import static org.junit.Assert.assertTrue;
27 
28 import android.app.AppOpsManager;
29 import android.content.Context;
30 import android.content.Intent;
31 import android.content.pm.ApplicationInfo;
32 import android.content.pm.PackageManager;
33 import android.content.pm.UserInfo;
34 import android.net.Uri;
35 import android.os.UserHandle;
36 import android.os.UserManager;
37 import android.support.test.uiautomator.By;
38 import android.support.test.uiautomator.BySelector;
39 import android.support.test.uiautomator.Direction;
40 import android.support.test.uiautomator.UiDevice;
41 import android.support.test.uiautomator.UiObject2;
42 import android.support.test.uiautomator.Until;
43 import android.widget.Switch;
44 import android.widget.TextView;
45 
46 import androidx.recyclerview.widget.RecyclerView;
47 import androidx.test.InstrumentationRegistry;
48 
49 import org.junit.After;
50 import org.junit.Before;
51 import org.junit.Test;
52 
53 import java.util.List;
54 
55 /**
56  * An abstract parent for testing settings activities that manage an AppOps permission.
57  */
58 abstract public class AppOpsSettingsTest {
59     private static final String WM_DISMISS_KEYGUARD_COMMAND = "wm dismiss-keyguard";
60     private static final long START_ACTIVITY_TIMEOUT = 5000;
61 
62     private Context mContext;
63     private UiDevice mUiDevice;
64     private PackageManager mPackageManager;
65     private AppOpsManager mAppOpsManager;
66     private List<UserInfo> mProfiles;
67     private String mPackageName;
68 
69     // These depend on which app op's settings UI is being tested.
70     private final String mActivityAction;
71     private final int mAppOpCode;
72 
AppOpsSettingsTest(String activityAction, int appOpCode)73     protected AppOpsSettingsTest(String activityAction, int appOpCode) {
74         mActivityAction = activityAction;
75         mAppOpCode = appOpCode;
76     }
77 
78     @Before
setUp()79     public void setUp() throws Exception {
80         mContext = InstrumentationRegistry.getTargetContext();
81         mPackageName = InstrumentationRegistry.getContext().getPackageName();
82         mPackageManager = mContext.getPackageManager();
83         mAppOpsManager = mContext.getSystemService(AppOpsManager.class);
84         mProfiles = mContext.getSystemService(UserManager.class).getProfiles(UserHandle.myUserId());
85         resetAppOpModeForAllProfiles();
86         mUiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
87         mUiDevice.wakeUp();
88         mUiDevice.executeShellCommand(WM_DISMISS_KEYGUARD_COMMAND);
89     }
90 
resetAppOpModeForAllProfiles()91     private void resetAppOpModeForAllProfiles() throws Exception {
92         for (UserInfo user : mProfiles) {
93             final int uid = mPackageManager.getPackageUidAsUser(mPackageName, user.id);
94             mAppOpsManager.setMode(mAppOpCode, uid, mPackageName, MODE_DEFAULT);
95         }
96     }
97 
98     /**
99      * Creates an intent for showing the permission settings for all apps.
100      */
createManageAllAppsIntent()101     private Intent createManageAllAppsIntent() {
102         final Intent intent = new Intent(mActivityAction);
103         intent.addFlags(FLAG_ACTIVITY_CLEAR_TASK | FLAG_ACTIVITY_NEW_TASK);
104         return intent;
105     }
106 
107     /**
108      * Creates an intent for showing the permission setting for a single app.
109      */
createManageSingleAppIntent(String packageName)110     private Intent createManageSingleAppIntent(String packageName) {
111         final Intent intent = createManageAllAppsIntent();
112         intent.setData(Uri.parse("package:" + packageName));
113         return intent;
114     }
115 
getApplicationLabel(String packageName)116     private String getApplicationLabel(String packageName) throws Exception {
117         final ApplicationInfo info = mPackageManager.getApplicationInfo(packageName, 0);
118         return mPackageManager.getApplicationLabel(info).toString();
119     }
120 
findAndVerifySwitchState(boolean checked)121     private UiObject2 findAndVerifySwitchState(boolean checked) {
122         final BySelector switchSelector = By.clazz(Switch.class).res("android:id/switch_widget");
123         final UiObject2 switchPref = mUiDevice.wait(Until.findObject(switchSelector),
124                 START_ACTIVITY_TIMEOUT);
125         assertNotNull("Switch not shown", switchPref);
126         assertTrue("Switch in invalid state", switchPref.isChecked() == checked);
127         return switchPref;
128     }
129 
130     @Test
testAppList()131     public void testAppList() throws Exception {
132         final String testAppLabel = getApplicationLabel(mPackageName);
133 
134         mContext.startActivity(createManageAllAppsIntent());
135         final BySelector preferenceListSelector =
136                 By.clazz(RecyclerView.class).res("com.android.settings:id/apps_list");
137         final UiObject2 preferenceList = mUiDevice.wait(Until.findObject(preferenceListSelector),
138                 START_ACTIVITY_TIMEOUT);
139         assertNotNull("App list not shown", preferenceList);
140 
141         final BySelector appLabelTextViewSelector = By.clazz(TextView.class)
142                 .res("android:id/title")
143                 .text(testAppLabel);
144         List<UiObject2> listOfMatchingTextViews;
145         do {
146             listOfMatchingTextViews = preferenceList.findObjects(appLabelTextViewSelector);
147             // assuming the number of profiles will be sufficiently small so that all the entries
148             // for the same package will fit in one screen at some time during the scroll.
149         } while (listOfMatchingTextViews.size() != mProfiles.size() &&
150                 preferenceList.scroll(Direction.DOWN, 0.2f));
151         assertEquals("Test app not listed for each profile", mProfiles.size(),
152                 listOfMatchingTextViews.size());
153 
154         for (UiObject2 matchingObject : listOfMatchingTextViews) {
155             matchingObject.click();
156             findAndVerifySwitchState(true);
157             mUiDevice.pressBack();
158         }
159     }
160 
testAppDetailScreenForAppOp(int appOpMode, int userId)161     private void testAppDetailScreenForAppOp(int appOpMode, int userId) throws Exception {
162         final String testAppLabel = getApplicationLabel(mPackageName);
163         final BySelector appDetailTitleSelector = By.clazz(TextView.class)
164                 .res("com.android.settings:id/app_detail_title")
165                 .text(testAppLabel);
166 
167         mAppOpsManager.setMode(mAppOpCode,
168                 mPackageManager.getPackageUidAsUser(mPackageName, userId), mPackageName, appOpMode);
169         mContext.startActivityAsUser(createManageSingleAppIntent(mPackageName),
170                 UserHandle.of(userId));
171         mUiDevice.wait(Until.findObject(appDetailTitleSelector), START_ACTIVITY_TIMEOUT);
172         findAndVerifySwitchState(appOpMode == MODE_ALLOWED || appOpMode == MODE_DEFAULT);
173         mUiDevice.pressBack();
174     }
175 
176     @Test
testSingleApp()177     public void testSingleApp() throws Exception {
178         // App op MODE_DEFAULT is already tested in #testAppList
179         for (UserInfo user : mProfiles) {
180             testAppDetailScreenForAppOp(MODE_ALLOWED, user.id);
181             testAppDetailScreenForAppOp(MODE_ERRORED, user.id);
182         }
183     }
184 
testSwitchToggle(int fromAppOp, int toAppOp)185     private void testSwitchToggle(int fromAppOp, int toAppOp) throws Exception {
186         final int packageUid = mPackageManager.getPackageUid(mPackageName, 0);
187         final boolean initialState = (fromAppOp == MODE_ALLOWED || fromAppOp == MODE_DEFAULT);
188 
189         mAppOpsManager.setMode(mAppOpCode, packageUid, mPackageName, fromAppOp);
190         mContext.startActivity(createManageSingleAppIntent(mPackageName));
191         final UiObject2 switchPref = findAndVerifySwitchState(initialState);
192         switchPref.click();
193         Thread.sleep(1000);
194         assertEquals("Toggling switch did not change app op", toAppOp,
195                 mAppOpsManager.checkOpNoThrow(mAppOpCode, packageUid,
196                         mPackageName));
197         mUiDevice.pressBack();
198     }
199 
200     @Test
testIfSwitchTogglesAppOp()201     public void testIfSwitchTogglesAppOp() throws Exception {
202         testSwitchToggle(MODE_ALLOWED, MODE_ERRORED);
203         testSwitchToggle(MODE_ERRORED, MODE_ALLOWED);
204     }
205 
206     @After
tearDown()207     public void tearDown() throws Exception {
208         mUiDevice.pressHome();
209         resetAppOpModeForAllProfiles();
210     }
211 }
212