1 /*
2  * Copyright (C) 2012 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.settingslib;
18 
19 import android.app.Activity;
20 import android.content.ActivityNotFoundException;
21 import android.content.ComponentName;
22 import android.content.Context;
23 import android.content.Intent;
24 import android.content.pm.PackageInfo;
25 import android.content.pm.PackageManager.NameNotFoundException;
26 import android.content.res.Resources;
27 import android.net.Uri;
28 import android.os.Build;
29 import android.provider.Settings.Global;
30 import android.text.TextUtils;
31 import android.util.Log;
32 import android.view.Menu;
33 import android.view.MenuItem;
34 import android.view.MenuItem.OnMenuItemClickListener;
35 
36 import androidx.annotation.RequiresApi;
37 import androidx.annotation.VisibleForTesting;
38 
39 import com.android.settingslib.widget.help.R;
40 
41 import java.net.URISyntaxException;
42 import java.util.Locale;
43 
44 /**
45  * Functions to easily prepare contextual help menu option items with an intent that opens up the
46  * browser to a particular URL, while taking into account the preferred language and app version.
47  */
48 public class HelpUtils {
49     private final static String TAG = HelpUtils.class.getSimpleName();
50 
51     @VisibleForTesting
52     static final int MENU_HELP = Menu.FIRST + 100;
53 
54     /**
55      * Help URL query parameter key for the preferred language.
56      */
57     private final static String PARAM_LANGUAGE_CODE = "hl";
58 
59     /**
60      * Help URL query parameter key for the app version.
61      */
62     private final static String PARAM_VERSION = "version";
63 
64     // Constants for help intents.
65     private static final String EXTRA_CONTEXT = "EXTRA_CONTEXT";
66     private static final String EXTRA_THEME = "EXTRA_THEME";
67     private static final String EXTRA_BACKUP_URI = "EXTRA_BACKUP_URI";
68 
69     /**
70      * Cached version code to prevent repeated calls to the package manager.
71      */
72     private static String sCachedVersionCode = null;
73 
74     /** Static helper that is not instantiable */
HelpUtils()75     private HelpUtils() {
76     }
77 
78     /**
79      * Prepares the help menu item by doing the following.
80      * - If the helpUrlString is empty or null, the help menu item is made invisible.
81      * - Otherwise, this makes the help menu item visible and sets the intent for the help menu
82      * item to view the URL.
83      *
84      * @return returns whether the help menu item has been made visible.
85      */
86     @RequiresApi(Build.VERSION_CODES.P)
prepareHelpMenuItem(Activity activity, Menu menu, String helpUri, String backupContext)87     public static boolean prepareHelpMenuItem(Activity activity, Menu menu, String helpUri,
88             String backupContext) {
89         // menu contains help item, skip it
90         if (menu.findItem(MENU_HELP) != null) {
91             return false;
92         }
93         MenuItem helpItem = menu.add(0, MENU_HELP, 0, R.string.help_feedback_label);
94         helpItem.setIcon(R.drawable.ic_help_actionbar);
95         return prepareHelpMenuItem(activity, helpItem, helpUri, backupContext);
96     }
97 
98     /**
99      * Prepares the help menu item by doing the following.
100      * - If the helpUrlString is empty or null, the help menu item is made invisible.
101      * - Otherwise, this makes the help menu item visible and sets the intent for the help menu
102      * item to view the URL.
103      *
104      * @return returns whether the help menu item has been made visible.
105      */
106     @RequiresApi(Build.VERSION_CODES.P)
prepareHelpMenuItem(Activity activity, Menu menu, int helpUriResource, String backupContext)107     public static boolean prepareHelpMenuItem(Activity activity, Menu menu, int helpUriResource,
108             String backupContext) {
109         // menu contains help item, skip it
110         if (menu.findItem(MENU_HELP) != null) {
111             return false;
112         }
113         MenuItem helpItem = menu.add(0, MENU_HELP, 0, R.string.help_feedback_label);
114         helpItem.setIcon(R.drawable.ic_help_actionbar);
115         return prepareHelpMenuItem(activity, helpItem, activity.getString(helpUriResource),
116                 backupContext);
117     }
118 
119     /**
120      * Prepares the help menu item by doing the following.
121      * - If the helpUrlString is empty or null, the help menu item is made invisible.
122      * - Otherwise, this makes the help menu item visible and sets the intent for the help menu
123      * item to view the URL.
124      *
125      * @return returns whether the help menu item has been made visible.
126      */
127     @VisibleForTesting
128     @RequiresApi(Build.VERSION_CODES.P)
prepareHelpMenuItem(final Activity activity, MenuItem helpMenuItem, String helpUriString, String backupContext)129     static boolean prepareHelpMenuItem(final Activity activity, MenuItem helpMenuItem,
130             String helpUriString, String backupContext) {
131         if (Global.getInt(activity.getContentResolver(), Global.DEVICE_PROVISIONED, 0) == 0) {
132             return false;
133         }
134         if (TextUtils.isEmpty(helpUriString)) {
135             // The help url string is empty or null, so set the help menu item to be invisible.
136             helpMenuItem.setVisible(false);
137 
138             // return that the help menu item is not visible (i.e. false)
139             return false;
140         } else {
141             final Intent intent = getHelpIntent(activity, helpUriString, backupContext);
142 
143             // Set the intent to the help menu item, show the help menu item in the overflow
144             // menu, and make it visible.
145             if (intent != null) {
146                 helpMenuItem.setOnMenuItemClickListener(new OnMenuItemClickListener() {
147                     @Override
148                     public boolean onMenuItemClick(MenuItem item) {
149                         /**
150                          * TODO: Enable metrics logger for @SystemApi (b/111552654)
151                          *
152                          MetricsLogger.action(activity,
153                          MetricsEvent.ACTION_SETTING_HELP_AND_FEEDBACK,
154                          intent.getStringExtra(EXTRA_CONTEXT));
155                          */
156                         try {
157                             activity.startActivityForResult(intent, 0);
158                         } catch (ActivityNotFoundException exc) {
159                             Log.e(TAG, "No activity found for intent: " + intent);
160                         }
161                         return true;
162                     }
163                 });
164                 helpMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
165                 helpMenuItem.setVisible(true);
166             } else {
167                 helpMenuItem.setVisible(false);
168                 return false;
169             }
170 
171             // return that the help menu item is visible (i.e., true)
172             return true;
173         }
174     }
175 
176     /**
177      * Get the help intent from helpUriString.
178      */
179     @RequiresApi(Build.VERSION_CODES.P)
getHelpIntent(Context context, String helpUriString, String backupContext)180     public static Intent getHelpIntent(Context context, String helpUriString,
181             String backupContext) {
182         if (Global.getInt(context.getContentResolver(), Global.DEVICE_PROVISIONED, 0) == 0) {
183             return null;
184         }
185         // Try to handle as Intent Uri, otherwise just treat as Uri.
186         try {
187             Intent intent = Intent.parseUri(helpUriString,
188                     Intent.URI_ANDROID_APP_SCHEME | Intent.URI_INTENT_SCHEME);
189             addIntentParameters(context, intent, backupContext, true /* sendPackageName */);
190             ComponentName component = intent.resolveActivity(context.getPackageManager());
191             if (component != null) {
192                 return intent;
193             } else if (intent.hasExtra(EXTRA_BACKUP_URI)) {
194                 // This extra contains a backup URI for when the intent isn't available.
195                 return getHelpIntent(context, intent.getStringExtra(EXTRA_BACKUP_URI),
196                         backupContext);
197             } else {
198                 return null;
199             }
200         } catch (URISyntaxException e) {
201         }
202         // The help url string exists, so first add in some extra query parameters.
203         final Uri fullUri = uriWithAddedParameters(context, Uri.parse(helpUriString));
204 
205         // Then, create an intent that will be fired when the user
206         // selects this help menu item.
207         Intent intent = new Intent(Intent.ACTION_VIEW, fullUri);
208         intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
209                 | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
210         return intent;
211     }
212 
addIntentParameters(Context context, Intent intent, String backupContext, boolean sendPackageName)213     public static void addIntentParameters(Context context, Intent intent, String backupContext,
214             boolean sendPackageName) {
215         if (!intent.hasExtra(EXTRA_CONTEXT)) {
216             // Insert some context if none exists.
217             intent.putExtra(EXTRA_CONTEXT, backupContext);
218         }
219 
220         Resources resources = context.getResources();
221         boolean includePackageName =
222                 resources.getBoolean(android.R.bool.config_sendPackageName);
223 
224         if (sendPackageName && includePackageName) {
225             String[] packageNameKey =
226                     {resources.getString(android.R.string.config_helpPackageNameKey)};
227             String[] packageNameValue =
228                     {resources.getString(android.R.string.config_helpPackageNameValue)};
229             String helpIntentExtraKey =
230                     resources.getString(android.R.string.config_helpIntentExtraKey);
231             String helpIntentNameKey =
232                     resources.getString(android.R.string.config_helpIntentNameKey);
233             String feedbackIntentExtraKey =
234                     resources.getString(android.R.string.config_feedbackIntentExtraKey);
235             String feedbackIntentNameKey =
236                     resources.getString(android.R.string.config_feedbackIntentNameKey);
237             intent.putExtra(helpIntentExtraKey, packageNameKey);
238             intent.putExtra(helpIntentNameKey, packageNameValue);
239             intent.putExtra(feedbackIntentExtraKey, packageNameKey);
240             intent.putExtra(feedbackIntentNameKey, packageNameValue);
241         }
242         intent.putExtra(EXTRA_THEME, 3 /* System Default theme */);
243     }
244 
245     /**
246      * Adds two query parameters into the Uri, namely the language code and the version code
247      * of the app's package as gotten via the context.
248      *
249      * @return the uri with added query parameters
250      */
251     @RequiresApi(Build.VERSION_CODES.P)
uriWithAddedParameters(Context context, Uri baseUri)252     public static Uri uriWithAddedParameters(Context context, Uri baseUri) {
253         Uri.Builder builder = baseUri.buildUpon();
254 
255         // Add in the preferred language
256         builder.appendQueryParameter(PARAM_LANGUAGE_CODE, Locale.getDefault().toString());
257 
258         // Add in the package version code
259         if (sCachedVersionCode == null) {
260             // There is no cached version code, so try to get it from the package manager.
261             try {
262                 // cache the version code
263                 PackageInfo info = context.getPackageManager().getPackageInfo(
264                         context.getPackageName(), 0);
265                 sCachedVersionCode = Long.toString(info.getLongVersionCode());
266 
267                 // append the version code to the uri
268                 builder.appendQueryParameter(PARAM_VERSION, sCachedVersionCode);
269             } catch (NameNotFoundException e) {
270                 // Cannot find the package name, so don't add in the version parameter
271                 // This shouldn't happen.
272                 Log.wtf(TAG, "Invalid package name for context", e);
273             }
274         } else {
275             builder.appendQueryParameter(PARAM_VERSION, sCachedVersionCode);
276         }
277 
278         // Build the full uri and return it
279         return builder.build();
280     }
281 }
282