1 /*
2  * Copyright (C) 2021 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.settings.location;
18 
19 import android.content.Context;
20 import android.content.Intent;
21 import android.content.pm.ActivityInfo;
22 import android.content.pm.ApplicationInfo;
23 import android.content.pm.PackageManager;
24 import android.content.pm.ResolveInfo;
25 import android.location.LocationManager;
26 import android.text.Html;
27 import android.text.TextUtils;
28 import android.util.Log;
29 
30 import androidx.preference.Preference;
31 import androidx.preference.PreferenceScreen;
32 
33 import com.android.settings.R;
34 import com.android.settingslib.HelpUtils;
35 import com.android.settingslib.widget.FooterPreference;
36 
37 import java.util.ArrayList;
38 import java.util.Collection;
39 import java.util.Collections;
40 import java.util.List;
41 
42 /**
43  * Preference controller for Location Settings footer.
44  */
45 public class LocationSettingsFooterPreferenceController extends LocationBasePreferenceController {
46     private static final String TAG = "LocationFooter";
47     private static final String PARAGRAPH_SEPARATOR = "<br><br>";
48     private static final Intent INJECT_INTENT =
49             new Intent(LocationManager.SETTINGS_FOOTER_DISPLAYED_ACTION);
50 
51     private final PackageManager mPackageManager;
52     private FooterPreference mFooterPreference;
53     private boolean mLocationEnabled;
54     private String mInjectedFooterString;
55 
LocationSettingsFooterPreferenceController(Context context, String key)56     public LocationSettingsFooterPreferenceController(Context context, String key) {
57         super(context, key);
58         mPackageManager = context.getPackageManager();
59     }
60 
61     @Override
displayPreference(PreferenceScreen screen)62     public void displayPreference(PreferenceScreen screen) {
63         super.displayPreference(screen);
64         mFooterPreference = screen.findPreference(getPreferenceKey());
65     }
66 
67     @Override
onLocationModeChanged(int mode, boolean restricted)68     public void onLocationModeChanged(int mode, boolean restricted) {
69         mLocationEnabled = mLocationEnabler.isEnabled(mode);
70         updateFooterPreference();
71     }
72 
73     /**
74      * Insert footer preferences.
75      */
76     @Override
updateState(Preference preference)77     public void updateState(Preference preference) {
78         Collection<FooterData> footerData = getFooterData();
79         for (FooterData data : footerData) {
80             try {
81                 mInjectedFooterString =
82                         mPackageManager
83                                 .getResourcesForApplication(data.applicationInfo)
84                                 .getString(data.footerStringRes);
85                 updateFooterPreference();
86             } catch (PackageManager.NameNotFoundException exception) {
87                 Log.w(
88                         TAG,
89                         "Resources not found for application "
90                                 + data.applicationInfo.packageName);
91             }
92         }
93     }
94 
updateFooterPreference()95     private void updateFooterPreference() {
96         String footerString = mContext.getString(R.string.location_settings_footer_general);
97         if (mLocationEnabled) {
98             if (!TextUtils.isEmpty(mInjectedFooterString)) {
99                 footerString = Html.escapeHtml(mInjectedFooterString) + PARAGRAPH_SEPARATOR
100                         + footerString;
101             }
102         } else {
103             footerString = mContext.getString(R.string.location_settings_footer_location_off)
104                     + PARAGRAPH_SEPARATOR
105                     + footerString;
106         }
107         if (mFooterPreference != null) {
108             mFooterPreference.setTitle(Html.fromHtml(footerString));
109             mFooterPreference.setLearnMoreAction(v -> openLocationLearnMoreLink());
110             mFooterPreference.setLearnMoreText(mContext.getString(
111                     R.string.location_settings_footer_learn_more_content_description));
112         }
113     }
114 
openLocationLearnMoreLink()115     private void openLocationLearnMoreLink() {
116         mFragment.startActivityForResult(
117                 HelpUtils.getHelpIntent(
118                         mContext,
119                         mContext.getString(R.string.location_settings_footer_learn_more_link),
120                         /*backupContext=*/""),
121                 /*requestCode=*/ 0);
122     }
123 
124     /**
125      * Location footer preference group should always be displayed.
126      */
127     @Override
getAvailabilityStatus()128     public int getAvailabilityStatus() {
129         return AVAILABLE;
130     }
131 
132     /**
133      * Return a list of strings with text provided by ACTION_INJECT_FOOTER broadcast receivers.
134      */
getFooterData()135     private List<FooterData> getFooterData() {
136         // Fetch footer text from system apps
137         List<ResolveInfo> resolveInfos =
138                 mPackageManager.queryBroadcastReceivers(
139                         INJECT_INTENT, PackageManager.GET_META_DATA);
140         if (resolveInfos == null) {
141             Log.e(TAG, "Unable to resolve intent " + INJECT_INTENT);
142             return Collections.emptyList();
143         }
144 
145         if (Log.isLoggable(TAG, Log.DEBUG)) {
146             Log.d(TAG, "Found broadcast receivers: " + resolveInfos);
147         }
148 
149         List<FooterData> footerDataList = new ArrayList<>(resolveInfos.size());
150         for (ResolveInfo resolveInfo : resolveInfos) {
151             ActivityInfo activityInfo = resolveInfo.activityInfo;
152             ApplicationInfo appInfo = activityInfo.applicationInfo;
153 
154             // If a non-system app tries to inject footer, ignore it
155             if ((appInfo.flags & ApplicationInfo.FLAG_SYSTEM) == 0) {
156                 Log.w(TAG, "Ignoring attempt to inject footer from app not in system image: "
157                         + resolveInfo);
158                 continue;
159             }
160 
161             // Get the footer text resource id from broadcast receiver's metadata
162             if (activityInfo.metaData == null) {
163                 if (Log.isLoggable(TAG, Log.DEBUG)) {
164                     Log.d(TAG, "No METADATA in broadcast receiver " + activityInfo.name);
165                 }
166                 continue;
167             }
168 
169             final int footerTextRes =
170                     activityInfo.metaData.getInt(LocationManager.METADATA_SETTINGS_FOOTER_STRING);
171             if (footerTextRes == 0) {
172                 Log.w(
173                         TAG,
174                         "No mapping of integer exists for "
175                                 + LocationManager.METADATA_SETTINGS_FOOTER_STRING);
176                 continue;
177             }
178             footerDataList.add(new FooterData(footerTextRes, appInfo));
179         }
180         return footerDataList;
181     }
182 
183     /**
184      * Contains information related to a footer.
185      */
186     private static class FooterData {
187 
188         // The string resource of the footer
189         public final int footerStringRes;
190 
191         // Application info of receiver injecting this footer
192         public final ApplicationInfo applicationInfo;
193 
FooterData(int footerRes, ApplicationInfo appInfo)194         FooterData(int footerRes, ApplicationInfo appInfo) {
195             this.footerStringRes = footerRes;
196             this.applicationInfo = appInfo;
197         }
198     }
199 }
200