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 17 package com.android.settings.development; 18 19 import static com.android.settings.development.DevelopmentOptionsActivityRequestCodes.REQUEST_MOCK_LOCATION_APP; 20 21 import android.Manifest; 22 import android.app.Activity; 23 import android.app.AppOpsManager; 24 import android.app.settings.SettingsEnums; 25 import android.content.Context; 26 import android.content.Intent; 27 import android.content.pm.ApplicationInfo; 28 import android.content.pm.PackageManager; 29 import android.os.Bundle; 30 import android.provider.Settings; 31 import android.text.TextUtils; 32 33 import androidx.annotation.Nullable; 34 import androidx.annotation.VisibleForTesting; 35 import androidx.preference.Preference; 36 37 import com.android.settings.core.PreferenceControllerMixin; 38 import com.android.settings.core.SubSettingLauncher; 39 import com.android.settingslib.development.DeveloperOptionsPreferenceController; 40 41 import java.util.List; 42 43 public class MockLocationAppPreferenceController extends DeveloperOptionsPreferenceController 44 implements PreferenceControllerMixin, OnActivityResultListener { 45 46 private static final String MOCK_LOCATION_APP_KEY = "mock_location_app"; 47 private static final int[] MOCK_LOCATION_APP_OPS = new int[]{AppOpsManager.OP_MOCK_LOCATION}; 48 49 @Nullable private final DevelopmentSettingsDashboardFragment mFragment; 50 private final AppOpsManager mAppsOpsManager; 51 private final PackageManager mPackageManager; 52 MockLocationAppPreferenceController(Context context, @Nullable DevelopmentSettingsDashboardFragment fragment)53 public MockLocationAppPreferenceController(Context context, 54 @Nullable DevelopmentSettingsDashboardFragment fragment) { 55 super(context); 56 57 mFragment = fragment; 58 mAppsOpsManager = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE); 59 mPackageManager = context.getPackageManager(); 60 } 61 62 @Override getPreferenceKey()63 public String getPreferenceKey() { 64 return MOCK_LOCATION_APP_KEY; 65 } 66 67 @Override handlePreferenceTreeClick(Preference preference)68 public boolean handlePreferenceTreeClick(Preference preference) { 69 if (!TextUtils.equals(preference.getKey(), getPreferenceKey())) { 70 return false; 71 } 72 if (Flags.deprecateListActivity()) { 73 final Bundle args = new Bundle(); 74 args.putString(DevelopmentAppPicker.EXTRA_REQUESTING_PERMISSION, 75 Manifest.permission.ACCESS_MOCK_LOCATION); 76 final String debugApp = Settings.Global.getString( 77 mContext.getContentResolver(), Settings.Global.DEBUG_APP); 78 args.putString(DevelopmentAppPicker.EXTRA_SELECTING_APP, debugApp); 79 new SubSettingLauncher(mContext) 80 .setDestination(DevelopmentAppPicker.class.getName()) 81 .setSourceMetricsCategory(SettingsEnums.DEVELOPMENT) 82 .setArguments(args) 83 .setTitleRes(com.android.settingslib.R.string.select_application) 84 .setResultListener(mFragment, REQUEST_MOCK_LOCATION_APP) 85 .launch(); 86 } else { 87 final Intent intent = new Intent(mContext, AppPicker.class); 88 intent.putExtra(AppPicker.EXTRA_REQUESTIING_PERMISSION, 89 Manifest.permission.ACCESS_MOCK_LOCATION); 90 mFragment.startActivityForResult(intent, REQUEST_MOCK_LOCATION_APP); 91 } 92 return true; 93 } 94 95 @Override updateState(Preference preference)96 public void updateState(Preference preference) { 97 updateMockLocation(); 98 } 99 100 @Override onActivityResult(int requestCode, int resultCode, Intent data)101 public boolean onActivityResult(int requestCode, int resultCode, Intent data) { 102 if (requestCode != REQUEST_MOCK_LOCATION_APP || resultCode != Activity.RESULT_OK) { 103 return false; 104 } 105 writeMockLocation(data.getAction()); 106 updateMockLocation(); 107 return true; 108 } 109 110 @Override onDeveloperOptionsDisabled()111 public void onDeveloperOptionsDisabled() { 112 super.onDeveloperOptionsDisabled(); 113 removeAllMockLocations(); 114 } 115 updateMockLocation()116 private void updateMockLocation() { 117 final String mockLocationApp = getCurrentMockLocationApp(); 118 119 if (!TextUtils.isEmpty(mockLocationApp)) { 120 mPreference.setSummary( 121 mContext.getResources() 122 .getString(com.android.settingslib.R.string.mock_location_app_set, 123 getAppLabel(mockLocationApp))); 124 } else { 125 mPreference.setSummary( 126 mContext.getResources() 127 .getString(com.android.settingslib.R.string.mock_location_app_not_set)); 128 } 129 } 130 writeMockLocation(String mockLocationAppName)131 private void writeMockLocation(String mockLocationAppName) { 132 removeAllMockLocations(); 133 // Enable the app op of the new mock location app if such. 134 if (!TextUtils.isEmpty(mockLocationAppName)) { 135 try { 136 final ApplicationInfo ai = mPackageManager.getApplicationInfo( 137 mockLocationAppName, PackageManager.MATCH_DISABLED_COMPONENTS); 138 mAppsOpsManager.setMode(AppOpsManager.OP_MOCK_LOCATION, ai.uid, 139 mockLocationAppName, AppOpsManager.MODE_ALLOWED); 140 } catch (PackageManager.NameNotFoundException e) { 141 /* ignore */ 142 } 143 } 144 } 145 getAppLabel(String mockLocationApp)146 private String getAppLabel(String mockLocationApp) { 147 try { 148 final ApplicationInfo ai = mPackageManager.getApplicationInfo( 149 mockLocationApp, PackageManager.MATCH_DISABLED_COMPONENTS); 150 final CharSequence appLabel = mPackageManager.getApplicationLabel(ai); 151 return appLabel != null ? appLabel.toString() : mockLocationApp; 152 } catch (PackageManager.NameNotFoundException e) { 153 return mockLocationApp; 154 } 155 } 156 removeAllMockLocations()157 private void removeAllMockLocations() { 158 // Disable the app op of the previous mock location app if such. 159 final List<AppOpsManager.PackageOps> packageOps = mAppsOpsManager.getPackagesForOps( 160 MOCK_LOCATION_APP_OPS); 161 if (packageOps == null) { 162 return; 163 } 164 // Should be one but in case we are in a bad state due to use of command line tools. 165 for (AppOpsManager.PackageOps packageOp : packageOps) { 166 if (packageOp.getOps().get(0).getMode() != AppOpsManager.MODE_ERRORED) { 167 removeMockLocationForApp(packageOp.getPackageName()); 168 } 169 } 170 } 171 removeMockLocationForApp(String appName)172 private void removeMockLocationForApp(String appName) { 173 try { 174 final ApplicationInfo ai = mPackageManager.getApplicationInfo( 175 appName, PackageManager.MATCH_DISABLED_COMPONENTS); 176 mAppsOpsManager.setMode(AppOpsManager.OP_MOCK_LOCATION, ai.uid, 177 appName, AppOpsManager.MODE_ERRORED); 178 } catch (PackageManager.NameNotFoundException e) { 179 /* ignore */ 180 } 181 } 182 183 @VisibleForTesting getCurrentMockLocationApp()184 String getCurrentMockLocationApp() { 185 final List<AppOpsManager.PackageOps> packageOps = mAppsOpsManager.getPackagesForOps( 186 MOCK_LOCATION_APP_OPS); 187 if (packageOps != null) { 188 for (AppOpsManager.PackageOps packageOp : packageOps) { 189 if (packageOp.getOps().get(0).getMode() == AppOpsManager.MODE_ALLOWED) { 190 return packageOp.getPackageName(); 191 } 192 } 193 } 194 return null; 195 } 196 } 197