• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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