diff --git a/res/values/strings.xml b/res/values/strings.xml
index 255996fbb84..1fa85394518 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -686,9 +686,6 @@
Loading\u2026
- Location may use sources like GPS, Wi\u2011Fi, mobile networks, and sensors to help estimate
- your device\u2019s location.
-
<br><br>Apps with the Nearby devices permission can determine the
relative position of connected devices.
diff --git a/src/com/android/settings/location/LocationSettingsFooterPreferenceController.java b/src/com/android/settings/location/LocationSettingsFooterPreferenceController.java
index 48034bdd8ef..d2d5c1ffa5b 100644
--- a/src/com/android/settings/location/LocationSettingsFooterPreferenceController.java
+++ b/src/com/android/settings/location/LocationSettingsFooterPreferenceController.java
@@ -17,21 +17,42 @@
package com.android.settings.location;
import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.location.LocationManager;
import android.text.Html;
+import android.util.Log;
+import androidx.preference.Preference;
import androidx.preference.PreferenceScreen;
import com.android.settings.R;
import com.android.settingslib.widget.FooterPreference;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
/**
* Preference controller for Location Settings footer.
*/
public class LocationSettingsFooterPreferenceController extends LocationBasePreferenceController {
- FooterPreference mFooterPreference;
+ private static final String TAG = "LocationFooter";
+ private static final Intent INJECT_INTENT =
+ new Intent(LocationManager.SETTINGS_FOOTER_DISPLAYED_ACTION);
+
+ private final PackageManager mPackageManager;
+ private FooterPreference mFooterPreference;
+ private boolean mLocationEnabled;
+ private String mInjectedFooterString;
public LocationSettingsFooterPreferenceController(Context context, String key) {
super(context, key);
+ mPackageManager = context.getPackageManager();
}
@Override
@@ -42,9 +63,118 @@ public class LocationSettingsFooterPreferenceController extends LocationBasePref
@Override
public void onLocationModeChanged(int mode, boolean restricted) {
- boolean enabled = mLocationEnabler.isEnabled(mode);
- mFooterPreference.setTitle(Html.fromHtml(mContext.getString(
- enabled ? R.string.location_settings_footer_location_on
- : R.string.location_settings_footer_location_off)));
+ mLocationEnabled = mLocationEnabler.isEnabled(mode);
+ updateFooterPreference();
+ }
+
+ /**
+ * Insert footer preferences.
+ */
+ @Override
+ public void updateState(Preference preference) {
+ Collection footerData = getFooterData();
+ for (FooterData data : footerData) {
+ try {
+ mInjectedFooterString =
+ mPackageManager
+ .getResourcesForApplication(data.applicationInfo)
+ .getString(data.footerStringRes);
+ updateFooterPreference();
+ } catch (PackageManager.NameNotFoundException exception) {
+ Log.w(
+ TAG,
+ "Resources not found for application "
+ + data.applicationInfo.packageName);
+ }
+ }
+ }
+
+ private void updateFooterPreference() {
+ String footerString = mContext.getString(
+ mLocationEnabled ? R.string.location_settings_footer_location_on
+ : R.string.location_settings_footer_location_off);
+ if (mLocationEnabled) {
+ footerString = mInjectedFooterString + footerString;
+ }
+ if (mFooterPreference != null) {
+ mFooterPreference.setTitle(Html.fromHtml(footerString));
+ }
+ }
+
+ /**
+ * Location footer preference group should be displayed if there is at least one footer to
+ * inject.
+ */
+ @Override
+ public int getAvailabilityStatus() {
+ return !getFooterData().isEmpty() ? AVAILABLE : UNSUPPORTED_ON_DEVICE;
+ }
+
+ /**
+ * Return a list of strings with text provided by ACTION_INJECT_FOOTER broadcast receivers.
+ */
+ private List getFooterData() {
+ // Fetch footer text from system apps
+ List resolveInfos =
+ mPackageManager.queryBroadcastReceivers(
+ INJECT_INTENT, PackageManager.GET_META_DATA);
+ if (resolveInfos == null) {
+ Log.e(TAG, "Unable to resolve intent " + INJECT_INTENT);
+ return Collections.emptyList();
+ }
+
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "Found broadcast receivers: " + resolveInfos);
+ }
+
+ List footerDataList = new ArrayList<>(resolveInfos.size());
+ for (ResolveInfo resolveInfo : resolveInfos) {
+ ActivityInfo activityInfo = resolveInfo.activityInfo;
+ ApplicationInfo appInfo = activityInfo.applicationInfo;
+
+ // If a non-system app tries to inject footer, ignore it
+ if ((appInfo.flags & ApplicationInfo.FLAG_SYSTEM) == 0) {
+ Log.w(TAG, "Ignoring attempt to inject footer from app not in system image: "
+ + resolveInfo);
+ continue;
+ }
+
+ // Get the footer text resource id from broadcast receiver's metadata
+ if (activityInfo.metaData == null) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "No METADATA in broadcast receiver " + activityInfo.name);
+ }
+ continue;
+ }
+
+ final int footerTextRes =
+ activityInfo.metaData.getInt(LocationManager.METADATA_SETTINGS_FOOTER_STRING);
+ if (footerTextRes == 0) {
+ Log.w(
+ TAG,
+ "No mapping of integer exists for "
+ + LocationManager.METADATA_SETTINGS_FOOTER_STRING);
+ continue;
+ }
+ footerDataList.add(new FooterData(footerTextRes, appInfo));
+ }
+ return footerDataList;
+ }
+
+ /**
+ * Contains information related to a footer.
+ */
+ private static class FooterData {
+
+ // The string resource of the footer
+ public final int footerStringRes;
+
+ // Application info of receiver injecting this footer
+ public final ApplicationInfo applicationInfo;
+
+ FooterData(int footerRes, ApplicationInfo appInfo) {
+ this.footerStringRes = footerRes;
+ this.applicationInfo = appInfo;
+ }
}
}
diff --git a/tests/robotests/src/com/android/settings/location/LocationSettingsFooterPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/location/LocationSettingsFooterPreferenceControllerTest.java
new file mode 100644
index 00000000000..7929b718abe
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/location/LocationSettingsFooterPreferenceControllerTest.java
@@ -0,0 +1,214 @@
+/*
+ * Copyright (C) 2021 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.location;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyChar;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.ResolveInfo;
+import android.content.res.Resources;
+import android.location.LocationManager;
+import android.os.Bundle;
+import android.text.Html;
+
+import androidx.lifecycle.LifecycleOwner;
+import androidx.preference.PreferenceScreen;
+
+import com.android.settings.R;
+import com.android.settingslib.core.lifecycle.Lifecycle;
+import com.android.settingslib.widget.FooterPreference;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@RunWith(RobolectricTestRunner.class)
+public class LocationSettingsFooterPreferenceControllerTest {
+
+ private static final int TEST_RES_ID = 1234;
+ private static final String TEST_TEXT = "text";
+ private static final String PREFERENCE_KEY = "location_footer";
+
+ private Context mContext;
+ private LocationSettingsFooterPreferenceController mController;
+ private Lifecycle mLifecycle;
+
+ @Mock
+ private PreferenceScreen mPreferenceScreen;
+ @Mock
+ private FooterPreference mFooterPreference;
+ @Mock
+ private PackageManager mPackageManager;
+ @Mock
+ private Resources mResources;
+
+ @Before
+ public void setUp() throws NameNotFoundException {
+ MockitoAnnotations.initMocks(this);
+ mContext = spy(RuntimeEnvironment.application);
+ when(mContext.getPackageManager()).thenReturn(mPackageManager);
+
+ LifecycleOwner lifecycleOwner = () -> mLifecycle;
+ mLifecycle = new Lifecycle(lifecycleOwner);
+ LocationSettings locationSettings = spy(new LocationSettings());
+ when(locationSettings.getSettingsLifecycle()).thenReturn(mLifecycle);
+
+ mController = spy(new LocationSettingsFooterPreferenceController(mContext, PREFERENCE_KEY));
+ mController.init(locationSettings);
+
+ when(mPreferenceScreen.findPreference(PREFERENCE_KEY)).thenReturn(mFooterPreference);
+ when(mPackageManager.getResourcesForApplication(any(ApplicationInfo.class)))
+ .thenReturn(mResources);
+ when(mResources.getString(TEST_RES_ID)).thenReturn(TEST_TEXT);
+ mController.displayPreference(mPreferenceScreen);
+ }
+
+ @Test
+ public void isAvailable_hasValidFooter_returnsTrue() {
+ final List testResolveInfos = new ArrayList<>();
+ testResolveInfos.add(
+ getTestResolveInfo(/*isSystemApp*/ true, /*hasRequiredMetadata*/ true));
+ when(mPackageManager.queryBroadcastReceivers(any(Intent.class), anyInt()))
+ .thenReturn(testResolveInfos);
+
+ assertThat(mController.isAvailable()).isTrue();
+ }
+
+ @Test
+ public void isAvailable_noSystemApp_returnsFalse() {
+ final List testResolveInfos = new ArrayList<>();
+ testResolveInfos.add(
+ getTestResolveInfo(/*isSystemApp*/ false, /*hasRequiredMetadata*/ true));
+ when(mPackageManager.queryBroadcastReceivers(any(Intent.class), anyInt()))
+ .thenReturn(testResolveInfos);
+ assertThat(mController.isAvailable()).isFalse();
+ }
+
+ @Test
+ public void isAvailable_noRequiredMetadata_returnsFalse() {
+ final List testResolveInfos = new ArrayList<>();
+ testResolveInfos.add(
+ getTestResolveInfo(/*isSystemApp*/ true, /*hasRequiredMetadata*/ false));
+ when(mPackageManager.queryBroadcastReceivers(any(Intent.class), anyInt()))
+ .thenReturn(testResolveInfos);
+ assertThat(mController.isAvailable()).isFalse();
+ }
+
+ @Test
+ public void updateState_setTitle() {
+ final List testResolveInfos = new ArrayList<>();
+ testResolveInfos.add(
+ getTestResolveInfo(/*isSystemApp*/ true, /*hasRequiredMetadata*/ true));
+ when(mPackageManager.queryBroadcastReceivers(any(Intent.class), anyInt()))
+ .thenReturn(testResolveInfos);
+ mController.updateState(mFooterPreference);
+ ArgumentCaptor title = ArgumentCaptor.forClass(CharSequence.class);
+ verify(mFooterPreference).setTitle(title.capture());
+ assertThat(title.getValue()).isNotNull();
+ }
+
+ @Test
+ public void onLocationModeChanged_off_setTitle() {
+ final List testResolveInfos = new ArrayList<>();
+ testResolveInfos.add(
+ getTestResolveInfo(/*isSystemApp*/ true, /*hasRequiredMetadata*/ true));
+ when(mPackageManager.queryBroadcastReceivers(any(Intent.class), anyInt()))
+ .thenReturn(testResolveInfos);
+ mController.updateState(mFooterPreference);
+ verify(mFooterPreference).setTitle(any());
+ mController.onLocationModeChanged(/* mode= */ 0, /* restricted= */ false);
+ ArgumentCaptor title = ArgumentCaptor.forClass(CharSequence.class);
+ verify(mFooterPreference, times(2)).setTitle(title.capture());
+ assertThat(title.getValue().toString()).isEqualTo(
+ Html.fromHtml(mContext.getString(
+ R.string.location_settings_footer_location_off)).toString());
+ }
+
+ @Test
+ public void onLocationModeChanged_on_setTitle() {
+ final List testResolveInfos = new ArrayList<>();
+ testResolveInfos.add(
+ getTestResolveInfo(/*isSystemApp*/ true, /*hasRequiredMetadata*/ true));
+ when(mPackageManager.queryBroadcastReceivers(any(Intent.class), anyInt()))
+ .thenReturn(testResolveInfos);
+ mController.updateState(mFooterPreference);
+ verify(mFooterPreference).setTitle(any());
+ mController.onLocationModeChanged(/* mode= */ 1, /* restricted= */ false);
+ ArgumentCaptor title = ArgumentCaptor.forClass(CharSequence.class);
+ verify(mFooterPreference, times(2)).setTitle(title.capture());
+ assertThat(title.getValue().toString()).isNotEqualTo(
+ Html.fromHtml(mContext.getString(
+ R.string.location_settings_footer_location_off)).toString());
+ }
+
+ @Test
+ public void updateState_notSystemApp_ignore() {
+ final List testResolveInfos = new ArrayList<>();
+ testResolveInfos.add(
+ getTestResolveInfo(/*isSystemApp*/ false, /*hasRequiredMetadata*/ true));
+ when(mPackageManager.queryBroadcastReceivers(any(Intent.class), anyInt()))
+ .thenReturn(testResolveInfos);
+ mController.updateState(mFooterPreference);
+ verify(mFooterPreference, never()).setTitle(anyChar());
+ }
+
+ /**
+ * Returns a ResolveInfo object for testing
+ * @param isSystemApp If true, the application is a system app.
+ * @param hasRequiredMetaData If true, the broadcast receiver has a valid value for
+ * {@link LocationManager#METADATA_SETTINGS_FOOTER_STRING}
+ */
+ private ResolveInfo getTestResolveInfo(boolean isSystemApp, boolean hasRequiredMetaData) {
+ ResolveInfo testResolveInfo = new ResolveInfo();
+ ApplicationInfo testAppInfo = new ApplicationInfo();
+ if (isSystemApp) {
+ testAppInfo.flags |= ApplicationInfo.FLAG_SYSTEM;
+ }
+ ActivityInfo testActivityInfo = new ActivityInfo();
+ testActivityInfo.name = "TestActivityName";
+ testActivityInfo.packageName = "TestPackageName";
+ testActivityInfo.applicationInfo = testAppInfo;
+ if (hasRequiredMetaData) {
+ testActivityInfo.metaData = new Bundle();
+ testActivityInfo.metaData.putInt(
+ LocationManager.METADATA_SETTINGS_FOOTER_STRING, TEST_RES_ID);
+ }
+ testResolveInfo.activityInfo = testActivityInfo;
+ return testResolveInfo;
+ }
+}