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