Adding app specific screen for "External Sources"
Adding app specific screen so that third party apps and package
installer can directly intent there to direct users to change the
setting. Also added automater tests.
Test: adb shell am instrument -w -e class \
'com.android.settings.applications.ExternalSourcesSettingsTest' \
'com.android.settings.tests/android.support.test.runner.AndroidJUnitRunner'
Bug: 33792674
Bug: 35487166
Change-Id: I5a481ee7032979df94a9e267ea74aa90da6a0906
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index b000c14..82632f8 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -2885,6 +2885,19 @@
android:value="com.android.settings.applications.ManageApplications" />
</activity>
+
+ <activity android:name="Settings$AppWriteSettingsActivity"
+ android:label="@string/write_settings_title"
+ android:taskAffinity="">
+ <intent-filter android:priority="1">
+ <action android:name="android.settings.action.MANAGE_WRITE_SETTINGS" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <data android:scheme="package" />
+ </intent-filter>
+ <meta-data android:name="com.android.settings.FRAGMENT_CLASS"
+ android:value="com.android.settings.applications.WriteSettingsDetails" />
+ </activity>
+
<activity android:name="Settings$ManageExternalSourcesActivity"
android:label="@string/install_other_apps"
android:taskAffinity="">
@@ -2896,16 +2909,16 @@
android:value="com.android.settings.applications.ManageApplications" />
</activity>
- <activity android:name="Settings$AppWriteSettingsActivity"
- android:label="@string/write_settings_title"
- android:taskAffinity="">
+ <activity android:name="Settings$ManageAppExternalSourcesActivity"
+ android:label="@string/install_other_apps"
+ android:taskAffinity="">
<intent-filter android:priority="1">
- <action android:name="android.settings.action.MANAGE_WRITE_SETTINGS" />
+ <action android:name="android.settings.action.MANAGE_EXTERNAL_SOURCES" />
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="package" />
</intent-filter>
<meta-data android:name="com.android.settings.FRAGMENT_CLASS"
- android:value="com.android.settings.applications.WriteSettingsDetails" />
+ android:value="com.android.settings.applications.ExternalSourcesDetails" />
</activity>
<activity android:name="ShowAdminSupportDetailsDialog"
diff --git a/src/com/android/settings/Settings.java b/src/com/android/settings/Settings.java
index a96fb1e..83162eb 100644
--- a/src/com/android/settings/Settings.java
+++ b/src/com/android/settings/Settings.java
@@ -132,6 +132,7 @@
public static class ManageExternalSourcesActivity extends SettingsActivity {
/* empty */ }
+ public static class ManageAppExternalSourcesActivity extends SettingsActivity { /* empty */ }
public static class WifiCallingSuggestionActivity extends SettingsActivity { /* empty */ }
public static class ZenModeAutomationSuggestionActivity extends SettingsActivity { /* empty */ }
diff --git a/src/com/android/settings/applications/ExternalSourcesDetails.java b/src/com/android/settings/applications/ExternalSourcesDetails.java
index af9251c..6441437 100644
--- a/src/com/android/settings/applications/ExternalSourcesDetails.java
+++ b/src/com/android/settings/applications/ExternalSourcesDetails.java
@@ -15,6 +15,9 @@
*/
package com.android.settings.applications;
+import static android.app.Activity.RESULT_CANCELED;
+import static android.app.Activity.RESULT_OK;
+
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
import android.app.AlertDialog;
@@ -26,6 +29,7 @@
import android.support.v7.preference.Preference.OnPreferenceChangeListener;
import com.android.settings.R;
+import com.android.settings.Settings;
import com.android.settings.applications.AppStateInstallAppsBridge.InstallAppsState;
import com.android.settingslib.applications.ApplicationsState.AppEntry;
@@ -67,6 +71,10 @@
final boolean checked = (Boolean) newValue;
if (preference == mSwitchPref) {
if (mInstallAppsState != null && checked != mInstallAppsState.canInstallApps()) {
+ if (Settings.ManageAppExternalSourcesActivity.class.getName().equals(
+ getIntent().getComponent().getClassName())) {
+ setResult(checked ? RESULT_OK : RESULT_CANCELED);
+ }
setCanInstallApps(checked);
refreshUi();
}
@@ -97,9 +105,13 @@
protected boolean refreshUi() {
mInstallAppsState = mAppBridge.createInstallAppsStateFor(mPackageName,
mPackageInfo.applicationInfo.uid);
-
- final boolean canWrite = mInstallAppsState.canInstallApps();
- mSwitchPref.setChecked(canWrite);
+ if (!mInstallAppsState.isPotentialAppSource()) {
+ // Invalid app entry. Should not allow changing permission
+ mSwitchPref.setEnabled(false);
+ return true;
+ }
+ final boolean canInstallApps = mInstallAppsState.canInstallApps();
+ mSwitchPref.setChecked(canInstallApps);
return true;
}
diff --git a/src/com/android/settings/core/gateway/SettingsGateway.java b/src/com/android/settings/core/gateway/SettingsGateway.java
index 742f727..8c3dfc1 100644
--- a/src/com/android/settings/core/gateway/SettingsGateway.java
+++ b/src/com/android/settings/core/gateway/SettingsGateway.java
@@ -50,6 +50,7 @@
import com.android.settings.applications.AdvancedAppSettings;
import com.android.settings.applications.AppAndNotificationDashboardFragment;
import com.android.settings.applications.DrawOverlayDetails;
+import com.android.settings.applications.ExternalSourcesDetails;
import com.android.settings.applications.InstalledAppDetails;
import com.android.settings.applications.ManageApplications;
import com.android.settings.applications.ManageDomainUrls;
@@ -214,6 +215,7 @@
ProcessStatsSummary.class.getName(),
DrawOverlayDetails.class.getName(),
WriteSettingsDetails.class.getName(),
+ ExternalSourcesDetails.class.getName(),
AdvancedAppSettings.class.getName(),
WallpaperTypeSettings.class.getName(),
VrListenerSettings.class.getName(),
diff --git a/tests/app/AndroidManifest.xml b/tests/app/AndroidManifest.xml
index 1c50700..6659e5f 100644
--- a/tests/app/AndroidManifest.xml
+++ b/tests/app/AndroidManifest.xml
@@ -22,6 +22,9 @@
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.USE_CREDENTIALS" />
+ <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
+ <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS" />
+ <uses-permission android:name="android.permission.UPDATE_APP_OPS_STATS" />
<application>
<uses-library android:name="android.test.runner" />
diff --git a/tests/app/src/com/android/settings/applications/ExternalSourcesSettingsTest.java b/tests/app/src/com/android/settings/applications/ExternalSourcesSettingsTest.java
new file mode 100644
index 0000000..9114c6f
--- /dev/null
+++ b/tests/app/src/com/android/settings/applications/ExternalSourcesSettingsTest.java
@@ -0,0 +1,200 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.applications;
+
+import static android.app.AppOpsManager.MODE_ALLOWED;
+import static android.app.AppOpsManager.MODE_DEFAULT;
+import static android.app.AppOpsManager.MODE_ERRORED;
+import static android.app.AppOpsManager.OP_REQUEST_INSTALL_PACKAGES;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import android.app.AppOpsManager;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.UserInfo;
+import android.net.Uri;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.provider.Settings;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.LargeTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.BySelector;
+import android.support.test.uiautomator.Direction;
+
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject2;
+import android.support.test.uiautomator.Until;
+import android.widget.ListView;
+import android.widget.Switch;
+import android.widget.TextView;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.List;
+
+@RunWith(AndroidJUnit4.class)
+@LargeTest
+public class ExternalSourcesSettingsTest {
+
+ private static final String TAG = ExternalSourcesSettingsTest.class.getSimpleName();
+ private static final String WM_DISMISS_KEYGUARD_COMMAND = "wm dismiss-keyguard";
+ private static final long START_ACTIVITY_TIMEOUT = 5000;
+
+ private Context mContext;
+ private UiDevice mUiDevice;
+ private PackageManager mPackageManager;
+ private AppOpsManager mAppOpsManager;
+ private List<UserInfo> mProfiles;
+ private String mPackageName;
+
+ @Before
+ public void setUp() throws Exception {
+ mContext = InstrumentationRegistry.getTargetContext();
+ mPackageName = InstrumentationRegistry.getContext().getPackageName();
+ mPackageManager = mContext.getPackageManager();
+ mAppOpsManager = mContext.getSystemService(AppOpsManager.class);
+ mProfiles = mContext.getSystemService(UserManager.class).getProfiles(UserHandle.myUserId());
+ resetAppOpModeForAllProfiles();
+ mUiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
+ mUiDevice.wakeUp();
+ mUiDevice.executeShellCommand(WM_DISMISS_KEYGUARD_COMMAND);
+ }
+
+ private void resetAppOpModeForAllProfiles() throws Exception {
+ for (UserInfo user : mProfiles) {
+ final int uid = mPackageManager.getPackageUidAsUser(mPackageName, user.id);
+ mAppOpsManager.setMode(OP_REQUEST_INSTALL_PACKAGES, uid, mPackageName, MODE_DEFAULT);
+ }
+ }
+
+ private Intent createManageExternalSourcesListIntent() {
+ final Intent manageExternalSourcesIntent = new Intent();
+ manageExternalSourcesIntent.setAction(Settings.ACTION_MANAGE_EXTERNAL_SOURCES);
+ return manageExternalSourcesIntent;
+ }
+
+ private Intent createManageExternalSourcesAppIntent(String packageName) {
+ final Intent intent = createManageExternalSourcesListIntent();
+ intent.setData(Uri.parse("package:" + packageName));
+ return intent;
+ }
+
+ private String getApplicationLabel(String packageName) throws Exception {
+ final ApplicationInfo info = mPackageManager.getApplicationInfo(packageName, 0);
+ return mPackageManager.getApplicationLabel(info).toString();
+ }
+
+ private UiObject2 findAndVerifySwitchState(boolean checked) {
+ final BySelector switchSelector = By.clazz(Switch.class).res("android:id/switch_widget");
+ final UiObject2 switchPref = mUiDevice.wait(Until.findObject(switchSelector),
+ START_ACTIVITY_TIMEOUT);
+ assertNotNull("Switch not shown", switchPref);
+ assertTrue("Switch in invalid state", switchPref.isChecked() == checked);
+ return switchPref;
+ }
+
+ @Test
+ public void testManageExternalSourcesList() throws Exception {
+ final String testAppLabel = getApplicationLabel(mPackageName);
+
+ mContext.startActivity(createManageExternalSourcesListIntent());
+ final BySelector preferenceListSelector = By.clazz(ListView.class).res("android:id/list");
+ final UiObject2 preferenceList = mUiDevice.wait(Until.findObject(preferenceListSelector),
+ START_ACTIVITY_TIMEOUT);
+ assertNotNull("App list not shown", preferenceList);
+
+ final BySelector appLabelTextViewSelector = By.clazz(TextView.class)
+ .res("android:id/title")
+ .text(testAppLabel);
+ List<UiObject2> listOfMatchingTextViews;
+ do {
+ listOfMatchingTextViews = preferenceList.findObjects(appLabelTextViewSelector);
+ // assuming the number of profiles will be sufficiently small so that all the entries
+ // for the same package will fit in one screen at some time during the scroll.
+ } while (listOfMatchingTextViews.size() != mProfiles.size() &&
+ preferenceList.scroll(Direction.DOWN, 0.2f));
+ assertEquals("Test app not listed for each profile", mProfiles.size(),
+ listOfMatchingTextViews.size());
+
+ for (UiObject2 matchingObject : listOfMatchingTextViews) {
+ matchingObject.click();
+ findAndVerifySwitchState(true);
+ mUiDevice.pressBack();
+ }
+ }
+
+ private void testAppDetailScreenForAppOp(int appOpMode, int userId) throws Exception {
+ final String testAppLabel = getApplicationLabel(mPackageName);
+ final BySelector appDetailTitleSelector = By.clazz(TextView.class)
+ .res("com.android.settings:id/app_detail_title")
+ .text(testAppLabel);
+
+ mAppOpsManager.setMode(OP_REQUEST_INSTALL_PACKAGES,
+ mPackageManager.getPackageUidAsUser(mPackageName, userId), mPackageName, appOpMode);
+ mContext.startActivityAsUser(createManageExternalSourcesAppIntent(mPackageName),
+ UserHandle.of(userId));
+ mUiDevice.wait(Until.findObject(appDetailTitleSelector), START_ACTIVITY_TIMEOUT);
+ findAndVerifySwitchState(appOpMode == MODE_ALLOWED || appOpMode == MODE_DEFAULT);
+ mUiDevice.pressBack();
+ }
+
+ @Test
+ public void testManageExternalSourcesForApp() throws Exception {
+ // App op MODE_DEFAULT is already tested in #testManageExternalSourcesList
+ for (UserInfo user : mProfiles) {
+ testAppDetailScreenForAppOp(MODE_ALLOWED, user.id);
+ testAppDetailScreenForAppOp(MODE_ERRORED, user.id);
+ }
+ }
+
+ private void testSwitchToggle(int fromAppOp, int toAppOp) throws Exception {
+ final int packageUid = mPackageManager.getPackageUid(mPackageName, 0);
+ final boolean initialState = (fromAppOp == MODE_ALLOWED || fromAppOp == MODE_DEFAULT);
+
+ mAppOpsManager.setMode(OP_REQUEST_INSTALL_PACKAGES, packageUid, mPackageName, fromAppOp);
+ mContext.startActivity(createManageExternalSourcesAppIntent(mPackageName));
+ final UiObject2 switchPref = findAndVerifySwitchState(initialState);
+ switchPref.click();
+ Thread.sleep(1000);
+ assertEquals("Toggling switch did not change app op", toAppOp,
+ mAppOpsManager.checkOpNoThrow(OP_REQUEST_INSTALL_PACKAGES, packageUid,
+ mPackageName));
+ mUiDevice.pressBack();
+ }
+
+ @Test
+ public void testIfSwitchTogglesAppOp() throws Exception {
+ testSwitchToggle(MODE_ALLOWED, MODE_ERRORED);
+ testSwitchToggle(MODE_ERRORED, MODE_ALLOWED);
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ mUiDevice.pressHome();
+ resetAppOpModeForAllProfiles();
+ }
+}