Home | History | Annotate | Download | only in settings
      1 /**
      2  * Copyright (C) 2007 Google Inc.
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not
      5  * use this file except in compliance with the License. You may obtain a copy
      6  * 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, WITHOUT
     12  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
     13  * License for the specific language governing permissions and limitations
     14  * under the License.
     15  */
     16 
     17 package com.android.settings;
     18 
     19 import android.app.Activity;
     20 import android.app.ActivityManager;
     21 import android.app.AlertDialog;
     22 import android.app.Dialog;
     23 import android.content.ContentResolver;
     24 import android.content.Context;
     25 import android.content.DialogInterface;
     26 import android.content.Intent;
     27 import android.content.pm.ApplicationInfo;
     28 import android.content.pm.PackageManager;
     29 import android.content.pm.PackageManager.NameNotFoundException;
     30 import android.content.pm.ResolveInfo;
     31 import android.content.pm.UserInfo;
     32 import android.content.res.Resources;
     33 import android.content.res.Resources.NotFoundException;
     34 import android.database.Cursor;
     35 import android.graphics.Bitmap;
     36 import android.graphics.BitmapFactory;
     37 import android.graphics.drawable.Drawable;
     38 import android.net.ConnectivityManager;
     39 import android.net.LinkProperties;
     40 import android.net.Uri;
     41 import android.os.BatteryManager;
     42 import android.os.Bundle;
     43 import android.os.ParcelFileDescriptor;
     44 import android.os.UserHandle;
     45 import android.os.UserManager;
     46 import android.preference.Preference;
     47 import android.preference.PreferenceActivity.Header;
     48 import android.preference.PreferenceFrameLayout;
     49 import android.preference.PreferenceGroup;
     50 import android.provider.ContactsContract.CommonDataKinds;
     51 import android.provider.ContactsContract.CommonDataKinds.Phone;
     52 import android.provider.ContactsContract.CommonDataKinds.StructuredName;
     53 import android.provider.ContactsContract.Contacts;
     54 import android.provider.ContactsContract.Data;
     55 import android.provider.ContactsContract.Profile;
     56 import android.provider.ContactsContract.RawContacts;
     57 import android.telephony.TelephonyManager;
     58 import android.text.TextUtils;
     59 import android.util.Log;
     60 import android.view.View;
     61 import android.view.ViewGroup;
     62 import android.widget.ListView;
     63 import android.widget.TabWidget;
     64 
     65 import com.android.settings.users.ProfileUpdateReceiver;
     66 
     67 import java.io.FileOutputStream;
     68 import java.io.IOException;
     69 import java.io.InputStream;
     70 import java.net.InetAddress;
     71 import java.util.Iterator;
     72 import java.util.List;
     73 import java.util.Locale;
     74 
     75 public class Utils {
     76 
     77     /**
     78      * Set the preference's title to the matching activity's label.
     79      */
     80     public static final int UPDATE_PREFERENCE_FLAG_SET_TITLE_TO_MATCHING_ACTIVITY = 1;
     81 
     82     /**
     83      * The opacity level of a disabled icon.
     84      */
     85     public static final float DISABLED_ALPHA = 0.4f;
     86 
     87     /**
     88      * Name of the meta-data item that should be set in the AndroidManifest.xml
     89      * to specify the icon that should be displayed for the preference.
     90      */
     91     private static final String META_DATA_PREFERENCE_ICON = "com.android.settings.icon";
     92 
     93     /**
     94      * Name of the meta-data item that should be set in the AndroidManifest.xml
     95      * to specify the title that should be displayed for the preference.
     96      */
     97     private static final String META_DATA_PREFERENCE_TITLE = "com.android.settings.title";
     98 
     99     /**
    100      * Name of the meta-data item that should be set in the AndroidManifest.xml
    101      * to specify the summary text that should be displayed for the preference.
    102      */
    103     private static final String META_DATA_PREFERENCE_SUMMARY = "com.android.settings.summary";
    104 
    105     /**
    106      * Finds a matching activity for a preference's intent. If a matching
    107      * activity is not found, it will remove the preference.
    108      *
    109      * @param context The context.
    110      * @param parentPreferenceGroup The preference group that contains the
    111      *            preference whose intent is being resolved.
    112      * @param preferenceKey The key of the preference whose intent is being
    113      *            resolved.
    114      * @param flags 0 or one or more of
    115      *            {@link #UPDATE_PREFERENCE_FLAG_SET_TITLE_TO_MATCHING_ACTIVITY}
    116      *            .
    117      * @return Whether an activity was found. If false, the preference was
    118      *         removed.
    119      */
    120     public static boolean updatePreferenceToSpecificActivityOrRemove(Context context,
    121             PreferenceGroup parentPreferenceGroup, String preferenceKey, int flags) {
    122 
    123         Preference preference = parentPreferenceGroup.findPreference(preferenceKey);
    124         if (preference == null) {
    125             return false;
    126         }
    127 
    128         Intent intent = preference.getIntent();
    129         if (intent != null) {
    130             // Find the activity that is in the system image
    131             PackageManager pm = context.getPackageManager();
    132             List<ResolveInfo> list = pm.queryIntentActivities(intent, 0);
    133             int listSize = list.size();
    134             for (int i = 0; i < listSize; i++) {
    135                 ResolveInfo resolveInfo = list.get(i);
    136                 if ((resolveInfo.activityInfo.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM)
    137                         != 0) {
    138 
    139                     // Replace the intent with this specific activity
    140                     preference.setIntent(new Intent().setClassName(
    141                             resolveInfo.activityInfo.packageName,
    142                             resolveInfo.activityInfo.name));
    143 
    144                     if ((flags & UPDATE_PREFERENCE_FLAG_SET_TITLE_TO_MATCHING_ACTIVITY) != 0) {
    145                         // Set the preference title to the activity's label
    146                         preference.setTitle(resolveInfo.loadLabel(pm));
    147                     }
    148 
    149                     return true;
    150                 }
    151             }
    152         }
    153 
    154         // Did not find a matching activity, so remove the preference
    155         parentPreferenceGroup.removePreference(preference);
    156 
    157         return false;
    158     }
    159 
    160     /**
    161      * Finds a matching activity for a preference's intent. If a matching
    162      * activity is not found, it will remove the preference. The icon, title and
    163      * summary of the preference will also be updated with the values retrieved
    164      * from the activity's meta-data elements. If no meta-data elements are
    165      * specified then the preference title will be set to match the label of the
    166      * activity, an icon and summary text will not be displayed.
    167      *
    168      * @param context The context.
    169      * @param parentPreferenceGroup The preference group that contains the
    170      *            preference whose intent is being resolved.
    171      * @param preferenceKey The key of the preference whose intent is being
    172      *            resolved.
    173      *
    174      * @return Whether an activity was found. If false, the preference was
    175      *         removed.
    176      *
    177      * @see {@link #META_DATA_PREFERENCE_ICON}
    178      *      {@link #META_DATA_PREFERENCE_TITLE}
    179      *      {@link #META_DATA_PREFERENCE_SUMMARY}
    180      */
    181     public static boolean updatePreferenceToSpecificActivityFromMetaDataOrRemove(Context context,
    182             PreferenceGroup parentPreferenceGroup, String preferenceKey) {
    183 
    184         IconPreferenceScreen preference = (IconPreferenceScreen)parentPreferenceGroup
    185                 .findPreference(preferenceKey);
    186         if (preference == null) {
    187             return false;
    188         }
    189 
    190         Intent intent = preference.getIntent();
    191         if (intent != null) {
    192             // Find the activity that is in the system image
    193             PackageManager pm = context.getPackageManager();
    194             List<ResolveInfo> list = pm.queryIntentActivities(intent, PackageManager.GET_META_DATA);
    195             int listSize = list.size();
    196             for (int i = 0; i < listSize; i++) {
    197                 ResolveInfo resolveInfo = list.get(i);
    198                 if ((resolveInfo.activityInfo.applicationInfo.flags
    199                         & ApplicationInfo.FLAG_SYSTEM) != 0) {
    200                     Drawable icon = null;
    201                     String title = null;
    202                     String summary = null;
    203 
    204                     // Get the activity's meta-data
    205                     try {
    206                         Resources res = pm
    207                                 .getResourcesForApplication(resolveInfo.activityInfo.packageName);
    208                         Bundle metaData = resolveInfo.activityInfo.metaData;
    209 
    210                         if (res != null && metaData != null) {
    211                             icon = res.getDrawable(metaData.getInt(META_DATA_PREFERENCE_ICON));
    212                             title = res.getString(metaData.getInt(META_DATA_PREFERENCE_TITLE));
    213                             summary = res.getString(metaData.getInt(META_DATA_PREFERENCE_SUMMARY));
    214                         }
    215                     } catch (NameNotFoundException e) {
    216                         // Ignore
    217                     } catch (NotFoundException e) {
    218                         // Ignore
    219                     }
    220 
    221                     // Set the preference title to the activity's label if no
    222                     // meta-data is found
    223                     if (TextUtils.isEmpty(title)) {
    224                         title = resolveInfo.loadLabel(pm).toString();
    225                     }
    226 
    227                     // Set icon, title and summary for the preference
    228                     preference.setIcon(icon);
    229                     preference.setTitle(title);
    230                     preference.setSummary(summary);
    231 
    232                     // Replace the intent with this specific activity
    233                     preference.setIntent(new Intent().setClassName(
    234                             resolveInfo.activityInfo.packageName,
    235                             resolveInfo.activityInfo.name));
    236 
    237                    return true;
    238                 }
    239             }
    240         }
    241 
    242         // Did not find a matching activity, so remove the preference
    243         parentPreferenceGroup.removePreference(preference);
    244 
    245         return false;
    246     }
    247 
    248     public static boolean updateHeaderToSpecificActivityFromMetaDataOrRemove(Context context,
    249             List<Header> target, Header header) {
    250 
    251         Intent intent = header.intent;
    252         if (intent != null) {
    253             // Find the activity that is in the system image
    254             PackageManager pm = context.getPackageManager();
    255             List<ResolveInfo> list = pm.queryIntentActivities(intent, PackageManager.GET_META_DATA);
    256             int listSize = list.size();
    257             for (int i = 0; i < listSize; i++) {
    258                 ResolveInfo resolveInfo = list.get(i);
    259                 if ((resolveInfo.activityInfo.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM)
    260                         != 0) {
    261                     Drawable icon = null;
    262                     String title = null;
    263                     String summary = null;
    264 
    265                     // Get the activity's meta-data
    266                     try {
    267                         Resources res = pm.getResourcesForApplication(
    268                                 resolveInfo.activityInfo.packageName);
    269                         Bundle metaData = resolveInfo.activityInfo.metaData;
    270 
    271                         if (res != null && metaData != null) {
    272                             icon = res.getDrawable(metaData.getInt(META_DATA_PREFERENCE_ICON));
    273                             title = res.getString(metaData.getInt(META_DATA_PREFERENCE_TITLE));
    274                             summary = res.getString(metaData.getInt(META_DATA_PREFERENCE_SUMMARY));
    275                         }
    276                     } catch (NameNotFoundException e) {
    277                         // Ignore
    278                     } catch (NotFoundException e) {
    279                         // Ignore
    280                     }
    281 
    282                     // Set the preference title to the activity's label if no
    283                     // meta-data is found
    284                     if (TextUtils.isEmpty(title)) {
    285                         title = resolveInfo.loadLabel(pm).toString();
    286                     }
    287 
    288                     // Set icon, title and summary for the preference
    289                     // TODO:
    290                     //header.icon = icon;
    291                     header.title = title;
    292                     header.summary = summary;
    293                     // Replace the intent with this specific activity
    294                     header.intent = new Intent().setClassName(resolveInfo.activityInfo.packageName,
    295                             resolveInfo.activityInfo.name);
    296 
    297                     return true;
    298                 }
    299             }
    300         }
    301 
    302         // Did not find a matching activity, so remove the preference
    303         target.remove(header);
    304 
    305         return false;
    306     }
    307 
    308     /**
    309      * Returns true if Monkey is running.
    310      */
    311     public static boolean isMonkeyRunning() {
    312         return ActivityManager.isUserAMonkey();
    313     }
    314 
    315     /**
    316      * Returns whether the device is voice-capable (meaning, it is also a phone).
    317      */
    318     public static boolean isVoiceCapable(Context context) {
    319         TelephonyManager telephony =
    320                 (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
    321         return telephony != null && telephony.isVoiceCapable();
    322     }
    323 
    324     public static boolean isWifiOnly(Context context) {
    325         ConnectivityManager cm = (ConnectivityManager)context.getSystemService(
    326                 Context.CONNECTIVITY_SERVICE);
    327         return (cm.isNetworkSupported(ConnectivityManager.TYPE_MOBILE) == false);
    328     }
    329 
    330     /**
    331      * Returns the WIFI IP Addresses, if any, taking into account IPv4 and IPv6 style addresses.
    332      * @param context the application context
    333      * @return the formatted and newline-separated IP addresses, or null if none.
    334      */
    335     public static String getWifiIpAddresses(Context context) {
    336         ConnectivityManager cm = (ConnectivityManager)
    337                 context.getSystemService(Context.CONNECTIVITY_SERVICE);
    338         LinkProperties prop = cm.getLinkProperties(ConnectivityManager.TYPE_WIFI);
    339         return formatIpAddresses(prop);
    340     }
    341 
    342     /**
    343      * Returns the default link's IP addresses, if any, taking into account IPv4 and IPv6 style
    344      * addresses.
    345      * @param context the application context
    346      * @return the formatted and newline-separated IP addresses, or null if none.
    347      */
    348     public static String getDefaultIpAddresses(Context context) {
    349         ConnectivityManager cm = (ConnectivityManager)
    350                 context.getSystemService(Context.CONNECTIVITY_SERVICE);
    351         LinkProperties prop = cm.getActiveLinkProperties();
    352         return formatIpAddresses(prop);
    353     }
    354 
    355     private static String formatIpAddresses(LinkProperties prop) {
    356         if (prop == null) return null;
    357         Iterator<InetAddress> iter = prop.getAllAddresses().iterator();
    358         // If there are no entries, return null
    359         if (!iter.hasNext()) return null;
    360         // Concatenate all available addresses, comma separated
    361         String addresses = "";
    362         while (iter.hasNext()) {
    363             addresses += iter.next().getHostAddress();
    364             if (iter.hasNext()) addresses += "\n";
    365         }
    366         return addresses;
    367     }
    368 
    369     public static Locale createLocaleFromString(String localeStr) {
    370         // TODO: is there a better way to actually construct a locale that will match?
    371         // The main problem is, on top of Java specs, locale.toString() and
    372         // new Locale(locale.toString()).toString() do not return equal() strings in
    373         // many cases, because the constructor takes the only string as the language
    374         // code. So : new Locale("en", "US").toString() => "en_US"
    375         // And : new Locale("en_US").toString() => "en_us"
    376         if (null == localeStr)
    377             return Locale.getDefault();
    378         String[] brokenDownLocale = localeStr.split("_", 3);
    379         // split may not return a 0-length array.
    380         if (1 == brokenDownLocale.length) {
    381             return new Locale(brokenDownLocale[0]);
    382         } else if (2 == brokenDownLocale.length) {
    383             return new Locale(brokenDownLocale[0], brokenDownLocale[1]);
    384         } else {
    385             return new Locale(brokenDownLocale[0], brokenDownLocale[1], brokenDownLocale[2]);
    386         }
    387     }
    388 
    389     public static boolean isBatteryPresent(Intent batteryChangedIntent) {
    390         return batteryChangedIntent.getBooleanExtra(BatteryManager.EXTRA_PRESENT, true);
    391     }
    392 
    393     public static String getBatteryPercentage(Intent batteryChangedIntent) {
    394         int level = batteryChangedIntent.getIntExtra(BatteryManager.EXTRA_LEVEL, 0);
    395         int scale = batteryChangedIntent.getIntExtra(BatteryManager.EXTRA_SCALE, 100);
    396         return String.valueOf(level * 100 / scale) + "%";
    397     }
    398 
    399     public static String getBatteryStatus(Resources res, Intent batteryChangedIntent) {
    400         final Intent intent = batteryChangedIntent;
    401 
    402         int plugType = intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0);
    403         int status = intent.getIntExtra(BatteryManager.EXTRA_STATUS,
    404                 BatteryManager.BATTERY_STATUS_UNKNOWN);
    405         String statusString;
    406         if (status == BatteryManager.BATTERY_STATUS_CHARGING) {
    407             statusString = res.getString(R.string.battery_info_status_charging);
    408             if (plugType > 0) {
    409                 int resId;
    410                 if (plugType == BatteryManager.BATTERY_PLUGGED_AC) {
    411                     resId = R.string.battery_info_status_charging_ac;
    412                 } else if (plugType == BatteryManager.BATTERY_PLUGGED_USB) {
    413                     resId = R.string.battery_info_status_charging_usb;
    414                 } else {
    415                     resId = R.string.battery_info_status_charging_wireless;
    416                 }
    417                 statusString = statusString + " " + res.getString(resId);
    418             }
    419         } else if (status == BatteryManager.BATTERY_STATUS_DISCHARGING) {
    420             statusString = res.getString(R.string.battery_info_status_discharging);
    421         } else if (status == BatteryManager.BATTERY_STATUS_NOT_CHARGING) {
    422             statusString = res.getString(R.string.battery_info_status_not_charging);
    423         } else if (status == BatteryManager.BATTERY_STATUS_FULL) {
    424             statusString = res.getString(R.string.battery_info_status_full);
    425         } else {
    426             statusString = res.getString(R.string.battery_info_status_unknown);
    427         }
    428 
    429         return statusString;
    430     }
    431 
    432     public static void forcePrepareCustomPreferencesList(
    433             ViewGroup parent, View child, ListView list, boolean ignoreSidePadding) {
    434         list.setScrollBarStyle(View.SCROLLBARS_OUTSIDE_OVERLAY);
    435         list.setClipToPadding(false);
    436         prepareCustomPreferencesList(parent, child, list, ignoreSidePadding);
    437     }
    438 
    439     /**
    440      * Prepare a custom preferences layout, moving padding to {@link ListView}
    441      * when outside scrollbars are requested. Usually used to display
    442      * {@link ListView} and {@link TabWidget} with correct padding.
    443      */
    444     public static void prepareCustomPreferencesList(
    445             ViewGroup parent, View child, View list, boolean ignoreSidePadding) {
    446         final boolean movePadding = list.getScrollBarStyle() == View.SCROLLBARS_OUTSIDE_OVERLAY;
    447         if (movePadding && parent instanceof PreferenceFrameLayout) {
    448             ((PreferenceFrameLayout.LayoutParams) child.getLayoutParams()).removeBorders = true;
    449 
    450             final Resources res = list.getResources();
    451             final int paddingSide = res.getDimensionPixelSize(R.dimen.settings_side_margin);
    452             final int paddingBottom = res.getDimensionPixelSize(
    453                     com.android.internal.R.dimen.preference_fragment_padding_bottom);
    454 
    455             final int effectivePaddingSide = ignoreSidePadding ? 0 : paddingSide;
    456             list.setPaddingRelative(effectivePaddingSide, 0, effectivePaddingSide, paddingBottom);
    457         }
    458     }
    459 
    460     /**
    461      * Return string resource that best describes combination of tethering
    462      * options available on this device.
    463      */
    464     public static int getTetheringLabel(ConnectivityManager cm) {
    465         String[] usbRegexs = cm.getTetherableUsbRegexs();
    466         String[] wifiRegexs = cm.getTetherableWifiRegexs();
    467         String[] bluetoothRegexs = cm.getTetherableBluetoothRegexs();
    468 
    469         boolean usbAvailable = usbRegexs.length != 0;
    470         boolean wifiAvailable = wifiRegexs.length != 0;
    471         boolean bluetoothAvailable = bluetoothRegexs.length != 0;
    472 
    473         if (wifiAvailable && usbAvailable && bluetoothAvailable) {
    474             return R.string.tether_settings_title_all;
    475         } else if (wifiAvailable && usbAvailable) {
    476             return R.string.tether_settings_title_all;
    477         } else if (wifiAvailable && bluetoothAvailable) {
    478             return R.string.tether_settings_title_all;
    479         } else if (wifiAvailable) {
    480             return R.string.tether_settings_title_wifi;
    481         } else if (usbAvailable && bluetoothAvailable) {
    482             return R.string.tether_settings_title_usb_bluetooth;
    483         } else if (usbAvailable) {
    484             return R.string.tether_settings_title_usb;
    485         } else {
    486             return R.string.tether_settings_title_bluetooth;
    487         }
    488     }
    489 
    490     /* Used by UserSettings as well. Call this on a non-ui thread. */
    491     public static boolean copyMeProfilePhoto(Context context, UserInfo user) {
    492         Uri contactUri = Profile.CONTENT_URI;
    493 
    494         InputStream avatarDataStream = Contacts.openContactPhotoInputStream(
    495                     context.getContentResolver(),
    496                     contactUri, true);
    497         // If there's no profile photo, assign a default avatar
    498         if (avatarDataStream == null) {
    499             return false;
    500         }
    501         int userId = user != null ? user.id : UserHandle.myUserId();
    502         UserManager um = (UserManager) context.getSystemService(Context.USER_SERVICE);
    503         Bitmap icon = BitmapFactory.decodeStream(avatarDataStream);
    504         um.setUserIcon(userId, icon);
    505         try {
    506             avatarDataStream.close();
    507         } catch (IOException ioe) { }
    508         return true;
    509     }
    510 
    511     public static String getMeProfileName(Context context, boolean full) {
    512         if (full) {
    513             return getProfileDisplayName(context);
    514         } else {
    515             return getShorterNameIfPossible(context);
    516         }
    517     }
    518 
    519     private static String getShorterNameIfPossible(Context context) {
    520         final String given = getLocalProfileGivenName(context);
    521         return !TextUtils.isEmpty(given) ? given : getProfileDisplayName(context);
    522     }
    523 
    524     private static String getLocalProfileGivenName(Context context) {
    525         final ContentResolver cr = context.getContentResolver();
    526 
    527         // Find the raw contact ID for the local ME profile raw contact.
    528         final long localRowProfileId;
    529         final Cursor localRawProfile = cr.query(
    530                 Profile.CONTENT_RAW_CONTACTS_URI,
    531                 new String[] {RawContacts._ID},
    532                 RawContacts.ACCOUNT_TYPE + " IS NULL AND " +
    533                         RawContacts.ACCOUNT_NAME + " IS NULL",
    534                 null, null);
    535         if (localRawProfile == null) return null;
    536 
    537         try {
    538             if (!localRawProfile.moveToFirst()) {
    539                 return null;
    540             }
    541             localRowProfileId = localRawProfile.getLong(0);
    542         } finally {
    543             localRawProfile.close();
    544         }
    545 
    546         // Find the structured name for the raw contact.
    547         final Cursor structuredName = cr.query(
    548                 Profile.CONTENT_URI.buildUpon().appendPath(Contacts.Data.CONTENT_DIRECTORY).build(),
    549                 new String[] {CommonDataKinds.StructuredName.GIVEN_NAME,
    550                     CommonDataKinds.StructuredName.FAMILY_NAME},
    551                 Data.RAW_CONTACT_ID + "=" + localRowProfileId,
    552                 null, null);
    553         if (structuredName == null) return null;
    554 
    555         try {
    556             if (!structuredName.moveToFirst()) {
    557                 return null;
    558             }
    559             String partialName = structuredName.getString(0);
    560             if (TextUtils.isEmpty(partialName)) {
    561                 partialName = structuredName.getString(1);
    562             }
    563             return partialName;
    564         } finally {
    565             structuredName.close();
    566         }
    567     }
    568 
    569     private static final String getProfileDisplayName(Context context) {
    570         final ContentResolver cr = context.getContentResolver();
    571         final Cursor profile = cr.query(Profile.CONTENT_URI,
    572                 new String[] {Profile.DISPLAY_NAME}, null, null, null);
    573         if (profile == null) return null;
    574 
    575         try {
    576             if (!profile.moveToFirst()) {
    577                 return null;
    578             }
    579             return profile.getString(0);
    580         } finally {
    581             profile.close();
    582         }
    583     }
    584 
    585     /** Not global warming, it's global change warning. */
    586     public static Dialog buildGlobalChangeWarningDialog(final Context context, int titleResId,
    587             final Runnable positiveAction) {
    588         final AlertDialog.Builder builder = new AlertDialog.Builder(context);
    589         builder.setTitle(titleResId);
    590         builder.setMessage(R.string.global_change_warning);
    591         builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
    592             @Override
    593             public void onClick(DialogInterface dialog, int which) {
    594                 positiveAction.run();
    595             }
    596         });
    597         builder.setNegativeButton(android.R.string.cancel, null);
    598 
    599         return builder.create();
    600     }
    601 
    602     public static boolean hasMultipleUsers(Context context) {
    603         return ((UserManager) context.getSystemService(Context.USER_SERVICE))
    604                 .getUsers().size() > 1;
    605     }
    606 }
    607