1 /* 2 * Copyright (C) 2018 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file 5 * except in compliance with the License. You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software distributed under the 10 * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 11 * KIND, either express or implied. See the License for the specific language governing 12 * permissions and limitations under the License. 13 */ 14 package com.android.settings.location; 15 16 import android.content.ComponentName; 17 import android.content.Context; 18 import android.content.Intent; 19 import android.content.pm.ActivityInfo; 20 import android.content.pm.ApplicationInfo; 21 import android.content.pm.PackageManager; 22 import android.content.pm.PackageManager.NameNotFoundException; 23 import android.content.pm.ResolveInfo; 24 import android.location.LocationManager; 25 import android.util.Log; 26 27 import androidx.annotation.VisibleForTesting; 28 import androidx.preference.Preference; 29 import androidx.preference.PreferenceCategory; 30 31 import com.android.settingslib.core.lifecycle.Lifecycle; 32 import com.android.settingslib.core.lifecycle.LifecycleObserver; 33 import com.android.settingslib.core.lifecycle.events.OnPause; 34 import com.android.settingslib.widget.FooterPreference; 35 36 import java.util.ArrayList; 37 import java.util.Collection; 38 import java.util.Collections; 39 import java.util.List; 40 41 /** 42 * Preference controller for location footer preference category 43 */ 44 public class LocationFooterPreferenceController extends LocationBasePreferenceController 45 implements LifecycleObserver, OnPause { 46 private static final String TAG = "LocationFooter"; 47 private static final String KEY_LOCATION_FOOTER = "location_footer"; 48 private static final Intent INJECT_INTENT = 49 new Intent(LocationManager.SETTINGS_FOOTER_DISPLAYED_ACTION); 50 private final Context mContext; 51 private final PackageManager mPackageManager; 52 private Collection<ComponentName> mFooterInjectors; 53 LocationFooterPreferenceController(Context context, Lifecycle lifecycle)54 public LocationFooterPreferenceController(Context context, Lifecycle lifecycle) { 55 super(context, lifecycle); 56 mContext = context; 57 mPackageManager = mContext.getPackageManager(); 58 mFooterInjectors = new ArrayList<>(); 59 if (lifecycle != null) { 60 lifecycle.addObserver(this); 61 } 62 } 63 64 @Override getPreferenceKey()65 public String getPreferenceKey() { 66 return KEY_LOCATION_FOOTER; 67 } 68 69 /** 70 * Insert footer preferences. Send a {@link LocationManager#SETTINGS_FOOTER_DISPLAYED_ACTION} 71 * broadcast to receivers who have injected a footer 72 */ 73 @Override updateState(Preference preference)74 public void updateState(Preference preference) { 75 PreferenceCategory category = (PreferenceCategory) preference; 76 category.removeAll(); 77 mFooterInjectors.clear(); 78 Collection<FooterData> footerData = getFooterData(); 79 for (FooterData data : footerData) { 80 // Generate a footer preference with the given text 81 FooterPreference footerPreference = new FooterPreference(preference.getContext()); 82 String footerString; 83 try { 84 footerString = 85 mPackageManager 86 .getResourcesForApplication(data.applicationInfo) 87 .getString(data.footerStringRes); 88 } catch (NameNotFoundException exception) { 89 Log.w( 90 TAG, 91 "Resources not found for application " 92 + data.applicationInfo.packageName); 93 continue; 94 } 95 footerPreference.setTitle(footerString); 96 // Inject the footer 97 category.addPreference(footerPreference); 98 // Send broadcast to the injector announcing a footer has been injected 99 sendBroadcastFooterDisplayed(data.componentName); 100 mFooterInjectors.add(data.componentName); 101 } 102 } 103 104 /** 105 * Do nothing on location mode changes. 106 */ 107 @Override onLocationModeChanged(int mode, boolean restricted)108 public void onLocationModeChanged(int mode, boolean restricted) {} 109 110 /** 111 * Location footer preference group should be displayed if there is at least one footer to 112 * inject. 113 */ 114 @Override isAvailable()115 public boolean isAvailable() { 116 return !getFooterData().isEmpty(); 117 } 118 119 /** 120 * Send a {@link LocationManager#SETTINGS_FOOTER_REMOVED_ACTION} broadcast to footer injectors 121 * when LocationFragment is on pause 122 */ 123 @Override onPause()124 public void onPause() { 125 // Send broadcast to the footer injectors. Notify them the footer is not visible. 126 for (ComponentName componentName : mFooterInjectors) { 127 final Intent intent = new Intent(LocationManager.SETTINGS_FOOTER_REMOVED_ACTION); 128 intent.setComponent(componentName); 129 mContext.sendBroadcast(intent); 130 } 131 } 132 133 /** 134 * Send a {@link LocationManager#SETTINGS_FOOTER_DISPLAYED_ACTION} broadcast to a footer 135 * injector. 136 */ 137 @VisibleForTesting sendBroadcastFooterDisplayed(ComponentName componentName)138 void sendBroadcastFooterDisplayed(ComponentName componentName) { 139 Intent intent = new Intent(LocationManager.SETTINGS_FOOTER_DISPLAYED_ACTION); 140 intent.setComponent(componentName); 141 mContext.sendBroadcast(intent); 142 } 143 144 /** 145 * Return a list of strings with text provided by ACTION_INJECT_FOOTER broadcast receivers. 146 */ getFooterData()147 private Collection<FooterData> getFooterData() { 148 // Fetch footer text from system apps 149 final List<ResolveInfo> resolveInfos = 150 mPackageManager.queryBroadcastReceivers( 151 INJECT_INTENT, PackageManager.GET_META_DATA); 152 if (resolveInfos == null) { 153 Log.e(TAG, "Unable to resolve intent " + INJECT_INTENT); 154 return Collections.emptyList(); 155 } 156 157 if (Log.isLoggable(TAG, Log.DEBUG)) { 158 Log.d(TAG, "Found broadcast receivers: " + resolveInfos); 159 } 160 161 final Collection<FooterData> footerDataList = new ArrayList<>(resolveInfos.size()); 162 for (ResolveInfo resolveInfo : resolveInfos) { 163 final ActivityInfo activityInfo = resolveInfo.activityInfo; 164 final ApplicationInfo appInfo = activityInfo.applicationInfo; 165 166 // If a non-system app tries to inject footer, ignore it 167 if ((appInfo.flags & ApplicationInfo.FLAG_SYSTEM) == 0) { 168 Log.w(TAG, "Ignoring attempt to inject footer from app not in system image: " 169 + resolveInfo); 170 continue; 171 } 172 173 // Get the footer text resource id from broadcast receiver's metadata 174 if (activityInfo.metaData == null) { 175 if (Log.isLoggable(TAG, Log.DEBUG)) { 176 Log.d(TAG, "No METADATA in broadcast receiver " + activityInfo.name); 177 } 178 continue; 179 } 180 181 final int footerTextRes = 182 activityInfo.metaData.getInt(LocationManager.METADATA_SETTINGS_FOOTER_STRING); 183 if (footerTextRes == 0) { 184 Log.w( 185 TAG, 186 "No mapping of integer exists for " 187 + LocationManager.METADATA_SETTINGS_FOOTER_STRING); 188 continue; 189 } 190 footerDataList.add( 191 new FooterData( 192 footerTextRes, 193 appInfo, 194 new ComponentName(activityInfo.packageName, activityInfo.name))); 195 } 196 return footerDataList; 197 } 198 199 /** 200 * Contains information related to a footer. 201 */ 202 private static class FooterData { 203 204 // The string resource of the footer 205 final int footerStringRes; 206 207 // Application info of receiver injecting this footer 208 final ApplicationInfo applicationInfo; 209 210 // The component that injected the footer. It must be a receiver of broadcast 211 // LocationManager.SETTINGS_FOOTER_DISPLAYED_ACTION 212 final ComponentName componentName; 213 FooterData(int footerRes, ApplicationInfo appInfo, ComponentName componentName)214 FooterData(int footerRes, ApplicationInfo appInfo, ComponentName componentName) { 215 this.footerStringRes = footerRes; 216 this.applicationInfo = appInfo; 217 this.componentName = componentName; 218 } 219 } 220 } 221