diff --git a/src/com/android/settings/development/DevelopmentOptionsActivityRequestCodes.java b/src/com/android/settings/development/DevelopmentOptionsActivityRequestCodes.java index 667ae3115e2..b7b27591df9 100644 --- a/src/com/android/settings/development/DevelopmentOptionsActivityRequestCodes.java +++ b/src/com/android/settings/development/DevelopmentOptionsActivityRequestCodes.java @@ -23,4 +23,6 @@ public interface DevelopmentOptionsActivityRequestCodes { int REQUEST_CODE_ENABLE_OEM_UNLOCK = 0; int REQUEST_CODE_DEBUG_APP = 1; + + int REQUEST_MOCK_LOCATION_APP = 2; } diff --git a/src/com/android/settings/development/DevelopmentSettingsDashboardFragment.java b/src/com/android/settings/development/DevelopmentSettingsDashboardFragment.java index 767c7cae026..5d7a1642584 100644 --- a/src/com/android/settings/development/DevelopmentSettingsDashboardFragment.java +++ b/src/com/android/settings/development/DevelopmentSettingsDashboardFragment.java @@ -263,7 +263,7 @@ public class DevelopmentSettingsDashboardFragment extends RestrictedDashboardFra controllers.add(new ClearAdbKeysPreferenceController(context, fragment)); controllers.add(new LocalTerminalPreferenceController(context)); controllers.add(new BugReportInPowerPreferenceControllerV2(context)); - // select mock location app + controllers.add(new MockLocationAppPreferenceController(context, fragment)); controllers.add(new DebugViewAttributesPreferenceController(context)); controllers.add(new SelectDebugAppPreferenceController(context, fragment)); controllers.add(new WaitForDebuggerPreferenceController(context)); diff --git a/src/com/android/settings/development/MockLocationAppPreferenceController.java b/src/com/android/settings/development/MockLocationAppPreferenceController.java new file mode 100644 index 00000000000..9f6c4d3d1e8 --- /dev/null +++ b/src/com/android/settings/development/MockLocationAppPreferenceController.java @@ -0,0 +1,183 @@ +/* + * 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.development; + +import static com.android.settings.development.DevelopmentOptionsActivityRequestCodes + .REQUEST_MOCK_LOCATION_APP; + +import android.Manifest; +import android.app.Activity; +import android.app.AppOpsManager; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.support.v7.preference.Preference; +import android.support.v7.preference.PreferenceScreen; +import android.text.TextUtils; + +import com.android.settings.R; +import com.android.settingslib.wrapper.PackageManagerWrapper; + +import java.util.List; + +public class MockLocationAppPreferenceController extends DeveloperOptionsPreferenceController { + + private static final String MOCK_LOCATION_APP_KEY = "mock_location_app"; + private static final int[] MOCK_LOCATION_APP_OPS = new int[]{AppOpsManager.OP_MOCK_LOCATION}; + + private final DevelopmentSettingsDashboardFragment mFragment; + private final AppOpsManager mAppsOpsManager; + private final PackageManagerWrapper mPackageManager; + private Preference mPreference; + + public MockLocationAppPreferenceController(Context context, + DevelopmentSettingsDashboardFragment fragment) { + super(context); + + mFragment = fragment; + mAppsOpsManager = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE); + mPackageManager = new PackageManagerWrapper(context.getPackageManager()); + } + + @Override + public String getPreferenceKey() { + return MOCK_LOCATION_APP_KEY; + } + + @Override + public void displayPreference(PreferenceScreen screen) { + super.displayPreference(screen); + + mPreference = screen.findPreference(getPreferenceKey()); + } + + @Override + public boolean handlePreferenceTreeClick(Preference preference) { + if (!TextUtils.equals(preference.getKey(), getPreferenceKey())) { + return false; + } + final Intent intent = new Intent(mContext, AppPicker.class); + intent.putExtra(AppPicker.EXTRA_REQUESTIING_PERMISSION, + Manifest.permission.ACCESS_MOCK_LOCATION); + mFragment.startActivityForResult(intent, REQUEST_MOCK_LOCATION_APP); + return true; + } + + @Override + public void updateState(Preference preference) { + updateMockLocation(); + } + + @Override + public boolean onActivityResult(int requestCode, int resultCode, Intent data) { + if (requestCode != REQUEST_MOCK_LOCATION_APP || resultCode != Activity.RESULT_OK) { + return false; + } + writeMockLocation(data.getAction()); + updateMockLocation(); + return true; + } + + @Override + protected void onDeveloperOptionsSwitchEnabled() { + mPreference.setEnabled(true); + } + + @Override + protected void onDeveloperOptionsSwitchDisabled() { + mPreference.setEnabled(false); + } + + private void updateMockLocation() { + final String mockLocationApp = getCurrentMockLocationApp(); + + if (!TextUtils.isEmpty(mockLocationApp)) { + mPreference.setSummary( + mContext.getResources().getString(R.string.mock_location_app_set, + getAppLabel(mockLocationApp))); + } else { + mPreference.setSummary( + mContext.getResources().getString(R.string.mock_location_app_not_set)); + } + } + + private void writeMockLocation(String mockLocationAppName) { + removeAllMockLocations(); + // Enable the app op of the new mock location app if such. + if (!TextUtils.isEmpty(mockLocationAppName)) { + try { + final ApplicationInfo ai = mPackageManager.getApplicationInfo( + mockLocationAppName, PackageManager.MATCH_DISABLED_COMPONENTS); + mAppsOpsManager.setMode(AppOpsManager.OP_MOCK_LOCATION, ai.uid, + mockLocationAppName, AppOpsManager.MODE_ALLOWED); + } catch (PackageManager.NameNotFoundException e) { + /* ignore */ + } + } + } + + private String getAppLabel(String mockLocationApp) { + try { + final ApplicationInfo ai = mPackageManager.getApplicationInfo( + mockLocationApp, PackageManager.MATCH_DISABLED_COMPONENTS); + final CharSequence appLabel = mPackageManager.getApplicationLabel(ai); + return appLabel != null ? appLabel.toString() : mockLocationApp; + } catch (PackageManager.NameNotFoundException e) { + return mockLocationApp; + } + } + + private void removeAllMockLocations() { + // Disable the app op of the previous mock location app if such. + final List packageOps = mAppsOpsManager.getPackagesForOps( + MOCK_LOCATION_APP_OPS); + if (packageOps == null) { + return; + } + // Should be one but in case we are in a bad state due to use of command line tools. + for (AppOpsManager.PackageOps packageOp : packageOps) { + if (packageOp.getOps().get(0).getMode() != AppOpsManager.MODE_ERRORED) { + removeMockLocationForApp(packageOp.getPackageName()); + } + } + } + + private void removeMockLocationForApp(String appName) { + try { + final ApplicationInfo ai = mPackageManager.getApplicationInfo( + appName, PackageManager.MATCH_DISABLED_COMPONENTS); + mAppsOpsManager.setMode(AppOpsManager.OP_MOCK_LOCATION, ai.uid, + appName, AppOpsManager.MODE_ERRORED); + } catch (PackageManager.NameNotFoundException e) { + /* ignore */ + } + } + + private String getCurrentMockLocationApp() { + final List packageOps = mAppsOpsManager.getPackagesForOps( + MOCK_LOCATION_APP_OPS); + if (packageOps != null) { + for (AppOpsManager.PackageOps packageOp : packageOps) { + if (packageOp.getOps().get(0).getMode() == AppOpsManager.MODE_ALLOWED) { + return packageOps.get(0).getPackageName(); + } + } + } + return null; + } +} diff --git a/tests/robotests/src/com/android/settings/development/MockLocationAppPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/development/MockLocationAppPreferenceControllerTest.java new file mode 100644 index 00000000000..0aab0dbc823 --- /dev/null +++ b/tests/robotests/src/com/android/settings/development/MockLocationAppPreferenceControllerTest.java @@ -0,0 +1,166 @@ +package com.android.settings.development; + +import static com.android.settings.development.DevelopmentOptionsActivityRequestCodes + .REQUEST_MOCK_LOCATION_APP; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.Activity; +import android.app.AppOpsManager; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.support.v7.preference.Preference; +import android.support.v7.preference.PreferenceScreen; + +import com.android.settings.R; +import com.android.settings.TestConfig; +import com.android.settings.testutils.SettingsRobolectricTestRunner; +import com.android.settingslib.wrapper.PackageManagerWrapper; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; +import org.robolectric.util.ReflectionHelpers; + +import java.util.Collections; + +@RunWith(SettingsRobolectricTestRunner.class) +@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION) +public class MockLocationAppPreferenceControllerTest { + + @Mock + private DevelopmentSettingsDashboardFragment mFragment; + @Mock + private AppOpsManager mAppOpsManager; + @Mock + private PackageManagerWrapper mPackageManager; + @Mock + private Preference mPreference; + @Mock + private PreferenceScreen mScreen; + @Mock + private AppOpsManager.PackageOps mPackageOps; + @Mock + private AppOpsManager.OpEntry mOpEntry; + @Mock + private ApplicationInfo mApplicationInfo; + + private Context mContext; + private MockLocationAppPreferenceController mController; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + mContext = RuntimeEnvironment.application; + mController = spy(new MockLocationAppPreferenceController(mContext, mFragment)); + ReflectionHelpers.setField(mController, "mAppsOpsManager", mAppOpsManager); + ReflectionHelpers.setField(mController, "mPackageManager", mPackageManager); + when(mScreen.findPreference(mController.getPreferenceKey())).thenReturn(mPreference); + mController.displayPreference(mScreen); + } + + @Test + public void updateState_foobarAppSelected_shouldSetSummaryToFoobar() { + final String appName = "foobar"; + when(mAppOpsManager.getPackagesForOps(any())).thenReturn( + Collections.singletonList(mPackageOps)); + when(mPackageOps.getOps()).thenReturn(Collections.singletonList(mOpEntry)); + when(mOpEntry.getMode()).thenReturn(AppOpsManager.MODE_ALLOWED); + when(mPackageOps.getPackageName()).thenReturn(appName); + + mController.updateState(mPreference); + + verify(mPreference).setSummary( + mContext.getResources().getString(R.string.mock_location_app_set, appName)); + } + + @Test + public void updateState_noAppSelected_shouldSetSummaryToDefault() { + when(mAppOpsManager.getPackagesForOps(any())).thenReturn( + Collections.emptyList()); + + mController.updateState(mPreference); + + verify(mPreference).setSummary( + mContext.getResources().getString(R.string.mock_location_app_not_set)); + } + + @Test + public void onActivityResult_fooPrevAppBarNewApp_shouldRemoveFooAndSetBarAsMockLocationApp() + throws PackageManager.NameNotFoundException { + final String prevAppName = "foo"; + final String newAppName = "bar"; + final Intent intent = new Intent(); + intent.setAction(newAppName); + when(mAppOpsManager.getPackagesForOps(any())).thenReturn( + Collections.singletonList(mPackageOps)); + when(mPackageOps.getOps()).thenReturn(Collections.singletonList(mOpEntry)); + when(mOpEntry.getMode()).thenReturn(AppOpsManager.MODE_ALLOWED); + when(mPackageOps.getPackageName()).thenReturn(prevAppName); + when(mPackageManager.getApplicationInfo(anyString(), + eq(PackageManager.MATCH_DISABLED_COMPONENTS))).thenReturn(mApplicationInfo); + + final boolean handled = mController.onActivityResult(REQUEST_MOCK_LOCATION_APP, + Activity.RESULT_OK, intent); + + assertThat(handled).isTrue(); + verify(mAppOpsManager).setMode(anyInt(), anyInt(), eq(newAppName), + eq(AppOpsManager.MODE_ALLOWED)); + verify(mAppOpsManager).setMode(anyInt(), anyInt(), eq(prevAppName), + eq(AppOpsManager.MODE_ERRORED)); + } + + @Test + public void onActivityResult_incorrectCode_shouldReturnFalse() { + final boolean handled = mController.onActivityResult(30983150 /* request code */, + Activity.RESULT_OK, null /* intent */); + + assertThat(handled).isFalse(); + } + + @Test + public void handlePreferenceTreeClick_samePreferenceKey_shouldLaunchActivity() { + final String preferenceKey = mController.getPreferenceKey(); + when(mPreference.getKey()).thenReturn(preferenceKey); + + final boolean handled = mController.handlePreferenceTreeClick(mPreference); + + assertThat(handled).isTrue(); + verify(mFragment).startActivityForResult(any(), eq(REQUEST_MOCK_LOCATION_APP)); + } + + @Test + public void handlePreferenceTreeClick_incorrectPreferenceKey_shouldReturnFalse() { + when(mPreference.getKey()).thenReturn("SomeRandomKey"); + + assertThat(mController.handlePreferenceTreeClick(mPreference)).isFalse(); + } + + @Test + public void onDeveloperOptionsSwitchDisabled_shouldDisablePreference() { + mController.onDeveloperOptionsSwitchDisabled(); + + verify(mPreference).setEnabled(false); + } + + @Test + public void onDeveloperOptionsSwitchEnabled_shouldEnablePreference() { + mController.onDeveloperOptionsSwitchEnabled(); + + verify(mPreference).setEnabled(true); + } +}