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