1 /* 2 * Copyright (C) 2018 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.car.settings.location; 18 19 import android.car.drivingstate.CarUxRestrictions; 20 import android.content.ComponentName; 21 import android.content.Context; 22 import android.content.Intent; 23 import android.content.pm.ActivityInfo; 24 import android.content.pm.ApplicationInfo; 25 import android.content.pm.PackageManager; 26 import android.content.pm.ResolveInfo; 27 import android.location.LocationManager; 28 29 import androidx.annotation.StringRes; 30 import androidx.annotation.VisibleForTesting; 31 import androidx.preference.PreferenceGroup; 32 33 import com.android.car.settings.R; 34 import com.android.car.settings.common.FragmentController; 35 import com.android.car.settings.common.Logger; 36 import com.android.car.settings.common.PreferenceController; 37 import com.android.car.ui.preference.CarUiPreference; 38 39 import java.util.ArrayList; 40 import java.util.Collections; 41 import java.util.List; 42 43 /** 44 * Injects Location Footers into a {@link PreferenceGroup} with a matching key. 45 */ 46 public class LocationFooterPreferenceController extends PreferenceController<PreferenceGroup> { 47 private static final Logger LOG = new Logger(LocationFooterPreferenceController.class); 48 private static final Intent INJECT_INTENT = 49 new Intent(LocationManager.SETTINGS_FOOTER_DISPLAYED_ACTION); 50 51 private List<LocationFooter> mLocationFooters; 52 // List of Location Footer Injectors that will be used to broadcast a 53 // LocationManager.SETTINGS_FOOTER_REMOVED_ACTION intent on controller stop. 54 private final List<ComponentName> mFooterInjectors = new ArrayList<>(); 55 private PackageManager mPackageManager; 56 LocationFooterPreferenceController(Context context, String preferenceKey, FragmentController fragmentController, CarUxRestrictions uxRestrictions)57 public LocationFooterPreferenceController(Context context, String preferenceKey, 58 FragmentController fragmentController, CarUxRestrictions uxRestrictions) { 59 super(context, preferenceKey, fragmentController, uxRestrictions); 60 mPackageManager = context.getPackageManager(); 61 } 62 63 @VisibleForTesting setPackageManager(PackageManager packageManager)64 void setPackageManager(PackageManager packageManager) { 65 mPackageManager = packageManager; 66 } 67 68 @Override getPreferenceType()69 protected Class<PreferenceGroup> getPreferenceType() { 70 return PreferenceGroup.class; 71 } 72 73 @Override onCreateInternal()74 protected void onCreateInternal() { 75 mLocationFooters = getInjectedLocationFooters(); 76 for (LocationFooter footer : mLocationFooters) { 77 String footerString; 78 try { 79 footerString = mPackageManager 80 .getResourcesForApplication(footer.mApplicationInfo) 81 .getString(footer.mFooterStringRes); 82 } catch (PackageManager.NameNotFoundException exception) { 83 LOG.w("Resources not found for application " 84 + footer.mApplicationInfo.packageName); 85 continue; 86 } 87 88 // For each injected footer: Create a new preference, set the summary 89 // and icon, then inject under the footer preference group. 90 CarUiPreference newPreference = new CarUiPreference(getContext()); 91 newPreference.setSummary(footerString); 92 newPreference.setIcon(R.drawable.ic_settings_about); 93 newPreference.setSelectable(false); 94 getPreference().addPreference(newPreference); 95 96 // Send broadcast to the injector announcing a footer has been injected 97 sendBroadcast(footer.mComponentName, 98 LocationManager.SETTINGS_FOOTER_DISPLAYED_ACTION); 99 // Add the component to the list of injectors so that 100 // it receives a broadcast when the footer is removed. 101 mFooterInjectors.add(footer.mComponentName); 102 } 103 } 104 105 /** 106 * Send a {@link LocationManager#SETTINGS_FOOTER_REMOVED_ACTION} broadcast to footer injectors 107 * when LocationSettingsFragment is stopped. 108 */ 109 @Override onStopInternal()110 protected void onStopInternal() { 111 // Send broadcast to the footer injectors. Notify them the footer is not visible. 112 for (ComponentName componentName : mFooterInjectors) { 113 sendBroadcast(componentName, LocationManager.SETTINGS_FOOTER_REMOVED_ACTION); 114 } 115 } 116 117 @Override onDestroyInternal()118 protected void onDestroyInternal() { 119 mLocationFooters = null; 120 } 121 122 @Override updateState(PreferenceGroup preferenceGroup)123 protected void updateState(PreferenceGroup preferenceGroup) { 124 preferenceGroup.setVisible(preferenceGroup.getPreferenceCount() > 0); 125 } 126 127 /** 128 * Return a list of strings provided by ACTION_INJECT_FOOTER broadcast receivers. If there 129 * are no injectors, an immutable emptry list is returned. 130 */ getInjectedLocationFooters()131 private List<LocationFooter> getInjectedLocationFooters() { 132 List<ResolveInfo> resolveInfos = mPackageManager.queryBroadcastReceivers( 133 INJECT_INTENT, PackageManager.GET_META_DATA); 134 if (resolveInfos == null) { 135 LOG.e("Unable to resolve intent " + INJECT_INTENT); 136 return Collections.emptyList(); 137 } else { 138 LOG.d("Found broadcast receivers: " + resolveInfos); 139 } 140 141 List<LocationFooter> locationFooters = new ArrayList<>(resolveInfos.size()); 142 for (ResolveInfo resolveInfo : resolveInfos) { 143 ActivityInfo activityInfo = resolveInfo.activityInfo; 144 ApplicationInfo appInfo = activityInfo.applicationInfo; 145 146 // If a non-system app tries to inject footer, ignore it 147 if ((appInfo.flags & ApplicationInfo.FLAG_SYSTEM) == 0) { 148 LOG.w("Ignoring attempt to inject footer from a non-system app: " + resolveInfo); 149 continue; 150 } 151 152 // If the injector does not have valid METADATA, ignore it 153 if (activityInfo.metaData == null) { 154 LOG.d("No METADATA in broadcast receiver " + activityInfo.name); 155 continue; 156 } 157 158 // Get the footer text resource id from broadcast receiver's metadata 159 int footerTextRes = 160 activityInfo.metaData.getInt(LocationManager.METADATA_SETTINGS_FOOTER_STRING); 161 if (footerTextRes == 0) { 162 LOG.w("No mapping of integer exists for " 163 + LocationManager.METADATA_SETTINGS_FOOTER_STRING); 164 continue; 165 } 166 locationFooters.add(new LocationFooter(footerTextRes, appInfo, 167 new ComponentName(activityInfo.packageName, activityInfo.name))); 168 } 169 return locationFooters; 170 } 171 sendBroadcast(ComponentName componentName, String action)172 private void sendBroadcast(ComponentName componentName, String action) { 173 Intent intent = new Intent(action); 174 intent.setComponent(componentName); 175 getContext().sendBroadcast(intent); 176 } 177 178 /** 179 * Contains information related to a footer. 180 */ 181 private static class LocationFooter { 182 // The string resource of the footer. 183 @StringRes 184 private final int mFooterStringRes; 185 // Application info of the receiver injecting this footer. 186 private final ApplicationInfo mApplicationInfo; 187 // The component that injected the footer. It must be a receiver of 188 // LocationManager.SETTINGS_FOOTER_DISPLAYED_ACTION broadcast. 189 private final ComponentName mComponentName; 190 LocationFooter(@tringRes int footerRes, ApplicationInfo appInfo, ComponentName componentName)191 LocationFooter(@StringRes int footerRes, ApplicationInfo appInfo, 192 ComponentName componentName) { 193 mFooterStringRes = footerRes; 194 mApplicationInfo = appInfo; 195 mComponentName = componentName; 196 } 197 } 198 } 199