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