summaryrefslogtreecommitdiff
path: root/packages/SettingsLib/src
diff options
context:
space:
mode:
Diffstat (limited to 'packages/SettingsLib/src')
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/CustomEditTextPreference.java18
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/DeviceInfoUtils.java61
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/HelpUtils.java2
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/RestrictedLockUtils.java74
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/TwoTargetPreference.java25
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/Utils.java109
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java39
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/applications/PackageManagerWrapper.java131
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/applications/PackageManagerWrapperImpl.java121
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/applications/ServiceListing.java226
-rwxr-xr-xpackages/SettingsLib/src/com/android/settingslib/bluetooth/A2dpProfile.java9
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothA2dpWrapper.java58
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothManager.java1
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/bluetooth/OWNERS8
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/bluetooth/Utils.java82
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/core/AbstractPreferenceController.java122
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/core/ConfirmationDialogController.java46
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/core/instrumentation/EventLogWriter.java110
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/core/instrumentation/Instrumentable.java28
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/core/instrumentation/LogWriter.java84
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/core/instrumentation/MetricsFeatureProvider.java159
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/core/instrumentation/SharedPreferencesLogger.java259
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/core/instrumentation/VisibilityLoggerMixin.java95
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/core/lifecycle/Lifecycle.java87
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/core/lifecycle/LifecycleObserver.java5
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/core/lifecycle/ObservableActivity.java28
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/core/lifecycle/ObservableDialogFragment.java38
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/core/lifecycle/ObservableFragment.java37
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/core/lifecycle/ObservablePreferenceFragment.java38
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/core/lifecycle/events/OnAttach.java4
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/core/lifecycle/events/OnCreate.java6
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/core/lifecycle/events/OnDestroy.java7
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/core/lifecycle/events/OnPause.java7
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/core/lifecycle/events/OnResume.java7
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/core/lifecycle/events/OnStart.java8
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/core/lifecycle/events/OnStop.java8
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/datetime/ZoneGetter.java19
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/development/AbstractEnableAdbPreferenceController.java33
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/development/AbstractLogdSizePreferenceController.java184
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/development/AbstractLogpersistPreferenceController.java258
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/development/DeveloperOptionsPreferenceController.java76
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/development/DevelopmentSettingsEnabler.java52
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/development/SystemPropPoker.java98
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/deviceinfo/AbstractBluetoothAddressPreferenceController.java85
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/deviceinfo/AbstractConnectivityPreferenceController.java114
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/deviceinfo/AbstractImsStatusPreferenceController.java95
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/deviceinfo/AbstractIpAddressPreferenceController.java112
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/deviceinfo/AbstractSerialNumberPreferenceController.java66
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/deviceinfo/AbstractSimStatusImeiInfoPreferenceController.java36
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/deviceinfo/AbstractUptimePreferenceController.java119
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/deviceinfo/AbstractWifiMacAddressPreferenceController.java88
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/drawer/CategoryManager.java41
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/drawer/DashboardCategory.java105
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/drawer/SettingsDrawerActivity.java32
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/drawer/TileUtils.java44
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/drawer/UserAdapter.java5
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/fuelgauge/PowerWhitelistBackend.java108
-rwxr-xr-xpackages/SettingsLib/src/com/android/settingslib/graph/BatteryMeterDrawableBase.java18
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/graph/SignalDrawable.java517
-rw-r--r--[-rwxr-xr-x]packages/SettingsLib/src/com/android/settingslib/inputmethod/InputMethodPreference.java43
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/inputmethod/InputMethodSubtypePreference.java40
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/inputmethod/OWNERS5
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/license/LicenseHtmlGeneratorFromXml.java292
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/license/LicenseHtmlLoader.java110
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/location/RecentLocationApps.java47
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/net/DataUsageController.java5
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/notification/EnableZenModeDialog.java505
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/notification/ZenRadioLayout.java93
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/suggestions/SuggestionController.java174
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/suggestions/SuggestionControllerMixin.java140
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/suggestions/SuggestionList.java10
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/suggestions/SuggestionLoader.java54
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/suggestions/SuggestionParser.java2
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/utils/AsyncLoader.java110
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/utils/PowerUtil.java143
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/utils/StringUtil.java149
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/utils/ThreadUtils.java35
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/wifi/AccessPoint.java432
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/wifi/AccessPointPreference.java90
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/wifi/OWNERS7
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/wifi/TestAccessPointBuilder.java12
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/wifi/WifiStatusTracker.java5
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/wifi/WifiTracker.java339
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/wifi/WifiTrackerFactory.java11
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/wifi/WifiUtils.java197
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/wrapper/BluetoothA2dpWrapper.java (renamed from packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothA2dpWrapperImpl.java)38
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/wrapper/LocationManagerWrapper.java64
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/wrapper/PackageManagerWrapper.java246
88 files changed, 6556 insertions, 1294 deletions
diff --git a/packages/SettingsLib/src/com/android/settingslib/CustomEditTextPreference.java b/packages/SettingsLib/src/com/android/settingslib/CustomEditTextPreference.java
index 253ca11bc44e..8e29e50fc848 100644
--- a/packages/SettingsLib/src/com/android/settingslib/CustomEditTextPreference.java
+++ b/packages/SettingsLib/src/com/android/settingslib/CustomEditTextPreference.java
@@ -20,6 +20,7 @@ import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.os.Bundle;
+import android.support.annotation.CallSuper;
import android.support.v14.preference.EditTextPreferenceDialogFragment;
import android.support.v7.preference.EditTextPreference;
import android.util.AttributeSet;
@@ -30,7 +31,8 @@ public class CustomEditTextPreference extends EditTextPreference {
private CustomPreferenceDialogFragment mFragment;
- public CustomEditTextPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ public CustomEditTextPreference(Context context, AttributeSet attrs, int defStyleAttr,
+ int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
@@ -47,8 +49,13 @@ public class CustomEditTextPreference extends EditTextPreference {
}
public EditText getEditText() {
- return mFragment != null ? (EditText) mFragment.getDialog().findViewById(android.R.id.edit)
- : null;
+ if (mFragment != null) {
+ final Dialog dialog = mFragment.getDialog();
+ if (dialog != null) {
+ return (EditText) dialog.findViewById(android.R.id.edit);
+ }
+ }
+ return null;
}
public boolean isDialogOpen() {
@@ -69,7 +76,12 @@ public class CustomEditTextPreference extends EditTextPreference {
protected void onClick(DialogInterface dialog, int which) {
}
+ @CallSuper
protected void onBindDialogView(View view) {
+ final EditText editText = view.findViewById(android.R.id.edit);
+ if (editText != null) {
+ editText.requestFocus();
+ }
}
private void setFragment(CustomPreferenceDialogFragment fragment) {
diff --git a/packages/SettingsLib/src/com/android/settingslib/DeviceInfoUtils.java b/packages/SettingsLib/src/com/android/settingslib/DeviceInfoUtils.java
index 78a9064f1400..f2cd1033cd38 100644
--- a/packages/SettingsLib/src/com/android/settingslib/DeviceInfoUtils.java
+++ b/packages/SettingsLib/src/com/android/settingslib/DeviceInfoUtils.java
@@ -22,12 +22,15 @@ import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.os.Build;
+import android.system.Os;
+import android.system.StructUtsname;
import android.telephony.PhoneNumberUtils;
import android.telephony.SubscriptionInfo;
import android.telephony.TelephonyManager;
import android.text.TextUtils;
import android.text.format.DateFormat;
import android.util.Log;
+import android.support.annotation.VisibleForTesting;
import java.io.BufferedReader;
import java.io.FileReader;
@@ -45,7 +48,6 @@ import static android.content.Context.TELEPHONY_SERVICE;
public class DeviceInfoUtils {
private static final String TAG = "DeviceInfoUtils";
- private static final String FILENAME_PROC_VERSION = "/proc/version";
private static final String FILENAME_MSV = "/sys/board_properties/soc/msv";
/**
@@ -63,43 +65,36 @@ public class DeviceInfoUtils {
}
}
- public static String getFormattedKernelVersion() {
- try {
- return formatKernelVersion(readLine(FILENAME_PROC_VERSION));
- } catch (IOException e) {
- Log.e(TAG, "IO Exception when getting kernel version for Device Info screen",
- e);
-
- return "Unavailable";
- }
+ public static String getFormattedKernelVersion(Context context) {
+ return formatKernelVersion(context, Os.uname());
}
- public static String formatKernelVersion(String rawKernelVersion) {
- // Example (see tests for more):
- // Linux version 4.9.29-g958411d (android-build@xyz) (Android clang version 3.8.256229 \
- // (based on LLVM 3.8.256229)) #1 SMP PREEMPT Wed Jun 7 00:06:03 CST 2017
- // Linux version 4.9.29-geb63318482a7 (android-build@xyz) (gcc version 4.9.x 20150123 \
- // (prerelease) (GCC) ) #1 SMP PREEMPT Thu Jun 1 03:41:57 UTC 2017
- final String PROC_VERSION_REGEX =
- "Linux version (\\S+) " + /* group 1: "3.0.31-g6fb96c9" */
- "\\((\\S+?)\\) " + /* group 2: "(x@y.com) " */
- "\\((.+?)\\) " + /* group 3: kernel toolchain version information */
- "(#\\d+) " + /* group 4: "#1" */
+ @VisibleForTesting
+ static String formatKernelVersion(Context context, StructUtsname uname) {
+ if (uname == null) {
+ return context.getString(R.string.status_unavailable);
+ }
+ // Example:
+ // 4.9.29-g958411d
+ // #1 SMP PREEMPT Wed Jun 7 00:06:03 CST 2017
+ final String VERSION_REGEX =
+ "(#\\d+) " + /* group 1: "#1" */
"(?:.*?)?" + /* ignore: optional SMP, PREEMPT, and any CONFIG_FLAGS */
- "((Sun|Mon|Tue|Wed|Thu|Fri|Sat).+)"; /* group 5: "Thu Jun 28 11:02:39 PDT 2012" */
-
- Matcher m = Pattern.compile(PROC_VERSION_REGEX).matcher(rawKernelVersion);
+ "((Sun|Mon|Tue|Wed|Thu|Fri|Sat).+)"; /* group 2: "Thu Jun 28 11:02:39 PDT 2012" */
+ Matcher m = Pattern.compile(VERSION_REGEX).matcher(uname.version);
if (!m.matches()) {
- Log.e(TAG, "Regex did not match on /proc/version: " + rawKernelVersion);
- return "Unavailable";
- } else if (m.groupCount() < 4) {
- Log.e(TAG, "Regex match on /proc/version only returned " + m.groupCount()
- + " groups");
- return "Unavailable";
+ Log.e(TAG, "Regex did not match on uname version " + uname.version);
+ return context.getString(R.string.status_unavailable);
}
- return m.group(1) + " ("+ m.group(3) + ")\n" + // 3.0.31-g6fb96c9 (toolchain version)
- m.group(2) + " " + m.group(4) + "\n" + // x@y.com #1
- m.group(5); // Thu Jun 28 11:02:39 PDT 2012
+
+ // Example output:
+ // 4.9.29-g958411d
+ // #1 Wed Jun 7 00:06:03 CST 2017
+ return new StringBuilder().append(uname.release)
+ .append("\n")
+ .append(m.group(1))
+ .append(" ")
+ .append(m.group(2)).toString();
}
/**
diff --git a/packages/SettingsLib/src/com/android/settingslib/HelpUtils.java b/packages/SettingsLib/src/com/android/settingslib/HelpUtils.java
index 2c2641079953..8055caaad536 100644
--- a/packages/SettingsLib/src/com/android/settingslib/HelpUtils.java
+++ b/packages/SettingsLib/src/com/android/settingslib/HelpUtils.java
@@ -227,7 +227,7 @@ public class HelpUtils {
// cache the version code
PackageInfo info = context.getPackageManager().getPackageInfo(
context.getPackageName(), 0);
- sCachedVersionCode = Integer.toString(info.versionCode);
+ sCachedVersionCode = Long.toString(info.getLongVersionCode());
// append the version code to the uri
builder.appendQueryParameter(PARAM_VERSION, sCachedVersionCode);
diff --git a/packages/SettingsLib/src/com/android/settingslib/RestrictedLockUtils.java b/packages/SettingsLib/src/com/android/settingslib/RestrictedLockUtils.java
index 32e6389dc34f..7728f667ea51 100644
--- a/packages/SettingsLib/src/com/android/settingslib/RestrictedLockUtils.java
+++ b/packages/SettingsLib/src/com/android/settingslib/RestrictedLockUtils.java
@@ -28,6 +28,7 @@ import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.IPackageManager;
+import android.content.pm.PackageManager;
import android.content.pm.UserInfo;
import android.graphics.drawable.Drawable;
import android.os.RemoteException;
@@ -343,7 +344,8 @@ public class RestrictedLockUtils {
}
DevicePolicyManager dpm = (DevicePolicyManager) context.getSystemService(
Context.DEVICE_POLICY_SERVICE);
- if (dpm == null) {
+ PackageManager pm = context.getPackageManager();
+ if (!pm.hasSystemFeature(PackageManager.FEATURE_DEVICE_ADMIN) || dpm == null) {
return null;
}
boolean isAccountTypeDisabled = false;
@@ -361,6 +363,26 @@ public class RestrictedLockUtils {
}
/**
+ * Check if {@param packageName} is restricted by the profile or device owner from using
+ * metered data.
+ *
+ * @return EnforcedAdmin object containing the enforced admin component and admin user details,
+ * or {@code null} if the {@param packageName} is not restricted.
+ */
+ public static EnforcedAdmin checkIfMeteredDataRestricted(Context context,
+ String packageName, int userId) {
+ final EnforcedAdmin enforcedAdmin = getProfileOrDeviceOwner(context, userId);
+ if (enforcedAdmin == null) {
+ return null;
+ }
+
+ final DevicePolicyManager dpm = (DevicePolicyManager) context.getSystemService(
+ Context.DEVICE_POLICY_SERVICE);
+ return dpm.isMeteredDataDisabledForUser(enforcedAdmin.component, packageName, userId)
+ ? enforcedAdmin : null;
+ }
+
+ /**
* Checks if {@link android.app.admin.DevicePolicyManager#setAutoTimeRequired} is enforced
* on the device.
*
@@ -430,53 +452,9 @@ public class RestrictedLockUtils {
* the admin component will be set to {@code null} and userId to {@link UserHandle#USER_NULL}
*/
public static EnforcedAdmin checkIfMaximumTimeToLockIsSet(Context context) {
- final DevicePolicyManager dpm = (DevicePolicyManager) context.getSystemService(
- Context.DEVICE_POLICY_SERVICE);
- if (dpm == null) {
- return null;
- }
- EnforcedAdmin enforcedAdmin = null;
- final int userId = UserHandle.myUserId();
- final UserManager um = UserManager.get(context);
- final List<UserInfo> profiles = um.getProfiles(userId);
- final int profilesSize = profiles.size();
- // As we do not have a separate screen lock timeout settings for work challenge,
- // we need to combine all profiles maximum time to lock even work challenge is
- // enabled.
- for (int i = 0; i < profilesSize; i++) {
- final UserInfo userInfo = profiles.get(i);
- final List<ComponentName> admins = dpm.getActiveAdminsAsUser(userInfo.id);
- if (admins == null) {
- continue;
- }
- for (ComponentName admin : admins) {
- if (dpm.getMaximumTimeToLock(admin, userInfo.id) > 0) {
- if (enforcedAdmin == null) {
- enforcedAdmin = new EnforcedAdmin(admin, userInfo.id);
- } else {
- return EnforcedAdmin.MULTIPLE_ENFORCED_ADMIN;
- }
- // This same admins could have set policies both on the managed profile
- // and on the parent. So, if the admin has set the policy on the
- // managed profile here, we don't need to further check if that admin
- // has set policy on the parent admin.
- continue;
- }
- if (userInfo.isManagedProfile()) {
- // If userInfo.id is a managed profile, we also need to look at
- // the policies set on the parent.
- DevicePolicyManager parentDpm = sProxy.getParentProfileInstance(dpm, userInfo);
- if (parentDpm.getMaximumTimeToLock(admin, userInfo.id) > 0) {
- if (enforcedAdmin == null) {
- enforcedAdmin = new EnforcedAdmin(admin, userInfo.id);
- } else {
- return EnforcedAdmin.MULTIPLE_ENFORCED_ADMIN;
- }
- }
- }
- }
- }
- return enforcedAdmin;
+ return checkForLockSetting(context, UserHandle.myUserId(),
+ (DevicePolicyManager dpm, ComponentName admin, @UserIdInt int userId) ->
+ dpm.getMaximumTimeToLock(admin, userId) > 0);
}
private interface LockSettingCheck {
diff --git a/packages/SettingsLib/src/com/android/settingslib/TwoTargetPreference.java b/packages/SettingsLib/src/com/android/settingslib/TwoTargetPreference.java
index 1c161dffced9..8b39f60aa9ca 100644
--- a/packages/SettingsLib/src/com/android/settingslib/TwoTargetPreference.java
+++ b/packages/SettingsLib/src/com/android/settingslib/TwoTargetPreference.java
@@ -21,41 +21,56 @@ import android.support.v7.preference.Preference;
import android.support.v7.preference.PreferenceViewHolder;
import android.util.AttributeSet;
import android.view.View;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
public class TwoTargetPreference extends Preference {
+ private boolean mUseSmallIcon;
+ private int mSmallIconSize;
+
public TwoTargetPreference(Context context, AttributeSet attrs,
int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
- init();
+ init(context);
}
public TwoTargetPreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
- init();
+ init(context);
}
public TwoTargetPreference(Context context, AttributeSet attrs) {
super(context, attrs);
- init();
+ init(context);
}
public TwoTargetPreference(Context context) {
super(context);
- init();
+ init(context);
}
- private void init() {
+ private void init(Context context) {
setLayoutResource(R.layout.preference_two_target);
+ mSmallIconSize = context.getResources().getDimensionPixelSize(
+ R.dimen.two_target_pref_small_icon_size);
final int secondTargetResId = getSecondTargetResId();
if (secondTargetResId != 0) {
setWidgetLayoutResource(secondTargetResId);
}
}
+ public void setUseSmallIcon(boolean useSmallIcon) {
+ mUseSmallIcon = useSmallIcon;
+ }
+
@Override
public void onBindViewHolder(PreferenceViewHolder holder) {
super.onBindViewHolder(holder);
+ if (mUseSmallIcon) {
+ ImageView icon = holder.itemView.findViewById(android.R.id.icon);
+ icon.setLayoutParams(new LinearLayout.LayoutParams(mSmallIconSize, mSmallIconSize));
+ }
final View divider = holder.findViewById(R.id.two_target_divider);
final View widgetFrame = holder.findViewById(android.R.id.widget_frame);
final boolean shouldHideSecondTarget = shouldHideSecondTarget();
diff --git a/packages/SettingsLib/src/com/android/settingslib/Utils.java b/packages/SettingsLib/src/com/android/settingslib/Utils.java
index 64ec16a23b46..1f67dfb568a6 100644
--- a/packages/SettingsLib/src/com/android/settingslib/Utils.java
+++ b/packages/SettingsLib/src/com/android/settingslib/Utils.java
@@ -12,38 +12,74 @@ import android.content.res.ColorStateList;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
import android.graphics.Color;
-import android.graphics.PorterDuff;
import android.graphics.drawable.Drawable;
-import android.graphics.drawable.LayerDrawable;
+import android.location.LocationManager;
import android.net.ConnectivityManager;
-import android.net.NetworkBadging;
import android.os.BatteryManager;
+import android.os.UserHandle;
import android.os.UserManager;
import android.print.PrintManager;
import android.provider.Settings;
-import android.view.View;
-
import com.android.internal.util.UserIcons;
import com.android.settingslib.drawable.UserIconDrawable;
-
+import com.android.settingslib.wrapper.LocationManagerWrapper;
import java.text.NumberFormat;
public class Utils {
+
+ private static final String CURRENT_MODE_KEY = "CURRENT_MODE";
+ private static final String NEW_MODE_KEY = "NEW_MODE";
+
private static Signature[] sSystemSignature;
private static String sPermissionControllerPackageName;
private static String sServicesSystemSharedLibPackageName;
private static String sSharedSystemSharedLibPackageName;
- static final int[] WIFI_PIE_FOR_BADGING = {
- com.android.internal.R.drawable.ic_signal_wifi_badged_0_bars,
- com.android.internal.R.drawable.ic_signal_wifi_badged_1_bar,
- com.android.internal.R.drawable.ic_signal_wifi_badged_2_bars,
- com.android.internal.R.drawable.ic_signal_wifi_badged_3_bars,
- com.android.internal.R.drawable.ic_signal_wifi_badged_4_bars
+ static final int[] WIFI_PIE = {
+ com.android.internal.R.drawable.ic_wifi_signal_0,
+ com.android.internal.R.drawable.ic_wifi_signal_1,
+ com.android.internal.R.drawable.ic_wifi_signal_2,
+ com.android.internal.R.drawable.ic_wifi_signal_3,
+ com.android.internal.R.drawable.ic_wifi_signal_4
};
+ public static void updateLocationEnabled(Context context, boolean enabled, int userId,
+ int source) {
+ Settings.Secure.putIntForUser(
+ context.getContentResolver(), Settings.Secure.LOCATION_CHANGER, source,
+ userId);
+ Intent intent = new Intent(LocationManager.MODE_CHANGING_ACTION);
+
+ final int oldMode = Settings.Secure.getIntForUser(context.getContentResolver(),
+ Settings.Secure.LOCATION_MODE, Settings.Secure.LOCATION_MODE_OFF, userId);
+ final int newMode = enabled
+ ? Settings.Secure.LOCATION_MODE_HIGH_ACCURACY
+ : Settings.Secure.LOCATION_MODE_OFF;
+ intent.putExtra(CURRENT_MODE_KEY, oldMode);
+ intent.putExtra(NEW_MODE_KEY, newMode);
+ context.sendBroadcastAsUser(
+ intent, UserHandle.of(userId), android.Manifest.permission.WRITE_SECURE_SETTINGS);
+ LocationManager locationManager =
+ (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
+ LocationManagerWrapper wrapper = new LocationManagerWrapper(locationManager);
+ wrapper.setLocationEnabledForUser(enabled, UserHandle.of(userId));
+ }
+
+ public static boolean updateLocationMode(Context context, int oldMode, int newMode, int userId,
+ int source) {
+ Settings.Secure.putIntForUser(
+ context.getContentResolver(), Settings.Secure.LOCATION_CHANGER, source,
+ userId);
+ Intent intent = new Intent(LocationManager.MODE_CHANGING_ACTION);
+ intent.putExtra(CURRENT_MODE_KEY, oldMode);
+ intent.putExtra(NEW_MODE_KEY, newMode);
+ context.sendBroadcastAsUser(
+ intent, UserHandle.of(userId), android.Manifest.permission.WRITE_SECURE_SETTINGS);
+ return Settings.Secure.putIntForUser(
+ context.getContentResolver(), Settings.Secure.LOCATION_MODE, newMode, userId);
+ }
+
/**
* Return string resource that best describes combination of tethering
* options available on this device.
@@ -99,7 +135,7 @@ public class Utils {
public static Drawable getUserIcon(Context context, UserManager um, UserInfo user) {
final int iconSize = UserIconDrawable.getSizeForList(context);
if (user.isManagedProfile()) {
- Drawable drawable = context.getDrawable(com.android.internal.R.drawable.ic_corp_icon);
+ Drawable drawable = context.getDrawable(com.android.internal.R.drawable.ic_corp_badge);
drawable.setBounds(0, 0, iconSize, iconSize);
return drawable;
}
@@ -110,7 +146,8 @@ public class Utils {
}
}
return new UserIconDrawable(iconSize).setIconDrawable(
- UserIcons.getDefaultUserIcon(user.id, /* light= */ false)).bake();
+ UserIcons.getDefaultUserIcon(context.getResources(), user.id, /* light= */ false))
+ .bake();
}
/** Formats a double from 0.0..100.0 with an option to round **/
@@ -272,42 +309,17 @@ public class Utils {
}
/**
- * Returns a badged Wifi icon drawable.
- *
- * <p>The first layer contains the Wifi pie and the second layer contains the badge. Callers
- * should set the drawable to the appropriate size and tint color.
+ * Returns the Wifi icon resource for a given RSSI level.
*
- * @param context The caller's context (must have access to internal resources)
* @param level The number of bars to show (0-4)
- * @param badge The badge enum {@see android.net.ScoredNetwork}
*
- * @throws IllegalArgumentException if an invalid badge enum is given
- *
- * @deprecated TODO(sghuman): Finalize the form of this method and then move it to a new
- * location.
+ * @throws IllegalArgumentException if an invalid RSSI level is given.
*/
- public static LayerDrawable getBadgedWifiIcon(Context context, int level, int badge) {
- return new LayerDrawable(
- new Drawable[] {
- context.getDrawable(WIFI_PIE_FOR_BADGING[level]),
- context.getDrawable(getWifiBadgeResource(badge))
- });
- }
-
- private static int getWifiBadgeResource(int badge) {
- switch (badge) {
- case NetworkBadging.BADGING_NONE:
- return View.NO_ID;
- case NetworkBadging.BADGING_SD:
- return com.android.internal.R.drawable.ic_signal_wifi_badged_sd;
- case NetworkBadging.BADGING_HD:
- return com.android.internal.R.drawable.ic_signal_wifi_badged_hd;
- case NetworkBadging.BADGING_4K:
- return com.android.internal.R.drawable.ic_signal_wifi_badged_4k;
- default:
- throw new IllegalArgumentException(
- "No badge resource found for badge value: " + badge);
+ public static int getWifiIconResource(int level) {
+ if (level < 0 || level >= WIFI_PIE.length) {
+ throw new IllegalArgumentException("No Wifi icon found for level: " + level);
}
+ return WIFI_PIE[level];
}
public static int getDefaultStorageManagerDaysToRetain(Resources resources) {
@@ -325,4 +337,9 @@ public class Utils {
}
return defaultDays;
}
+
+ public static boolean isWifiOnly(Context context) {
+ return !context.getSystemService(ConnectivityManager.class)
+ .isNetworkSupported(ConnectivityManager.TYPE_MOBILE);
+ }
}
diff --git a/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java b/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java
index 40c2b1f3b771..3a0ae9f532bb 100644
--- a/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java
+++ b/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java
@@ -21,6 +21,9 @@ import android.app.AppGlobals;
import android.app.Application;
import android.app.usage.StorageStats;
import android.app.usage.StorageStatsManager;
+import android.arch.lifecycle.Lifecycle;
+import android.arch.lifecycle.LifecycleObserver;
+import android.arch.lifecycle.OnLifecycleEvent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
@@ -45,6 +48,7 @@ import android.os.RemoteException;
import android.os.SystemClock;
import android.os.UserHandle;
import android.os.UserManager;
+import android.support.annotation.VisibleForTesting;
import android.text.format.Formatter;
import android.util.IconDrawableFactory;
import android.util.Log;
@@ -180,7 +184,11 @@ public class ApplicationsState {
}
public Session newSession(Callbacks callbacks) {
- Session s = new Session(callbacks);
+ return newSession(callbacks, null);
+ }
+
+ public Session newSession(Callbacks callbacks, Lifecycle lifecycle) {
+ Session s = new Session(callbacks, lifecycle);
synchronized (mEntriesMap) {
mSessions.add(s);
}
@@ -586,7 +594,7 @@ public class ApplicationsState {
.replaceAll("").toLowerCase();
}
- public class Session {
+ public class Session implements LifecycleObserver {
final Callbacks mCallbacks;
boolean mResumed;
@@ -600,11 +608,20 @@ public class ApplicationsState {
ArrayList<AppEntry> mLastAppList;
boolean mRebuildForeground;
- Session(Callbacks callbacks) {
+ private final boolean mHasLifecycle;
+
+ Session(Callbacks callbacks, Lifecycle lifecycle) {
mCallbacks = callbacks;
+ if (lifecycle != null) {
+ lifecycle.addObserver(this);
+ mHasLifecycle = true;
+ } else {
+ mHasLifecycle = false;
+ }
}
- public void resume() {
+ @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
+ public void onResume() {
if (DEBUG_LOCKING) Log.v(TAG, "resume about to acquire lock...");
synchronized (mEntriesMap) {
if (!mResumed) {
@@ -616,7 +633,8 @@ public class ApplicationsState {
if (DEBUG_LOCKING) Log.v(TAG, "...resume releasing lock");
}
- public void pause() {
+ @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
+ public void onPause() {
if (DEBUG_LOCKING) Log.v(TAG, "pause about to acquire lock...");
synchronized (mEntriesMap) {
if (mResumed) {
@@ -735,8 +753,12 @@ public class ApplicationsState {
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
}
- public void release() {
- pause();
+ @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
+ public void onDestroy() {
+ if (!mHasLifecycle) {
+ // TODO: Legacy, remove this later once all usages are switched to Lifecycle
+ onPause();
+ }
synchronized (mEntriesMap) {
mSessions.remove(this);
}
@@ -1261,7 +1283,8 @@ public class ApplicationsState {
// A location where extra info can be placed to be used by custom filters.
public Object extraInfo;
- AppEntry(Context context, ApplicationInfo info, long id) {
+ @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
+ public AppEntry(Context context, ApplicationInfo info, long id) {
apkFile = new File(info.sourceDir);
this.id = id;
this.info = info;
diff --git a/packages/SettingsLib/src/com/android/settingslib/applications/PackageManagerWrapper.java b/packages/SettingsLib/src/com/android/settingslib/applications/PackageManagerWrapper.java
deleted file mode 100644
index 6c79a6124ca2..000000000000
--- a/packages/SettingsLib/src/com/android/settingslib/applications/PackageManagerWrapper.java
+++ /dev/null
@@ -1,131 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.settingslib.applications;
-
-import android.content.ComponentName;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.content.pm.ApplicationInfo;
-import android.content.pm.PackageInfo;
-import android.content.pm.PackageManager;
-import android.content.pm.PackageManager.NameNotFoundException;
-import android.content.pm.ResolveInfo;
-import android.graphics.drawable.Drawable;
-import android.os.UserHandle;
-
-import java.util.List;
-
-/**
- * This interface replicates a subset of the android.content.pm.PackageManager (PM). The interface
- * exists so that we can use a thin wrapper around the PM in production code and a mock in tests.
- * We cannot directly mock or shadow the PM, because some of the methods we rely on are newer than
- * the API version supported by Robolectric.
- */
-public interface PackageManagerWrapper {
-
- /**
- * Returns the real {@code PackageManager} object.
- */
- PackageManager getPackageManager();
-
- /**
- * Calls {@code PackageManager.getInstalledApplicationsAsUser()}.
- *
- * @see android.content.pm.PackageManager#getInstalledApplicationsAsUser
- */
- List<ApplicationInfo> getInstalledApplicationsAsUser(int flags, int userId);
-
- /**
- * Calls {@code PackageManager.hasSystemFeature()}.
- *
- * @see android.content.pm.PackageManager#hasSystemFeature
- */
- boolean hasSystemFeature(String name);
-
- /**
- * Calls {@code PackageManager.queryIntentActivitiesAsUser()}.
- *
- * @see android.content.pm.PackageManager#queryIntentActivitiesAsUser
- */
- List<ResolveInfo> queryIntentActivitiesAsUser(Intent intent, int flags, int userId);
-
- /**
- * Calls {@code PackageManager.getInstallReason()}.
- *
- * @see android.content.pm.PackageManager#getInstallReason
- */
- int getInstallReason(String packageName, UserHandle user);
-
- /**
- * Calls {@code PackageManager.getApplicationInfoAsUser}
- */
- ApplicationInfo getApplicationInfoAsUser(String packageName, int i, int userId)
- throws PackageManager.NameNotFoundException;
-
- /**
- * Calls {@code PackageManager.setDefaultBrowserPackageNameAsUser}
- */
- boolean setDefaultBrowserPackageNameAsUser(String packageName, int userId);
-
- /**
- * Calls {@code PackageManager.getDefaultBrowserPackageNameAsUser}
- */
- String getDefaultBrowserPackageNameAsUser(int userId);
-
- /**
- * Calls {@code PackageManager.getHomeActivities}
- */
- ComponentName getHomeActivities(List<ResolveInfo> homeActivities);
-
- /**
- * Calls {@code PackageManager.queryIntentServicesAsUser}
- */
- List<ResolveInfo> queryIntentServicesAsUser(Intent intent, int i, int user);
-
- /**
- * Calls {@code PackageManager.replacePreferredActivity}
- */
- void replacePreferredActivity(IntentFilter homeFilter, int matchCategoryEmpty,
- ComponentName[] componentNames, ComponentName component);
-
- /**
- * Gets information about a particular package from the package manager.
- * @param packageName The name of the package we would like information about.
- * @param i additional options flags. see javadoc for {@link PackageManager#getPackageInfo(String, int)}
- * @return The PackageInfo for the requested package
- * @throws NameNotFoundException
- */
- PackageInfo getPackageInfo(String packageName, int i) throws NameNotFoundException;
-
- /**
- * Retrieves the icon associated with this particular set of ApplicationInfo
- * @param info The ApplicationInfo to retrieve the icon for
- * @return The icon as a drawable.
- */
- Drawable getUserBadgedIcon(ApplicationInfo info);
-
- /**
- * Retrieves the label associated with the particular set of ApplicationInfo
- * @param app The ApplicationInfo to retrieve the label for
- * @return the label as a CharSequence
- */
- CharSequence loadLabel(ApplicationInfo app);
-
- /**
- * Retrieve all activities that can be performed for the given intent.
- */
- List<ResolveInfo> queryIntentActivities(Intent intent, int flags);
-}
diff --git a/packages/SettingsLib/src/com/android/settingslib/applications/PackageManagerWrapperImpl.java b/packages/SettingsLib/src/com/android/settingslib/applications/PackageManagerWrapperImpl.java
deleted file mode 100644
index dcb40b20365e..000000000000
--- a/packages/SettingsLib/src/com/android/settingslib/applications/PackageManagerWrapperImpl.java
+++ /dev/null
@@ -1,121 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.settingslib.applications;
-
-import android.content.ComponentName;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.content.pm.ApplicationInfo;
-import android.content.pm.PackageInfo;
-import android.content.pm.PackageManager;
-import android.content.pm.PackageManager.NameNotFoundException;
-import android.content.pm.ResolveInfo;
-import android.graphics.drawable.Drawable;
-import android.os.UserHandle;
-
-import java.util.List;
-
-/**
- * A thin wrapper class that simplifies testing by putting a mockable layer between the application
- * and the PackageManager. This class only provides access to the minimum number of functions from
- * the PackageManager needed for DeletionHelper to work.
- */
-public class PackageManagerWrapperImpl implements PackageManagerWrapper {
-
- private final PackageManager mPm;
-
- public PackageManagerWrapperImpl(PackageManager pm) {
- mPm = pm;
- }
-
- @Override
- public PackageManager getPackageManager() {
- return mPm;
- }
-
- @Override
- public List<ApplicationInfo> getInstalledApplicationsAsUser(int flags, int userId) {
- return mPm.getInstalledApplicationsAsUser(flags, userId);
- }
-
- @Override
- public boolean hasSystemFeature(String name) {
- return mPm.hasSystemFeature(name);
- }
-
- @Override
- public List<ResolveInfo> queryIntentActivitiesAsUser(Intent intent, int flags, int userId) {
- return mPm.queryIntentActivitiesAsUser(intent, flags, userId);
- }
-
- @Override
- public int getInstallReason(String packageName, UserHandle user) {
- return mPm.getInstallReason(packageName, user);
- }
-
- @Override
- public ApplicationInfo getApplicationInfoAsUser(String packageName, int i, int userId)
- throws PackageManager.NameNotFoundException {
- return mPm.getApplicationInfoAsUser(packageName, i, userId);
- }
-
- @Override
- public boolean setDefaultBrowserPackageNameAsUser(String packageName, int userId) {
- return mPm.setDefaultBrowserPackageNameAsUser(packageName, userId);
- }
-
- @Override
- public String getDefaultBrowserPackageNameAsUser(int userId) {
- return mPm.getDefaultBrowserPackageNameAsUser(userId);
- }
-
- @Override
- public ComponentName getHomeActivities(List<ResolveInfo> homeActivities) {
- return mPm.getHomeActivities(homeActivities);
- }
-
- @Override
- public List<ResolveInfo> queryIntentServicesAsUser(Intent intent, int i, int user) {
- return mPm.queryIntentServicesAsUser(intent, i, user);
- }
-
- @Override
- public void replacePreferredActivity(IntentFilter homeFilter, int matchCategoryEmpty,
- ComponentName[] componentNames, ComponentName component) {
- mPm.replacePreferredActivity(homeFilter, matchCategoryEmpty, componentNames, component);
- }
-
- @Override
- public PackageInfo getPackageInfo(String packageName, int i) throws NameNotFoundException {
- return mPm.getPackageInfo(packageName, i);
- }
-
- @Override
- public Drawable getUserBadgedIcon(ApplicationInfo info) {
- return mPm.getUserBadgedIcon(mPm.loadUnbadgedItemIcon(info, info),
- new UserHandle(UserHandle.getUserId(info.uid)));
- }
-
- @Override
- public CharSequence loadLabel(ApplicationInfo app) {
- return app.loadLabel(mPm);
- }
-
- @Override
- public List<ResolveInfo> queryIntentActivities(Intent intent, int flags) {
- return mPm.queryIntentActivities(intent, flags);
- }
-}
diff --git a/packages/SettingsLib/src/com/android/settingslib/applications/ServiceListing.java b/packages/SettingsLib/src/com/android/settingslib/applications/ServiceListing.java
new file mode 100644
index 000000000000..3c3c70ac364e
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/applications/ServiceListing.java
@@ -0,0 +1,226 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.applications;
+
+import android.app.ActivityManager;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.pm.ServiceInfo;
+import android.database.ContentObserver;
+import android.net.Uri;
+import android.os.Handler;
+import android.provider.Settings;
+import android.util.Slog;
+
+import com.android.settingslib.wrapper.PackageManagerWrapper;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+
+/**
+ * Class for managing services matching a given intent and requesting a given permission.
+ */
+public class ServiceListing {
+ private final ContentResolver mContentResolver;
+ private final Context mContext;
+ private final String mTag;
+ private final String mSetting;
+ private final String mIntentAction;
+ private final String mPermission;
+ private final String mNoun;
+ private final HashSet<ComponentName> mEnabledServices = new HashSet<>();
+ private final List<ServiceInfo> mServices = new ArrayList<>();
+ private final List<Callback> mCallbacks = new ArrayList<>();
+
+ private boolean mListening;
+
+ private ServiceListing(Context context, String tag,
+ String setting, String intentAction, String permission, String noun) {
+ mContentResolver = context.getContentResolver();
+ mContext = context;
+ mTag = tag;
+ mSetting = setting;
+ mIntentAction = intentAction;
+ mPermission = permission;
+ mNoun = noun;
+ }
+
+ public void addCallback(Callback callback) {
+ mCallbacks.add(callback);
+ }
+
+ public void removeCallback(Callback callback) {
+ mCallbacks.remove(callback);
+ }
+
+ public void setListening(boolean listening) {
+ if (mListening == listening) return;
+ mListening = listening;
+ if (mListening) {
+ // listen for package changes
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(Intent.ACTION_PACKAGE_ADDED);
+ filter.addAction(Intent.ACTION_PACKAGE_CHANGED);
+ filter.addAction(Intent.ACTION_PACKAGE_REMOVED);
+ filter.addAction(Intent.ACTION_PACKAGE_REPLACED);
+ filter.addDataScheme("package");
+ mContext.registerReceiver(mPackageReceiver, filter);
+ mContentResolver.registerContentObserver(Settings.Secure.getUriFor(mSetting),
+ false, mSettingsObserver);
+ } else {
+ mContext.unregisterReceiver(mPackageReceiver);
+ mContentResolver.unregisterContentObserver(mSettingsObserver);
+ }
+ }
+
+ private void saveEnabledServices() {
+ StringBuilder sb = null;
+ for (ComponentName cn : mEnabledServices) {
+ if (sb == null) {
+ sb = new StringBuilder();
+ } else {
+ sb.append(':');
+ }
+ sb.append(cn.flattenToString());
+ }
+ Settings.Secure.putString(mContentResolver, mSetting,
+ sb != null ? sb.toString() : "");
+ }
+
+ private void loadEnabledServices() {
+ mEnabledServices.clear();
+ final String flat = Settings.Secure.getString(mContentResolver, mSetting);
+ if (flat != null && !"".equals(flat)) {
+ final String[] names = flat.split(":");
+ for (String name : names) {
+ final ComponentName cn = ComponentName.unflattenFromString(name);
+ if (cn != null) {
+ mEnabledServices.add(cn);
+ }
+ }
+ }
+ }
+
+ public void reload() {
+ loadEnabledServices();
+ mServices.clear();
+ final int user = ActivityManager.getCurrentUser();
+
+ final PackageManagerWrapper pmWrapper =
+ new PackageManagerWrapper(mContext.getPackageManager());
+ List<ResolveInfo> installedServices = pmWrapper.queryIntentServicesAsUser(
+ new Intent(mIntentAction),
+ PackageManager.GET_SERVICES | PackageManager.GET_META_DATA,
+ user);
+
+ for (ResolveInfo resolveInfo : installedServices) {
+ ServiceInfo info = resolveInfo.serviceInfo;
+
+ if (!mPermission.equals(info.permission)) {
+ Slog.w(mTag, "Skipping " + mNoun + " service "
+ + info.packageName + "/" + info.name
+ + ": it does not require the permission "
+ + mPermission);
+ continue;
+ }
+ mServices.add(info);
+ }
+ for (Callback callback : mCallbacks) {
+ callback.onServicesReloaded(mServices);
+ }
+ }
+
+ public boolean isEnabled(ComponentName cn) {
+ return mEnabledServices.contains(cn);
+ }
+
+ public void setEnabled(ComponentName cn, boolean enabled) {
+ if (enabled) {
+ mEnabledServices.add(cn);
+ } else {
+ mEnabledServices.remove(cn);
+ }
+ saveEnabledServices();
+ }
+
+ private final ContentObserver mSettingsObserver = new ContentObserver(new Handler()) {
+ @Override
+ public void onChange(boolean selfChange, Uri uri) {
+ reload();
+ }
+ };
+
+ private final BroadcastReceiver mPackageReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ reload();
+ }
+ };
+
+ public interface Callback {
+ void onServicesReloaded(List<ServiceInfo> services);
+ }
+
+ public static class Builder {
+ private final Context mContext;
+ private String mTag;
+ private String mSetting;
+ private String mIntentAction;
+ private String mPermission;
+ private String mNoun;
+
+ public Builder(Context context) {
+ mContext = context;
+ }
+
+ public Builder setTag(String tag) {
+ mTag = tag;
+ return this;
+ }
+
+ public Builder setSetting(String setting) {
+ mSetting = setting;
+ return this;
+ }
+
+ public Builder setIntentAction(String intentAction) {
+ mIntentAction = intentAction;
+ return this;
+ }
+
+ public Builder setPermission(String permission) {
+ mPermission = permission;
+ return this;
+ }
+
+ public Builder setNoun(String noun) {
+ mNoun = noun;
+ return this;
+ }
+
+ public ServiceListing build() {
+ return new ServiceListing(mContext, mTag, mSetting, mIntentAction, mPermission, mNoun);
+ }
+ }
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/A2dpProfile.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/A2dpProfile.java
index 853cbbadbdac..6b9902425bcb 100755
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/A2dpProfile.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/A2dpProfile.java
@@ -30,6 +30,7 @@ import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
import com.android.settingslib.R;
+import com.android.settingslib.wrapper.BluetoothA2dpWrapper;
import java.util.ArrayList;
import java.util.Arrays;
@@ -42,7 +43,6 @@ public class A2dpProfile implements LocalBluetoothProfile {
private Context mContext;
private BluetoothA2dp mService;
- BluetoothA2dpWrapper.Factory mWrapperFactory;
private BluetoothA2dpWrapper mServiceWrapper;
private boolean mIsProfileReady;
@@ -67,7 +67,7 @@ public class A2dpProfile implements LocalBluetoothProfile {
public void onServiceConnected(int profile, BluetoothProfile proxy) {
if (V) Log.d(TAG,"Bluetooth service connected");
mService = (BluetoothA2dp) proxy;
- mServiceWrapper = mWrapperFactory.getInstance(mService);
+ mServiceWrapper = new BluetoothA2dpWrapper(mService);
// We just bound to the service, so refresh the UI for any connected A2DP devices.
List<BluetoothDevice> deviceList = mService.getConnectedDevices();
while (!deviceList.isEmpty()) {
@@ -101,14 +101,13 @@ public class A2dpProfile implements LocalBluetoothProfile {
mLocalAdapter = adapter;
mDeviceManager = deviceManager;
mProfileManager = profileManager;
- mWrapperFactory = new BluetoothA2dpWrapperImpl.Factory();
mLocalAdapter.getProfileProxy(context, new A2dpServiceListener(),
BluetoothProfile.A2DP);
}
@VisibleForTesting
- void setWrapperFactory(BluetoothA2dpWrapper.Factory factory) {
- mWrapperFactory = factory;
+ void setBluetoothA2dpWrapper(BluetoothA2dpWrapper wrapper) {
+ mServiceWrapper = wrapper;
}
public boolean isConnectable() {
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothA2dpWrapper.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothA2dpWrapper.java
deleted file mode 100644
index dace1bbd5381..000000000000
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothA2dpWrapper.java
+++ /dev/null
@@ -1,58 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.settingslib.bluetooth;
-
-import android.bluetooth.BluetoothA2dp;
-import android.bluetooth.BluetoothCodecStatus;
-import android.bluetooth.BluetoothDevice;
-
-/**
- * This interface replicates some methods of android.bluetooth.BluetoothA2dp that are new and not
- * yet available in our current version of Robolectric. It provides a thin wrapper to call the real
- * methods in production and a mock in tests.
- */
-public interface BluetoothA2dpWrapper {
-
- static interface Factory {
- BluetoothA2dpWrapper getInstance(BluetoothA2dp service);
- }
-
- /**
- * @return the real {@code BluetoothA2dp} object
- */
- BluetoothA2dp getService();
-
- /**
- * Wraps {@code BluetoothA2dp.getCodecStatus}
- */
- public BluetoothCodecStatus getCodecStatus(BluetoothDevice device);
-
- /**
- * Wraps {@code BluetoothA2dp.supportsOptionalCodecs}
- */
- int supportsOptionalCodecs(BluetoothDevice device);
-
- /**
- * Wraps {@code BluetoothA2dp.getOptionalCodecsEnabled}
- */
- int getOptionalCodecsEnabled(BluetoothDevice device);
-
- /**
- * Wraps {@code BluetoothA2dp.setOptionalCodecsEnabled}
- */
- void setOptionalCodecsEnabled(BluetoothDevice device, int value);
-}
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothManager.java
index 1993b459da53..373247162563 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothManager.java
@@ -73,6 +73,7 @@ public class LocalBluetoothManager {
mCachedDeviceManager, context);
mProfileManager = new LocalBluetoothProfileManager(context,
mLocalAdapter, mCachedDeviceManager, mEventManager);
+ mEventManager.readPairedDevices();
}
public LocalBluetoothAdapter getBluetoothAdapter() {
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/OWNERS b/packages/SettingsLib/src/com/android/settingslib/bluetooth/OWNERS
new file mode 100644
index 000000000000..7162121330ef
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/OWNERS
@@ -0,0 +1,8 @@
+# Default reviewers for this and subdirectories.
+asapperstein@google.com
+asargent@google.com
+eisenbach@google.com
+jackqdyulei@google.com
+siyuanh@google.com
+
+# Emergency approvers in case the above are not available \ No newline at end of file
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/Utils.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/Utils.java
index c9194268543a..0ee1dad9d744 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/Utils.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/Utils.java
@@ -1,9 +1,17 @@
package com.android.settingslib.bluetooth;
+import android.bluetooth.BluetoothClass;
+import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothProfile;
import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.support.annotation.DrawableRes;
+import android.util.Pair;
import com.android.settingslib.R;
+import com.android.settingslib.graph.BluetoothDeviceLayerDrawable;
+
+import java.util.List;
public class Utils {
public static final boolean V = false; // verbose logging
@@ -40,4 +48,78 @@ public class Utils {
void onShowError(Context context, String name, int messageResId);
}
+ public static Pair<Drawable, String> getBtClassDrawableWithDescription(Context context,
+ CachedBluetoothDevice cachedDevice) {
+ return getBtClassDrawableWithDescription(context, cachedDevice, 1 /* iconScale */);
+ }
+
+ public static Pair<Drawable, String> getBtClassDrawableWithDescription(Context context,
+ CachedBluetoothDevice cachedDevice, float iconScale) {
+ BluetoothClass btClass = cachedDevice.getBtClass();
+ final int level = cachedDevice.getBatteryLevel();
+ if (btClass != null) {
+ switch (btClass.getMajorDeviceClass()) {
+ case BluetoothClass.Device.Major.COMPUTER:
+ return new Pair<>(getBluetoothDrawable(context, R.drawable.ic_bt_laptop, level,
+ iconScale),
+ context.getString(R.string.bluetooth_talkback_computer));
+
+ case BluetoothClass.Device.Major.PHONE:
+ return new Pair<>(
+ getBluetoothDrawable(context, R.drawable.ic_bt_cellphone, level,
+ iconScale),
+ context.getString(R.string.bluetooth_talkback_phone));
+
+ case BluetoothClass.Device.Major.PERIPHERAL:
+ return new Pair<>(
+ getBluetoothDrawable(context, HidProfile.getHidClassDrawable(btClass),
+ level, iconScale),
+ context.getString(R.string.bluetooth_talkback_input_peripheral));
+
+ case BluetoothClass.Device.Major.IMAGING:
+ return new Pair<>(
+ getBluetoothDrawable(context, R.drawable.ic_settings_print, level,
+ iconScale),
+ context.getString(R.string.bluetooth_talkback_imaging));
+
+ default:
+ // unrecognized device class; continue
+ }
+ }
+
+ List<LocalBluetoothProfile> profiles = cachedDevice.getProfiles();
+ for (LocalBluetoothProfile profile : profiles) {
+ int resId = profile.getDrawableResource(btClass);
+ if (resId != 0) {
+ return new Pair<>(getBluetoothDrawable(context, resId, level, iconScale), null);
+ }
+ }
+ if (btClass != null) {
+ if (btClass.doesClassMatch(BluetoothClass.PROFILE_HEADSET)) {
+ return new Pair<>(
+ getBluetoothDrawable(context, R.drawable.ic_bt_headset_hfp, level,
+ iconScale),
+ context.getString(R.string.bluetooth_talkback_headset));
+ }
+ if (btClass.doesClassMatch(BluetoothClass.PROFILE_A2DP)) {
+ return new Pair<>(
+ getBluetoothDrawable(context, R.drawable.ic_bt_headphones_a2dp, level,
+ iconScale),
+ context.getString(R.string.bluetooth_talkback_headphone));
+ }
+ }
+ return new Pair<>(
+ getBluetoothDrawable(context, R.drawable.ic_settings_bluetooth, level, iconScale),
+ context.getString(R.string.bluetooth_talkback_bluetooth));
+ }
+
+ public static Drawable getBluetoothDrawable(Context context, @DrawableRes int resId,
+ int batteryLevel, float iconScale) {
+ if (batteryLevel != BluetoothDevice.BATTERY_LEVEL_UNKNOWN) {
+ return BluetoothDeviceLayerDrawable.createLayerDrawable(context, resId, batteryLevel,
+ iconScale);
+ } else {
+ return context.getDrawable(resId);
+ }
+ }
}
diff --git a/packages/SettingsLib/src/com/android/settingslib/core/AbstractPreferenceController.java b/packages/SettingsLib/src/com/android/settingslib/core/AbstractPreferenceController.java
index 38fe8790e4d0..d14b53b12fcd 100644
--- a/packages/SettingsLib/src/com/android/settingslib/core/AbstractPreferenceController.java
+++ b/packages/SettingsLib/src/com/android/settingslib/core/AbstractPreferenceController.java
@@ -10,79 +10,71 @@ import android.support.v7.preference.PreferenceScreen;
*/
public abstract class AbstractPreferenceController {
- protected final Context mContext;
+ protected final Context mContext;
- public AbstractPreferenceController(Context context) {
- mContext = context;
- }
+ public AbstractPreferenceController(Context context) {
+ mContext = context;
+ }
- /**
- * Displays preference in this controller.
- */
- public void displayPreference(PreferenceScreen screen) {
- if (isAvailable()) {
- if (this instanceof Preference.OnPreferenceChangeListener) {
- final Preference preference = screen.findPreference(getPreferenceKey());
- preference.setOnPreferenceChangeListener(
- (Preference.OnPreferenceChangeListener) this);
- }
- } else {
- removePreference(screen, getPreferenceKey());
- }
- }
+ /**
+ * Displays preference in this controller.
+ */
+ public void displayPreference(PreferenceScreen screen) {
+ final String prefKey = getPreferenceKey();
+ if (isAvailable()) {
+ setVisible(screen, prefKey, true /* visible */);
+ if (this instanceof Preference.OnPreferenceChangeListener) {
+ final Preference preference = screen.findPreference(prefKey);
+ preference.setOnPreferenceChangeListener(
+ (Preference.OnPreferenceChangeListener) this);
+ }
+ } else {
+ setVisible(screen, prefKey, false /* visible */);
+ }
+ }
- /**
- * Updates the current status of preference (summary, switch state, etc)
- */
- public void updateState(Preference preference) {
+ /**
+ * Updates the current status of preference (summary, switch state, etc)
+ */
+ public void updateState(Preference preference) {
- }
+ }
- /**
- * Returns true if preference is available (should be displayed)
- */
- public abstract boolean isAvailable();
+ /**
+ * Returns true if preference is available (should be displayed)
+ */
+ public abstract boolean isAvailable();
- /**
- * Handles preference tree click
- *
- * @param preference the preference being clicked
- * @return true if click is handled
- */
- public boolean handlePreferenceTreeClick(Preference preference) {
- return false;
- }
+ /**
+ * Handles preference tree click
+ *
+ * @param preference the preference being clicked
+ * @return true if click is handled
+ */
+ public boolean handlePreferenceTreeClick(Preference preference) {
+ return false;
+ }
- /**
- * Returns the key for this preference.
- */
- public abstract String getPreferenceKey();
+ /**
+ * Returns the key for this preference.
+ */
+ public abstract String getPreferenceKey();
- /**
- * Removes preference from screen.
- */
- protected final void removePreference(PreferenceScreen screen, String key) {
- findAndRemovePreference(screen, key);
- }
+ /**
+ * Show/hide a preference.
+ */
+ protected final void setVisible(PreferenceGroup group, String key, boolean isVisible) {
+ final Preference pref = group.findPreference(key);
+ if (pref != null) {
+ pref.setVisible(isVisible);
+ }
+ }
- // finds the preference recursively and removes it from its parent
- private boolean findAndRemovePreference(PreferenceGroup prefGroup, String key) {
- final int preferenceCount = prefGroup.getPreferenceCount();
- for (int i = 0; i < preferenceCount; i++) {
- final Preference preference = prefGroup.getPreference(i);
- final String curKey = preference.getKey();
-
- if (curKey != null && curKey.equals(key)) {
- return prefGroup.removePreference(preference);
- }
-
- if (preference instanceof PreferenceGroup) {
- if (findAndRemovePreference((PreferenceGroup) preference, key)) {
- return true;
- }
- }
- }
- return false;
- }
+ /**
+ * @return a String for the summary of the preference.
+ */
+ public String getSummary() {
+ return null;
+ }
}
diff --git a/packages/SettingsLib/src/com/android/settingslib/core/ConfirmationDialogController.java b/packages/SettingsLib/src/com/android/settingslib/core/ConfirmationDialogController.java
new file mode 100644
index 000000000000..72ab8c3848c5
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/core/ConfirmationDialogController.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.core;
+
+import android.support.annotation.Nullable;
+import android.support.v7.preference.Preference;
+
+/**
+ * Interface for {@link AbstractPreferenceController} objects which manage confirmation dialogs
+ */
+public interface ConfirmationDialogController {
+ /**
+ * Returns the key for this preference.
+ */
+ String getPreferenceKey();
+
+ /**
+ * Shows the dialog
+ * @param preference Preference object relevant to the dialog being shown
+ */
+ void showConfirmationDialog(@Nullable Preference preference);
+
+ /**
+ * Dismiss the dialog managed by this object
+ */
+ void dismissConfirmationDialog();
+
+ /**
+ * @return {@code true} if the dialog is showing
+ */
+ boolean isConfirmationDialogShowing();
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/core/instrumentation/EventLogWriter.java b/packages/SettingsLib/src/com/android/settingslib/core/instrumentation/EventLogWriter.java
new file mode 100644
index 000000000000..72273046ef29
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/core/instrumentation/EventLogWriter.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.core.instrumentation;
+
+import android.content.Context;
+import android.metrics.LogMaker;
+import android.util.Log;
+import android.util.Pair;
+
+import com.android.internal.logging.MetricsLogger;
+import com.android.internal.logging.nano.MetricsProto;
+
+/**
+ * {@link LogWriter} that writes data to eventlog.
+ */
+public class EventLogWriter implements LogWriter {
+
+ private final MetricsLogger mMetricsLogger = new MetricsLogger();
+
+ public void visible(Context context, int source, int category) {
+ final LogMaker logMaker = new LogMaker(category)
+ .setType(MetricsProto.MetricsEvent.TYPE_OPEN)
+ .addTaggedData(MetricsProto.MetricsEvent.FIELD_CONTEXT, source);
+ MetricsLogger.action(logMaker);
+ }
+
+ public void hidden(Context context, int category) {
+ MetricsLogger.hidden(context, category);
+ }
+
+ public void action(int category, int value, Pair<Integer, Object>... taggedData) {
+ if (taggedData == null || taggedData.length == 0) {
+ mMetricsLogger.action(category, value);
+ } else {
+ final LogMaker logMaker = new LogMaker(category)
+ .setType(MetricsProto.MetricsEvent.TYPE_ACTION)
+ .setSubtype(value);
+ for (Pair<Integer, Object> pair : taggedData) {
+ logMaker.addTaggedData(pair.first, pair.second);
+ }
+ mMetricsLogger.write(logMaker);
+ }
+ }
+
+ public void action(int category, boolean value, Pair<Integer, Object>... taggedData) {
+ action(category, value ? 1 : 0, taggedData);
+ }
+
+ public void action(Context context, int category, Pair<Integer, Object>... taggedData) {
+ action(context, category, "", taggedData);
+ }
+
+ public void actionWithSource(Context context, int source, int category) {
+ final LogMaker logMaker = new LogMaker(category)
+ .setType(MetricsProto.MetricsEvent.TYPE_ACTION);
+ if (source != MetricsProto.MetricsEvent.VIEW_UNKNOWN) {
+ logMaker.addTaggedData(MetricsProto.MetricsEvent.FIELD_CONTEXT, source);
+ }
+ MetricsLogger.action(logMaker);
+ }
+
+ /** @deprecated use {@link #action(int, int, Pair[])} */
+ @Deprecated
+ public void action(Context context, int category, int value) {
+ MetricsLogger.action(context, category, value);
+ }
+
+ /** @deprecated use {@link #action(int, boolean, Pair[])} */
+ @Deprecated
+ public void action(Context context, int category, boolean value) {
+ MetricsLogger.action(context, category, value);
+ }
+
+ public void action(Context context, int category, String pkg,
+ Pair<Integer, Object>... taggedData) {
+ if (taggedData == null || taggedData.length == 0) {
+ MetricsLogger.action(context, category, pkg);
+ } else {
+ final LogMaker logMaker = new LogMaker(category)
+ .setType(MetricsProto.MetricsEvent.TYPE_ACTION)
+ .setPackageName(pkg);
+ for (Pair<Integer, Object> pair : taggedData) {
+ logMaker.addTaggedData(pair.first, pair.second);
+ }
+ MetricsLogger.action(logMaker);
+ }
+ }
+
+ public void count(Context context, String name, int value) {
+ MetricsLogger.count(context, name, value);
+ }
+
+ public void histogram(Context context, String name, int bucket) {
+ MetricsLogger.histogram(context, name, bucket);
+ }
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/core/instrumentation/Instrumentable.java b/packages/SettingsLib/src/com/android/settingslib/core/instrumentation/Instrumentable.java
new file mode 100644
index 000000000000..dbc61c26e82e
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/core/instrumentation/Instrumentable.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.core.instrumentation;
+
+public interface Instrumentable {
+
+ int METRICS_CATEGORY_UNKNOWN = 0;
+
+ /**
+ * Instrumented name for a view as defined in
+ * {@link com.android.internal.logging.nano.MetricsProto.MetricsEvent}.
+ */
+ int getMetricsCategory();
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/core/instrumentation/LogWriter.java b/packages/SettingsLib/src/com/android/settingslib/core/instrumentation/LogWriter.java
new file mode 100644
index 000000000000..4b9f5727208d
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/core/instrumentation/LogWriter.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.settingslib.core.instrumentation;
+
+import android.content.Context;
+import android.util.Pair;
+
+/**
+ * Generic log writer interface.
+ */
+public interface LogWriter {
+
+ /**
+ * Logs a visibility event when view becomes visible.
+ */
+ void visible(Context context, int source, int category);
+
+ /**
+ * Logs a visibility event when view becomes hidden.
+ */
+ void hidden(Context context, int category);
+
+ /**
+ * Logs a user action.
+ */
+ void action(int category, int value, Pair<Integer, Object>... taggedData);
+
+ /**
+ * Logs a user action.
+ */
+ void action(int category, boolean value, Pair<Integer, Object>... taggedData);
+
+ /**
+ * Logs an user action.
+ */
+ void action(Context context, int category, Pair<Integer, Object>... taggedData);
+
+ /**
+ * Logs an user action.
+ */
+ void actionWithSource(Context context, int source, int category);
+
+ /**
+ * Logs an user action.
+ * @deprecated use {@link #action(int, int, Pair[])}
+ */
+ @Deprecated
+ void action(Context context, int category, int value);
+
+ /**
+ * Logs an user action.
+ * @deprecated use {@link #action(int, boolean, Pair[])}
+ */
+ @Deprecated
+ void action(Context context, int category, boolean value);
+
+ /**
+ * Logs an user action.
+ */
+ void action(Context context, int category, String pkg, Pair<Integer, Object>... taggedData);
+
+ /**
+ * Logs a count.
+ */
+ void count(Context context, String name, int value);
+
+ /**
+ * Logs a histogram event.
+ */
+ void histogram(Context context, String name, int bucket);
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/core/instrumentation/MetricsFeatureProvider.java b/packages/SettingsLib/src/com/android/settingslib/core/instrumentation/MetricsFeatureProvider.java
new file mode 100644
index 000000000000..1e5b378e931c
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/core/instrumentation/MetricsFeatureProvider.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.settingslib.core.instrumentation;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.text.TextUtils;
+import android.util.Pair;
+
+import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * FeatureProvider for metrics.
+ */
+public class MetricsFeatureProvider {
+ private List<LogWriter> mLoggerWriters;
+
+ public MetricsFeatureProvider() {
+ mLoggerWriters = new ArrayList<>();
+ installLogWriters();
+ }
+
+ protected void installLogWriters() {
+ mLoggerWriters.add(new EventLogWriter());
+ }
+
+ public void visible(Context context, int source, int category) {
+ for (LogWriter writer : mLoggerWriters) {
+ writer.visible(context, source, category);
+ }
+ }
+
+ public void hidden(Context context, int category) {
+ for (LogWriter writer : mLoggerWriters) {
+ writer.hidden(context, category);
+ }
+ }
+
+ public void actionWithSource(Context context, int source, int category) {
+ for (LogWriter writer : mLoggerWriters) {
+ writer.actionWithSource(context, source, category);
+ }
+ }
+
+ /**
+ * Logs a user action. Includes the elapsed time since the containing
+ * fragment has been visible.
+ */
+ public void action(VisibilityLoggerMixin visibilityLogger, int category, int value) {
+ for (LogWriter writer : mLoggerWriters) {
+ writer.action(category, value,
+ sinceVisibleTaggedData(visibilityLogger.elapsedTimeSinceVisible()));
+ }
+ }
+
+ /**
+ * Logs a user action. Includes the elapsed time since the containing
+ * fragment has been visible.
+ */
+ public void action(VisibilityLoggerMixin visibilityLogger, int category, boolean value) {
+ for (LogWriter writer : mLoggerWriters) {
+ writer.action(category, value,
+ sinceVisibleTaggedData(visibilityLogger.elapsedTimeSinceVisible()));
+ }
+ }
+
+ public void action(Context context, int category, Pair<Integer, Object>... taggedData) {
+ for (LogWriter writer : mLoggerWriters) {
+ writer.action(context, category, taggedData);
+ }
+ }
+
+ /** @deprecated use {@link #action(VisibilityLoggerMixin, int, int)} */
+ @Deprecated
+ public void action(Context context, int category, int value) {
+ for (LogWriter writer : mLoggerWriters) {
+ writer.action(context, category, value);
+ }
+ }
+
+ /** @deprecated use {@link #action(VisibilityLoggerMixin, int, boolean)} */
+ @Deprecated
+ public void action(Context context, int category, boolean value) {
+ for (LogWriter writer : mLoggerWriters) {
+ writer.action(context, category, value);
+ }
+ }
+
+ public void action(Context context, int category, String pkg,
+ Pair<Integer, Object>... taggedData) {
+ for (LogWriter writer : mLoggerWriters) {
+ writer.action(context, category, pkg, taggedData);
+ }
+ }
+
+ public void count(Context context, String name, int value) {
+ for (LogWriter writer : mLoggerWriters) {
+ writer.count(context, name, value);
+ }
+ }
+
+ public void histogram(Context context, String name, int bucket) {
+ for (LogWriter writer : mLoggerWriters) {
+ writer.histogram(context, name, bucket);
+ }
+ }
+
+ public int getMetricsCategory(Object object) {
+ if (object == null || !(object instanceof Instrumentable)) {
+ return MetricsEvent.VIEW_UNKNOWN;
+ }
+ return ((Instrumentable) object).getMetricsCategory();
+ }
+
+ public void logDashboardStartIntent(Context context, Intent intent,
+ int sourceMetricsCategory) {
+ if (intent == null) {
+ return;
+ }
+ final ComponentName cn = intent.getComponent();
+ if (cn == null) {
+ final String action = intent.getAction();
+ if (TextUtils.isEmpty(action)) {
+ // Not loggable
+ return;
+ }
+ action(context, MetricsEvent.ACTION_SETTINGS_TILE_CLICK, action,
+ Pair.create(MetricsEvent.FIELD_CONTEXT, sourceMetricsCategory));
+ return;
+ } else if (TextUtils.equals(cn.getPackageName(), context.getPackageName())) {
+ // Going to a Setting internal page, skip click logging in favor of page's own
+ // visibility logging.
+ return;
+ }
+ action(context, MetricsEvent.ACTION_SETTINGS_TILE_CLICK, cn.flattenToString(),
+ Pair.create(MetricsEvent.FIELD_CONTEXT, sourceMetricsCategory));
+ }
+
+ private Pair<Integer, Object> sinceVisibleTaggedData(long timestamp) {
+ return Pair.create(MetricsEvent.NOTIFICATION_SINCE_VISIBLE_MILLIS, timestamp);
+ }
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/core/instrumentation/SharedPreferencesLogger.java b/packages/SettingsLib/src/com/android/settingslib/core/instrumentation/SharedPreferencesLogger.java
new file mode 100644
index 000000000000..facce4e0bcbb
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/core/instrumentation/SharedPreferencesLogger.java
@@ -0,0 +1,259 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package com.android.settingslib.core.instrumentation;
+
+import android.annotation.Nullable;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.pm.PackageManager;
+import android.os.AsyncTask;
+import android.support.annotation.VisibleForTesting;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.Pair;
+
+import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentSkipListSet;
+
+public class SharedPreferencesLogger implements SharedPreferences {
+
+ private static final String LOG_TAG = "SharedPreferencesLogger";
+
+ private final String mTag;
+ private final Context mContext;
+ private final MetricsFeatureProvider mMetricsFeature;
+ private final Set<String> mPreferenceKeySet;
+
+ public SharedPreferencesLogger(Context context, String tag,
+ MetricsFeatureProvider metricsFeature) {
+ mContext = context;
+ mTag = tag;
+ mMetricsFeature = metricsFeature;
+ mPreferenceKeySet = new ConcurrentSkipListSet<>();
+ }
+
+ @Override
+ public Map<String, ?> getAll() {
+ return null;
+ }
+
+ @Override
+ public String getString(String key, @Nullable String defValue) {
+ return defValue;
+ }
+
+ @Override
+ public Set<String> getStringSet(String key, @Nullable Set<String> defValues) {
+ return defValues;
+ }
+
+ @Override
+ public int getInt(String key, int defValue) {
+ return defValue;
+ }
+
+ @Override
+ public long getLong(String key, long defValue) {
+ return defValue;
+ }
+
+ @Override
+ public float getFloat(String key, float defValue) {
+ return defValue;
+ }
+
+ @Override
+ public boolean getBoolean(String key, boolean defValue) {
+ return defValue;
+ }
+
+ @Override
+ public boolean contains(String key) {
+ return false;
+ }
+
+ @Override
+ public Editor edit() {
+ return new EditorLogger();
+ }
+
+ @Override
+ public void registerOnSharedPreferenceChangeListener(
+ OnSharedPreferenceChangeListener listener) {
+ }
+
+ @Override
+ public void unregisterOnSharedPreferenceChangeListener(
+ OnSharedPreferenceChangeListener listener) {
+ }
+
+ private void logValue(String key, Object value) {
+ logValue(key, value, false /* forceLog */);
+ }
+
+ private void logValue(String key, Object value, boolean forceLog) {
+ final String prefKey = buildPrefKey(mTag, key);
+ if (!forceLog && !mPreferenceKeySet.contains(prefKey)) {
+ // Pref key doesn't exist in set, this is initial display so we skip metrics but
+ // keeps track of this key.
+ mPreferenceKeySet.add(prefKey);
+ return;
+ }
+ // TODO: Remove count logging to save some resource.
+ mMetricsFeature.count(mContext, buildCountName(prefKey, value), 1);
+
+ final Pair<Integer, Object> valueData;
+ if (value instanceof Long) {
+ final Long longVal = (Long) value;
+ final int intVal;
+ if (longVal > Integer.MAX_VALUE) {
+ intVal = Integer.MAX_VALUE;
+ } else if (longVal < Integer.MIN_VALUE) {
+ intVal = Integer.MIN_VALUE;
+ } else {
+ intVal = longVal.intValue();
+ }
+ valueData = Pair.create(MetricsEvent.FIELD_SETTINGS_PREFERENCE_CHANGE_INT_VALUE,
+ intVal);
+ } else if (value instanceof Integer) {
+ valueData = Pair.create(MetricsEvent.FIELD_SETTINGS_PREFERENCE_CHANGE_INT_VALUE,
+ value);
+ } else if (value instanceof Boolean) {
+ valueData = Pair.create(MetricsEvent.FIELD_SETTINGS_PREFERENCE_CHANGE_INT_VALUE,
+ (Boolean) value ? 1 : 0);
+ } else if (value instanceof Float) {
+ valueData = Pair.create(MetricsEvent.FIELD_SETTINGS_PREFERENCE_CHANGE_FLOAT_VALUE,
+ value);
+ } else if (value instanceof String) {
+ Log.d(LOG_TAG, "Tried to log string preference " + prefKey + " = " + value);
+ valueData = null;
+ } else {
+ Log.w(LOG_TAG, "Tried to log unloggable object" + value);
+ valueData = null;
+ }
+ if (valueData != null) {
+ // Pref key exists in set, log it's change in metrics.
+ mMetricsFeature.action(mContext, MetricsEvent.ACTION_SETTINGS_PREFERENCE_CHANGE,
+ Pair.create(MetricsEvent.FIELD_SETTINGS_PREFERENCE_CHANGE_NAME, prefKey),
+ valueData);
+ }
+ }
+
+ @VisibleForTesting
+ void logPackageName(String key, String value) {
+ final String prefKey = mTag + "/" + key;
+ mMetricsFeature.action(mContext, MetricsEvent.ACTION_SETTINGS_PREFERENCE_CHANGE, value,
+ Pair.create(MetricsEvent.FIELD_SETTINGS_PREFERENCE_CHANGE_NAME, prefKey));
+ }
+
+ private void safeLogValue(String key, String value) {
+ new AsyncPackageCheck().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, key, value);
+ }
+
+ public static String buildCountName(String prefKey, Object value) {
+ return prefKey + "|" + value;
+ }
+
+ public static String buildPrefKey(String tag, String key) {
+ return tag + "/" + key;
+ }
+
+ private class AsyncPackageCheck extends AsyncTask<String, Void, Void> {
+ @Override
+ protected Void doInBackground(String... params) {
+ String key = params[0];
+ String value = params[1];
+ PackageManager pm = mContext.getPackageManager();
+ try {
+ // Check if this might be a component.
+ ComponentName name = ComponentName.unflattenFromString(value);
+ if (value != null) {
+ value = name.getPackageName();
+ }
+ } catch (Exception e) {
+ }
+ try {
+ pm.getPackageInfo(value, PackageManager.MATCH_ANY_USER);
+ logPackageName(key, value);
+ } catch (PackageManager.NameNotFoundException e) {
+ // Clearly not a package, and it's unlikely this preference is in prefSet, so
+ // lets force log it.
+ logValue(key, value, true /* forceLog */);
+ }
+ return null;
+ }
+ }
+
+ public class EditorLogger implements Editor {
+ @Override
+ public Editor putString(String key, @Nullable String value) {
+ safeLogValue(key, value);
+ return this;
+ }
+
+ @Override
+ public Editor putStringSet(String key, @Nullable Set<String> values) {
+ safeLogValue(key, TextUtils.join(",", values));
+ return this;
+ }
+
+ @Override
+ public Editor putInt(String key, int value) {
+ logValue(key, value);
+ return this;
+ }
+
+ @Override
+ public Editor putLong(String key, long value) {
+ logValue(key, value);
+ return this;
+ }
+
+ @Override
+ public Editor putFloat(String key, float value) {
+ logValue(key, value);
+ return this;
+ }
+
+ @Override
+ public Editor putBoolean(String key, boolean value) {
+ logValue(key, value);
+ return this;
+ }
+
+ @Override
+ public Editor remove(String key) {
+ return this;
+ }
+
+ @Override
+ public Editor clear() {
+ return this;
+ }
+
+ @Override
+ public boolean commit() {
+ return true;
+ }
+
+ @Override
+ public void apply() {
+ }
+ }
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/core/instrumentation/VisibilityLoggerMixin.java b/packages/SettingsLib/src/com/android/settingslib/core/instrumentation/VisibilityLoggerMixin.java
new file mode 100644
index 000000000000..c23f22648764
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/core/instrumentation/VisibilityLoggerMixin.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.core.instrumentation;
+
+import android.app.Activity;
+import android.arch.lifecycle.Lifecycle.Event;
+import android.arch.lifecycle.LifecycleObserver;
+import android.arch.lifecycle.OnLifecycleEvent;
+import android.content.Intent;
+
+import android.os.SystemClock;
+import com.android.internal.logging.nano.MetricsProto;
+
+import static com.android.settingslib.core.instrumentation.Instrumentable.METRICS_CATEGORY_UNKNOWN;
+
+/**
+ * Logs visibility change of a fragment.
+ */
+public class VisibilityLoggerMixin implements LifecycleObserver {
+
+ private static final String TAG = "VisibilityLoggerMixin";
+
+ private final int mMetricsCategory;
+
+ private MetricsFeatureProvider mMetricsFeature;
+ private int mSourceMetricsCategory = MetricsProto.MetricsEvent.VIEW_UNKNOWN;
+ private long mVisibleTimestamp;
+
+ /**
+ * The metrics category constant for logging source when a setting fragment is opened.
+ */
+ public static final String EXTRA_SOURCE_METRICS_CATEGORY = ":settings:source_metrics";
+
+ private VisibilityLoggerMixin() {
+ mMetricsCategory = METRICS_CATEGORY_UNKNOWN;
+ }
+
+ public VisibilityLoggerMixin(int metricsCategory, MetricsFeatureProvider metricsFeature) {
+ mMetricsCategory = metricsCategory;
+ mMetricsFeature = metricsFeature;
+ }
+
+ @OnLifecycleEvent(Event.ON_RESUME)
+ public void onResume() {
+ mVisibleTimestamp = SystemClock.elapsedRealtime();
+ if (mMetricsFeature != null && mMetricsCategory != METRICS_CATEGORY_UNKNOWN) {
+ mMetricsFeature.visible(null /* context */, mSourceMetricsCategory, mMetricsCategory);
+ }
+ }
+
+ @OnLifecycleEvent(Event.ON_PAUSE)
+ public void onPause() {
+ mVisibleTimestamp = 0;
+ if (mMetricsFeature != null && mMetricsCategory != METRICS_CATEGORY_UNKNOWN) {
+ mMetricsFeature.hidden(null /* context */, mMetricsCategory);
+ }
+ }
+
+ /**
+ * Sets source metrics category for this logger. Source is the caller that opened this UI.
+ */
+ public void setSourceMetricsCategory(Activity activity) {
+ if (mSourceMetricsCategory != MetricsProto.MetricsEvent.VIEW_UNKNOWN || activity == null) {
+ return;
+ }
+ final Intent intent = activity.getIntent();
+ if (intent == null) {
+ return;
+ }
+ mSourceMetricsCategory = intent.getIntExtra(EXTRA_SOURCE_METRICS_CATEGORY,
+ MetricsProto.MetricsEvent.VIEW_UNKNOWN);
+ }
+
+ /** Returns elapsed time since onResume() */
+ public long elapsedTimeSinceVisible() {
+ if (mVisibleTimestamp == 0) {
+ return 0;
+ }
+ return SystemClock.elapsedRealtime() - mVisibleTimestamp;
+ }
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/core/lifecycle/Lifecycle.java b/packages/SettingsLib/src/com/android/settingslib/core/lifecycle/Lifecycle.java
index b2351a9ee0e4..451e5611979a 100644
--- a/packages/SettingsLib/src/com/android/settingslib/core/lifecycle/Lifecycle.java
+++ b/packages/SettingsLib/src/com/android/settingslib/core/lifecycle/Lifecycle.java
@@ -15,11 +15,18 @@
*/
package com.android.settingslib.core.lifecycle;
+import static android.arch.lifecycle.Lifecycle.Event.ON_ANY;
+
import android.annotation.UiThread;
+import android.arch.lifecycle.LifecycleOwner;
+import android.arch.lifecycle.LifecycleRegistry;
+import android.arch.lifecycle.OnLifecycleEvent;
import android.content.Context;
import android.os.Bundle;
+import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v7.preference.PreferenceScreen;
+import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
@@ -44,18 +51,46 @@ import java.util.List;
/**
* Dispatcher for lifecycle events.
*/
-public class Lifecycle {
+public class Lifecycle extends LifecycleRegistry {
+ private static final String TAG = "LifecycleObserver";
+
+ private final List<LifecycleObserver> mObservers = new ArrayList<>();
+ private final LifecycleProxy mProxy = new LifecycleProxy();
- protected final List<LifecycleObserver> mObservers = new ArrayList<>();
+ /**
+ * Creates a new LifecycleRegistry for the given provider.
+ * <p>
+ * You should usually create this inside your LifecycleOwner class's constructor and hold
+ * onto the same instance.
+ *
+ * @param provider The owner LifecycleOwner
+ */
+ public Lifecycle(@NonNull LifecycleOwner provider) {
+ super(provider);
+ addObserver(mProxy);
+ }
/**
* Registers a new observer of lifecycle events.
*/
@UiThread
- public <T extends LifecycleObserver> T addObserver(T observer) {
+ @Override
+ public void addObserver(android.arch.lifecycle.LifecycleObserver observer) {
ThreadUtils.ensureMainThread();
- mObservers.add(observer);
- return observer;
+ super.addObserver(observer);
+ if (observer instanceof LifecycleObserver) {
+ mObservers.add((LifecycleObserver) observer);
+ }
+ }
+
+ @UiThread
+ @Override
+ public void removeObserver(android.arch.lifecycle.LifecycleObserver observer) {
+ ThreadUtils.ensureMainThread();
+ super.removeObserver(observer);
+ if (observer instanceof LifecycleObserver) {
+ mObservers.remove(observer);
+ }
}
public void onAttach(Context context) {
@@ -67,6 +102,8 @@ public class Lifecycle {
}
}
+ // This method is not called from the proxy because it does not have access to the
+ // savedInstanceState
public void onCreate(Bundle savedInstanceState) {
for (int i = 0, size = mObservers.size(); i < size; i++) {
final LifecycleObserver observer = mObservers.get(i);
@@ -76,7 +113,7 @@ public class Lifecycle {
}
}
- public void onStart() {
+ private void onStart() {
for (int i = 0, size = mObservers.size(); i < size; i++) {
final LifecycleObserver observer = mObservers.get(i);
if (observer instanceof OnStart) {
@@ -94,7 +131,7 @@ public class Lifecycle {
}
}
- public void onResume() {
+ private void onResume() {
for (int i = 0, size = mObservers.size(); i < size; i++) {
final LifecycleObserver observer = mObservers.get(i);
if (observer instanceof OnResume) {
@@ -103,7 +140,7 @@ public class Lifecycle {
}
}
- public void onPause() {
+ private void onPause() {
for (int i = 0, size = mObservers.size(); i < size; i++) {
final LifecycleObserver observer = mObservers.get(i);
if (observer instanceof OnPause) {
@@ -121,7 +158,7 @@ public class Lifecycle {
}
}
- public void onStop() {
+ private void onStop() {
for (int i = 0, size = mObservers.size(); i < size; i++) {
final LifecycleObserver observer = mObservers.get(i);
if (observer instanceof OnStop) {
@@ -130,7 +167,7 @@ public class Lifecycle {
}
}
- public void onDestroy() {
+ private void onDestroy() {
for (int i = 0, size = mObservers.size(); i < size; i++) {
final LifecycleObserver observer = mObservers.get(i);
if (observer instanceof OnDestroy) {
@@ -168,4 +205,34 @@ public class Lifecycle {
}
return false;
}
+
+ private class LifecycleProxy
+ implements android.arch.lifecycle.LifecycleObserver {
+ @OnLifecycleEvent(ON_ANY)
+ public void onLifecycleEvent(LifecycleOwner owner, Event event) {
+ switch (event) {
+ case ON_CREATE:
+ // onCreate is called directly since we don't have savedInstanceState here
+ break;
+ case ON_START:
+ onStart();
+ break;
+ case ON_RESUME:
+ onResume();
+ break;
+ case ON_PAUSE:
+ onPause();
+ break;
+ case ON_STOP:
+ onStop();
+ break;
+ case ON_DESTROY:
+ onDestroy();
+ break;
+ case ON_ANY:
+ Log.wtf(TAG, "Should not receive an 'ANY' event!");
+ break;
+ }
+ }
+ }
}
diff --git a/packages/SettingsLib/src/com/android/settingslib/core/lifecycle/LifecycleObserver.java b/packages/SettingsLib/src/com/android/settingslib/core/lifecycle/LifecycleObserver.java
index 6c4107290274..ec8a8b537f48 100644
--- a/packages/SettingsLib/src/com/android/settingslib/core/lifecycle/LifecycleObserver.java
+++ b/packages/SettingsLib/src/com/android/settingslib/core/lifecycle/LifecycleObserver.java
@@ -17,6 +17,9 @@ package com.android.settingslib.core.lifecycle;
/**
* Observer of lifecycle events.
+ * @deprecated use {@link android.arch.lifecycle.LifecycleObserver} instead
*/
-public interface LifecycleObserver {
+@Deprecated
+public interface LifecycleObserver extends
+ android.arch.lifecycle.LifecycleObserver {
}
diff --git a/packages/SettingsLib/src/com/android/settingslib/core/lifecycle/ObservableActivity.java b/packages/SettingsLib/src/com/android/settingslib/core/lifecycle/ObservableActivity.java
index 727bec75c204..8b062f8447b0 100644
--- a/packages/SettingsLib/src/com/android/settingslib/core/lifecycle/ObservableActivity.java
+++ b/packages/SettingsLib/src/com/android/settingslib/core/lifecycle/ObservableActivity.java
@@ -15,8 +15,16 @@
*/
package com.android.settingslib.core.lifecycle;
+import static android.arch.lifecycle.Lifecycle.Event.ON_CREATE;
+import static android.arch.lifecycle.Lifecycle.Event.ON_DESTROY;
+import static android.arch.lifecycle.Lifecycle.Event.ON_PAUSE;
+import static android.arch.lifecycle.Lifecycle.Event.ON_RESUME;
+import static android.arch.lifecycle.Lifecycle.Event.ON_START;
+import static android.arch.lifecycle.Lifecycle.Event.ON_STOP;
+
import android.annotation.Nullable;
import android.app.Activity;
+import android.arch.lifecycle.LifecycleOwner;
import android.os.Bundle;
import android.os.PersistableBundle;
import android.view.Menu;
@@ -25,17 +33,19 @@ import android.view.MenuItem;
/**
* {@link Activity} that has hooks to observe activity lifecycle events.
*/
-public class ObservableActivity extends Activity {
+public class ObservableActivity extends Activity implements LifecycleOwner {
- private final Lifecycle mLifecycle = new Lifecycle();
+ private final Lifecycle mLifecycle = new Lifecycle(this);
- protected Lifecycle getLifecycle() {
+ public Lifecycle getLifecycle() {
return mLifecycle;
}
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
mLifecycle.onAttach(this);
+ mLifecycle.onCreate(savedInstanceState);
+ mLifecycle.handleLifecycleEvent(ON_CREATE);
super.onCreate(savedInstanceState);
}
@@ -43,36 +53,38 @@ public class ObservableActivity extends Activity {
public void onCreate(@Nullable Bundle savedInstanceState,
@Nullable PersistableBundle persistentState) {
mLifecycle.onAttach(this);
+ mLifecycle.onCreate(savedInstanceState);
+ mLifecycle.handleLifecycleEvent(ON_CREATE);
super.onCreate(savedInstanceState, persistentState);
}
@Override
protected void onStart() {
- mLifecycle.onStart();
+ mLifecycle.handleLifecycleEvent(ON_START);
super.onStart();
}
@Override
protected void onResume() {
- mLifecycle.onResume();
+ mLifecycle.handleLifecycleEvent(ON_RESUME);
super.onResume();
}
@Override
protected void onPause() {
- mLifecycle.onPause();
+ mLifecycle.handleLifecycleEvent(ON_PAUSE);
super.onPause();
}
@Override
protected void onStop() {
- mLifecycle.onStop();
+ mLifecycle.handleLifecycleEvent(ON_STOP);
super.onStop();
}
@Override
protected void onDestroy() {
- mLifecycle.onDestroy();
+ mLifecycle.handleLifecycleEvent(ON_DESTROY);
super.onDestroy();
}
diff --git a/packages/SettingsLib/src/com/android/settingslib/core/lifecycle/ObservableDialogFragment.java b/packages/SettingsLib/src/com/android/settingslib/core/lifecycle/ObservableDialogFragment.java
index 315bedc168bb..dc95384b26a5 100644
--- a/packages/SettingsLib/src/com/android/settingslib/core/lifecycle/ObservableDialogFragment.java
+++ b/packages/SettingsLib/src/com/android/settingslib/core/lifecycle/ObservableDialogFragment.java
@@ -15,9 +15,17 @@
*/
package com.android.settingslib.core.lifecycle;
+import static android.arch.lifecycle.Lifecycle.Event.ON_CREATE;
+import static android.arch.lifecycle.Lifecycle.Event.ON_DESTROY;
+import static android.arch.lifecycle.Lifecycle.Event.ON_PAUSE;
+import static android.arch.lifecycle.Lifecycle.Event.ON_RESUME;
+import static android.arch.lifecycle.Lifecycle.Event.ON_START;
+import static android.arch.lifecycle.Lifecycle.Event.ON_STOP;
+
import android.app.DialogFragment;
+import android.arch.lifecycle.LifecycleOwner;
import android.content.Context;
-import android.support.annotation.VisibleForTesting;
+import android.os.Bundle;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
@@ -25,9 +33,9 @@ import android.view.MenuItem;
/**
* {@link DialogFragment} that has hooks to observe fragment lifecycle events.
*/
-public class ObservableDialogFragment extends DialogFragment {
+public class ObservableDialogFragment extends DialogFragment implements LifecycleOwner {
- protected final Lifecycle mLifecycle = createLifecycle();
+ protected final Lifecycle mLifecycle = new Lifecycle(this);
@Override
public void onAttach(Context context) {
@@ -36,32 +44,39 @@ public class ObservableDialogFragment extends DialogFragment {
}
@Override
+ public void onCreate(Bundle savedInstanceState) {
+ mLifecycle.onCreate(savedInstanceState);
+ mLifecycle.handleLifecycleEvent(ON_CREATE);
+ super.onCreate(savedInstanceState);
+ }
+
+ @Override
public void onStart() {
- mLifecycle.onStart();
+ mLifecycle.handleLifecycleEvent(ON_START);
super.onStart();
}
@Override
public void onResume() {
- mLifecycle.onResume();
+ mLifecycle.handleLifecycleEvent(ON_RESUME);
super.onResume();
}
@Override
public void onPause() {
- mLifecycle.onPause();
+ mLifecycle.handleLifecycleEvent(ON_PAUSE);
super.onPause();
}
@Override
public void onStop() {
- mLifecycle.onStop();
+ mLifecycle.handleLifecycleEvent(ON_STOP);
super.onStop();
}
@Override
public void onDestroy() {
- mLifecycle.onDestroy();
+ mLifecycle.handleLifecycleEvent(ON_DESTROY);
super.onDestroy();
}
@@ -86,9 +101,8 @@ public class ObservableDialogFragment extends DialogFragment {
return lifecycleHandled;
}
- @VisibleForTesting(otherwise = VisibleForTesting.NONE)
- /** @return a new lifecycle. */
- public static Lifecycle createLifecycle() {
- return new Lifecycle();
+ @Override
+ public Lifecycle getLifecycle() {
+ return mLifecycle;
}
}
diff --git a/packages/SettingsLib/src/com/android/settingslib/core/lifecycle/ObservableFragment.java b/packages/SettingsLib/src/com/android/settingslib/core/lifecycle/ObservableFragment.java
index 3a00eba664c4..925eda6ef9d5 100644
--- a/packages/SettingsLib/src/com/android/settingslib/core/lifecycle/ObservableFragment.java
+++ b/packages/SettingsLib/src/com/android/settingslib/core/lifecycle/ObservableFragment.java
@@ -16,19 +16,27 @@
package com.android.settingslib.core.lifecycle;
+import static android.arch.lifecycle.Lifecycle.Event.ON_CREATE;
+import static android.arch.lifecycle.Lifecycle.Event.ON_DESTROY;
+import static android.arch.lifecycle.Lifecycle.Event.ON_PAUSE;
+import static android.arch.lifecycle.Lifecycle.Event.ON_RESUME;
+import static android.arch.lifecycle.Lifecycle.Event.ON_START;
+import static android.arch.lifecycle.Lifecycle.Event.ON_STOP;
+
import android.annotation.CallSuper;
import android.app.Fragment;
+import android.arch.lifecycle.LifecycleOwner;
import android.content.Context;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
-public class ObservableFragment extends Fragment {
+public class ObservableFragment extends Fragment implements LifecycleOwner {
- private final Lifecycle mLifecycle = new Lifecycle();
+ private final Lifecycle mLifecycle = new Lifecycle(this);
- protected Lifecycle getLifecycle() {
+ public Lifecycle getLifecycle() {
return mLifecycle;
}
@@ -43,6 +51,7 @@ public class ObservableFragment extends Fragment {
@Override
public void onCreate(Bundle savedInstanceState) {
mLifecycle.onCreate(savedInstanceState);
+ mLifecycle.handleLifecycleEvent(ON_CREATE);
super.onCreate(savedInstanceState);
}
@@ -56,35 +65,35 @@ public class ObservableFragment extends Fragment {
@CallSuper
@Override
public void onStart() {
- mLifecycle.onStart();
+ mLifecycle.handleLifecycleEvent(ON_START);
super.onStart();
}
@CallSuper
@Override
- public void onStop() {
- mLifecycle.onStop();
- super.onStop();
- }
-
- @CallSuper
- @Override
public void onResume() {
- mLifecycle.onResume();
+ mLifecycle.handleLifecycleEvent(ON_RESUME);
super.onResume();
}
@CallSuper
@Override
public void onPause() {
- mLifecycle.onPause();
+ mLifecycle.handleLifecycleEvent(ON_PAUSE);
super.onPause();
}
@CallSuper
@Override
+ public void onStop() {
+ mLifecycle.handleLifecycleEvent(ON_STOP);
+ super.onStop();
+ }
+
+ @CallSuper
+ @Override
public void onDestroy() {
- mLifecycle.onDestroy();
+ mLifecycle.handleLifecycleEvent(ON_DESTROY);
super.onDestroy();
}
diff --git a/packages/SettingsLib/src/com/android/settingslib/core/lifecycle/ObservablePreferenceFragment.java b/packages/SettingsLib/src/com/android/settingslib/core/lifecycle/ObservablePreferenceFragment.java
index 76e5c8579f05..abd77559612e 100644
--- a/packages/SettingsLib/src/com/android/settingslib/core/lifecycle/ObservablePreferenceFragment.java
+++ b/packages/SettingsLib/src/com/android/settingslib/core/lifecycle/ObservablePreferenceFragment.java
@@ -16,7 +16,15 @@
package com.android.settingslib.core.lifecycle;
+import static android.arch.lifecycle.Lifecycle.Event.ON_CREATE;
+import static android.arch.lifecycle.Lifecycle.Event.ON_DESTROY;
+import static android.arch.lifecycle.Lifecycle.Event.ON_PAUSE;
+import static android.arch.lifecycle.Lifecycle.Event.ON_RESUME;
+import static android.arch.lifecycle.Lifecycle.Event.ON_START;
+import static android.arch.lifecycle.Lifecycle.Event.ON_STOP;
+
import android.annotation.CallSuper;
+import android.arch.lifecycle.LifecycleOwner;
import android.content.Context;
import android.os.Bundle;
import android.support.v14.preference.PreferenceFragment;
@@ -28,11 +36,12 @@ import android.view.MenuItem;
/**
* {@link PreferenceFragment} that has hooks to observe fragment lifecycle events.
*/
-public abstract class ObservablePreferenceFragment extends PreferenceFragment {
+public abstract class ObservablePreferenceFragment extends PreferenceFragment
+ implements LifecycleOwner {
- private final Lifecycle mLifecycle = new Lifecycle();
+ private final Lifecycle mLifecycle = new Lifecycle(this);
- protected Lifecycle getLifecycle() {
+ public Lifecycle getLifecycle() {
return mLifecycle;
}
@@ -47,6 +56,7 @@ public abstract class ObservablePreferenceFragment extends PreferenceFragment {
@Override
public void onCreate(Bundle savedInstanceState) {
mLifecycle.onCreate(savedInstanceState);
+ mLifecycle.handleLifecycleEvent(ON_CREATE);
super.onCreate(savedInstanceState);
}
@@ -66,35 +76,35 @@ public abstract class ObservablePreferenceFragment extends PreferenceFragment {
@CallSuper
@Override
public void onStart() {
- mLifecycle.onStart();
+ mLifecycle.handleLifecycleEvent(ON_START);
super.onStart();
}
@CallSuper
@Override
- public void onStop() {
- mLifecycle.onStop();
- super.onStop();
- }
-
- @CallSuper
- @Override
public void onResume() {
- mLifecycle.onResume();
+ mLifecycle.handleLifecycleEvent(ON_RESUME);
super.onResume();
}
@CallSuper
@Override
public void onPause() {
- mLifecycle.onPause();
+ mLifecycle.handleLifecycleEvent(ON_PAUSE);
super.onPause();
}
@CallSuper
@Override
+ public void onStop() {
+ mLifecycle.handleLifecycleEvent(ON_STOP);
+ super.onStop();
+ }
+
+ @CallSuper
+ @Override
public void onDestroy() {
- mLifecycle.onDestroy();
+ mLifecycle.handleLifecycleEvent(ON_DESTROY);
super.onDestroy();
}
diff --git a/packages/SettingsLib/src/com/android/settingslib/core/lifecycle/events/OnAttach.java b/packages/SettingsLib/src/com/android/settingslib/core/lifecycle/events/OnAttach.java
index 152cbac3ad3d..e28c38736639 100644
--- a/packages/SettingsLib/src/com/android/settingslib/core/lifecycle/events/OnAttach.java
+++ b/packages/SettingsLib/src/com/android/settingslib/core/lifecycle/events/OnAttach.java
@@ -17,6 +17,10 @@ package com.android.settingslib.core.lifecycle.events;
import android.content.Context;
+/**
+ * @deprecated pass {@link Context} in constructor instead
+ */
+@Deprecated
public interface OnAttach {
void onAttach(Context context);
}
diff --git a/packages/SettingsLib/src/com/android/settingslib/core/lifecycle/events/OnCreate.java b/packages/SettingsLib/src/com/android/settingslib/core/lifecycle/events/OnCreate.java
index 44cbf8d26757..ad7068e2f9fb 100644
--- a/packages/SettingsLib/src/com/android/settingslib/core/lifecycle/events/OnCreate.java
+++ b/packages/SettingsLib/src/com/android/settingslib/core/lifecycle/events/OnCreate.java
@@ -16,8 +16,14 @@
package com.android.settingslib.core.lifecycle.events;
+import android.arch.lifecycle.Lifecycle;
+import android.arch.lifecycle.OnLifecycleEvent;
import android.os.Bundle;
+/**
+ * @deprecated use {@link OnLifecycleEvent(Lifecycle.Event) }
+ */
+@Deprecated
public interface OnCreate {
void onCreate(Bundle savedInstanceState);
}
diff --git a/packages/SettingsLib/src/com/android/settingslib/core/lifecycle/events/OnDestroy.java b/packages/SettingsLib/src/com/android/settingslib/core/lifecycle/events/OnDestroy.java
index ffa3d168b904..c37286e1efaa 100644
--- a/packages/SettingsLib/src/com/android/settingslib/core/lifecycle/events/OnDestroy.java
+++ b/packages/SettingsLib/src/com/android/settingslib/core/lifecycle/events/OnDestroy.java
@@ -15,6 +15,13 @@
*/
package com.android.settingslib.core.lifecycle.events;
+import android.arch.lifecycle.Lifecycle;
+import android.arch.lifecycle.OnLifecycleEvent;
+
+/**
+ * @deprecated use {@link OnLifecycleEvent(Lifecycle.Event) }
+ */
+@Deprecated
public interface OnDestroy {
void onDestroy();
}
diff --git a/packages/SettingsLib/src/com/android/settingslib/core/lifecycle/events/OnPause.java b/packages/SettingsLib/src/com/android/settingslib/core/lifecycle/events/OnPause.java
index 4a711058df21..a5ab39c46ab1 100644
--- a/packages/SettingsLib/src/com/android/settingslib/core/lifecycle/events/OnPause.java
+++ b/packages/SettingsLib/src/com/android/settingslib/core/lifecycle/events/OnPause.java
@@ -15,6 +15,13 @@
*/
package com.android.settingslib.core.lifecycle.events;
+import android.arch.lifecycle.Lifecycle;
+import android.arch.lifecycle.OnLifecycleEvent;
+
+/**
+ * @deprecated use {@link OnLifecycleEvent(Lifecycle.Event) }
+ */
+@Deprecated
public interface OnPause {
void onPause();
}
diff --git a/packages/SettingsLib/src/com/android/settingslib/core/lifecycle/events/OnResume.java b/packages/SettingsLib/src/com/android/settingslib/core/lifecycle/events/OnResume.java
index 8dd24e98401d..1effba4a750d 100644
--- a/packages/SettingsLib/src/com/android/settingslib/core/lifecycle/events/OnResume.java
+++ b/packages/SettingsLib/src/com/android/settingslib/core/lifecycle/events/OnResume.java
@@ -15,6 +15,13 @@
*/
package com.android.settingslib.core.lifecycle.events;
+import android.arch.lifecycle.Lifecycle;
+import android.arch.lifecycle.OnLifecycleEvent;
+
+/**
+ * @deprecated use {@link OnLifecycleEvent(Lifecycle.Event)}
+ */
+@Deprecated
public interface OnResume {
void onResume();
}
diff --git a/packages/SettingsLib/src/com/android/settingslib/core/lifecycle/events/OnStart.java b/packages/SettingsLib/src/com/android/settingslib/core/lifecycle/events/OnStart.java
index c88ddaa1d8de..07b8460367c1 100644
--- a/packages/SettingsLib/src/com/android/settingslib/core/lifecycle/events/OnStart.java
+++ b/packages/SettingsLib/src/com/android/settingslib/core/lifecycle/events/OnStart.java
@@ -15,7 +15,13 @@
*/
package com.android.settingslib.core.lifecycle.events;
-public interface OnStart {
+import android.arch.lifecycle.Lifecycle;
+import android.arch.lifecycle.OnLifecycleEvent;
+/**
+ * @deprecated use {@link OnLifecycleEvent(Lifecycle.Event) }
+ */
+@Deprecated
+public interface OnStart {
void onStart();
}
diff --git a/packages/SettingsLib/src/com/android/settingslib/core/lifecycle/events/OnStop.java b/packages/SettingsLib/src/com/android/settingslib/core/lifecycle/events/OnStop.java
index 32f61d98e4ad..d6a5967116be 100644
--- a/packages/SettingsLib/src/com/android/settingslib/core/lifecycle/events/OnStop.java
+++ b/packages/SettingsLib/src/com/android/settingslib/core/lifecycle/events/OnStop.java
@@ -15,7 +15,13 @@
*/
package com.android.settingslib.core.lifecycle.events;
-public interface OnStop {
+import android.arch.lifecycle.Lifecycle;
+import android.arch.lifecycle.OnLifecycleEvent;
+/**
+ * @deprecated use {@link OnLifecycleEvent(Lifecycle.Event) }
+ */
+@Deprecated
+public interface OnStop {
void onStop();
}
diff --git a/packages/SettingsLib/src/com/android/settingslib/datetime/ZoneGetter.java b/packages/SettingsLib/src/com/android/settingslib/datetime/ZoneGetter.java
index 1771208398fd..974b2a4389e2 100644
--- a/packages/SettingsLib/src/com/android/settingslib/datetime/ZoneGetter.java
+++ b/packages/SettingsLib/src/com/android/settingslib/datetime/ZoneGetter.java
@@ -20,6 +20,7 @@ import android.content.Context;
import android.content.res.XmlResourceParser;
import android.icu.text.TimeZoneFormat;
import android.icu.text.TimeZoneNames;
+import android.support.annotation.VisibleForTesting;
import android.support.v4.text.BidiFormatter;
import android.support.v4.text.TextDirectionHeuristicsCompat;
import android.text.SpannableString;
@@ -32,6 +33,8 @@ import android.view.View;
import com.android.settingslib.R;
+import libcore.util.TimeZoneFinder;
+
import org.xmlpull.v1.XmlPullParserException;
import java.util.ArrayList;
@@ -268,7 +271,7 @@ public class ZoneGetter {
* @param now The current time, used to tell whether daylight savings is active.
* @return A CharSequence suitable for display as the offset label of {@code tz}.
*/
- private static CharSequence getGmtOffsetText(TimeZoneFormat tzFormatter, Locale locale,
+ public static CharSequence getGmtOffsetText(TimeZoneFormat tzFormatter, Locale locale,
TimeZone tz, Date now) {
final SpannableStringBuilder builder = new SpannableStringBuilder();
@@ -349,7 +352,8 @@ public class ZoneGetter {
return gmtText;
}
- private static final class ZoneGetterData {
+ @VisibleForTesting
+ public static final class ZoneGetterData {
public final String[] olsonIdsToDisplay;
public final CharSequence[] gmtOffsetTexts;
public final TimeZone[] timeZones;
@@ -376,10 +380,13 @@ public class ZoneGetter {
}
// Create a lookup of local zone IDs.
- localZoneIds = new HashSet<String>();
- for (String olsonId : libcore.icu.TimeZoneNames.forLocale(locale)) {
- localZoneIds.add(olsonId);
- }
+ final List<String> zoneIds = lookupTimeZoneIdsByCountry(locale.getCountry());
+ localZoneIds = new HashSet<>(zoneIds);
+ }
+
+ @VisibleForTesting
+ public List<String> lookupTimeZoneIdsByCountry(String country) {
+ return TimeZoneFinder.getInstance().lookupTimeZoneIdsByCountry(country);
}
}
}
diff --git a/packages/SettingsLib/src/com/android/settingslib/development/AbstractEnableAdbPreferenceController.java b/packages/SettingsLib/src/com/android/settingslib/development/AbstractEnableAdbPreferenceController.java
index 75b6696d923c..3c02f6a234f4 100644
--- a/packages/SettingsLib/src/com/android/settingslib/development/AbstractEnableAdbPreferenceController.java
+++ b/packages/SettingsLib/src/com/android/settingslib/development/AbstractEnableAdbPreferenceController.java
@@ -16,11 +16,13 @@
package com.android.settingslib.development;
+import android.app.ActivityManager;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.os.UserManager;
import android.provider.Settings;
+import android.support.annotation.VisibleForTesting;
import android.support.v14.preference.SwitchPreference;
import android.support.v4.content.LocalBroadcastManager;
import android.support.v7.preference.Preference;
@@ -28,15 +30,20 @@ import android.support.v7.preference.PreferenceScreen;
import android.support.v7.preference.TwoStatePreference;
import android.text.TextUtils;
-import com.android.settingslib.core.AbstractPreferenceController;
+import com.android.settingslib.core.ConfirmationDialogController;
-public abstract class AbstractEnableAdbPreferenceController extends AbstractPreferenceController {
+public abstract class AbstractEnableAdbPreferenceController extends
+ DeveloperOptionsPreferenceController implements ConfirmationDialogController {
private static final String KEY_ENABLE_ADB = "enable_adb";
public static final String ACTION_ENABLE_ADB_STATE_CHANGED =
"com.android.settingslib.development.AbstractEnableAdbController."
+ "ENABLE_ADB_STATE_CHANGED";
- private SwitchPreference mPreference;
+ public static final int ADB_SETTING_ON = 1;
+ public static final int ADB_SETTING_OFF = 0;
+
+
+ protected SwitchPreference mPreference;
public AbstractEnableAdbPreferenceController(Context context) {
super(context);
@@ -62,12 +69,13 @@ public abstract class AbstractEnableAdbPreferenceController extends AbstractPref
private boolean isAdbEnabled() {
final ContentResolver cr = mContext.getContentResolver();
- return Settings.Global.getInt(cr, Settings.Global.ADB_ENABLED, 0) != 0;
+ return Settings.Global.getInt(cr, Settings.Global.ADB_ENABLED, ADB_SETTING_OFF)
+ != ADB_SETTING_OFF;
}
@Override
public void updateState(Preference preference) {
- ((TwoStatePreference)preference).setChecked(isAdbEnabled());
+ ((TwoStatePreference) preference).setChecked(isAdbEnabled());
}
public void enablePreference(boolean enabled) {
@@ -89,9 +97,13 @@ public abstract class AbstractEnableAdbPreferenceController extends AbstractPref
@Override
public boolean handlePreferenceTreeClick(Preference preference) {
+ if (isUserAMonkey()) {
+ return false;
+ }
+
if (TextUtils.equals(KEY_ENABLE_ADB, preference.getKey())) {
if (!isAdbEnabled()) {
- showConfirmationDialog((SwitchPreference) preference);
+ showConfirmationDialog(preference);
} else {
writeAdbSetting(false);
}
@@ -103,14 +115,17 @@ public abstract class AbstractEnableAdbPreferenceController extends AbstractPref
protected void writeAdbSetting(boolean enabled) {
Settings.Global.putInt(mContext.getContentResolver(),
- Settings.Global.ADB_ENABLED, enabled ? 1 : 0);
+ Settings.Global.ADB_ENABLED, enabled ? ADB_SETTING_ON : ADB_SETTING_OFF);
notifyStateChanged();
}
- protected void notifyStateChanged() {
+ private void notifyStateChanged() {
LocalBroadcastManager.getInstance(mContext)
.sendBroadcast(new Intent(ACTION_ENABLE_ADB_STATE_CHANGED));
}
- public abstract void showConfirmationDialog(SwitchPreference preference);
+ @VisibleForTesting
+ boolean isUserAMonkey() {
+ return ActivityManager.isUserAMonkey();
+ }
}
diff --git a/packages/SettingsLib/src/com/android/settingslib/development/AbstractLogdSizePreferenceController.java b/packages/SettingsLib/src/com/android/settingslib/development/AbstractLogdSizePreferenceController.java
new file mode 100644
index 000000000000..f79be7eaddb1
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/development/AbstractLogdSizePreferenceController.java
@@ -0,0 +1,184 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.development;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.SystemProperties;
+import android.support.annotation.VisibleForTesting;
+import android.support.v4.content.LocalBroadcastManager;
+import android.support.v7.preference.ListPreference;
+import android.support.v7.preference.Preference;
+import android.support.v7.preference.PreferenceScreen;
+
+import com.android.settingslib.R;
+
+public abstract class AbstractLogdSizePreferenceController extends
+ DeveloperOptionsPreferenceController implements Preference.OnPreferenceChangeListener {
+ public static final String ACTION_LOGD_SIZE_UPDATED = "com.android.settingslib.development."
+ + "AbstractLogdSizePreferenceController.LOGD_SIZE_UPDATED";
+ public static final String EXTRA_CURRENT_LOGD_VALUE = "CURRENT_LOGD_VALUE";
+
+ @VisibleForTesting
+ static final String LOW_RAM_CONFIG_PROPERTY_KEY = "ro.config.low_ram";
+ private static final String SELECT_LOGD_SIZE_KEY = "select_logd_size";
+ @VisibleForTesting
+ static final String SELECT_LOGD_SIZE_PROPERTY = "persist.logd.size";
+ static final String SELECT_LOGD_TAG_PROPERTY = "persist.log.tag";
+ // Tricky, isLoggable only checks for first character, assumes silence
+ static final String SELECT_LOGD_TAG_SILENCE = "Settings";
+ @VisibleForTesting
+ static final String SELECT_LOGD_SNET_TAG_PROPERTY = "persist.log.tag.snet_event_log";
+ private static final String SELECT_LOGD_RUNTIME_SNET_TAG_PROPERTY = "log.tag.snet_event_log";
+ private static final String SELECT_LOGD_DEFAULT_SIZE_PROPERTY = "ro.logd.size";
+ @VisibleForTesting
+ static final String SELECT_LOGD_DEFAULT_SIZE_VALUE = "262144";
+ private static final String SELECT_LOGD_SVELTE_DEFAULT_SIZE_VALUE = "65536";
+ // 32768 is merely a menu marker, 64K is our lowest log buffer size we replace it with.
+ @VisibleForTesting
+ static final String SELECT_LOGD_MINIMUM_SIZE_VALUE = "65536";
+ static final String SELECT_LOGD_OFF_SIZE_MARKER_VALUE = "32768";
+ @VisibleForTesting
+ static final String DEFAULT_SNET_TAG = "I";
+
+ private ListPreference mLogdSize;
+
+ public AbstractLogdSizePreferenceController(Context context) {
+ super(context);
+ }
+
+ @Override
+ public String getPreferenceKey() {
+ return SELECT_LOGD_SIZE_KEY;
+ }
+
+ @Override
+ public void displayPreference(PreferenceScreen screen) {
+ super.displayPreference(screen);
+ if (isAvailable()) {
+ mLogdSize = (ListPreference) screen.findPreference(SELECT_LOGD_SIZE_KEY);
+ }
+ }
+
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object newValue) {
+ if (preference == mLogdSize) {
+ writeLogdSizeOption(newValue);
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ public void enablePreference(boolean enabled) {
+ if (isAvailable()) {
+ mLogdSize.setEnabled(enabled);
+ }
+ }
+
+ private String defaultLogdSizeValue() {
+ String defaultValue = SystemProperties.get(SELECT_LOGD_DEFAULT_SIZE_PROPERTY);
+ if ((defaultValue == null) || (defaultValue.length() == 0)) {
+ if (SystemProperties.get("ro.config.low_ram").equals("true")) {
+ defaultValue = SELECT_LOGD_SVELTE_DEFAULT_SIZE_VALUE;
+ } else {
+ defaultValue = SELECT_LOGD_DEFAULT_SIZE_VALUE;
+ }
+ }
+ return defaultValue;
+ }
+
+ public void updateLogdSizeValues() {
+ if (mLogdSize != null) {
+ String currentTag = SystemProperties.get(SELECT_LOGD_TAG_PROPERTY);
+ String currentValue = SystemProperties.get(SELECT_LOGD_SIZE_PROPERTY);
+ if ((currentTag != null) && currentTag.startsWith(SELECT_LOGD_TAG_SILENCE)) {
+ currentValue = SELECT_LOGD_OFF_SIZE_MARKER_VALUE;
+ }
+ LocalBroadcastManager.getInstance(mContext).sendBroadcastSync(
+ new Intent(ACTION_LOGD_SIZE_UPDATED)
+ .putExtra(EXTRA_CURRENT_LOGD_VALUE, currentValue));
+ if ((currentValue == null) || (currentValue.length() == 0)) {
+ currentValue = defaultLogdSizeValue();
+ }
+ String[] values = mContext.getResources()
+ .getStringArray(R.array.select_logd_size_values);
+ String[] titles = mContext.getResources()
+ .getStringArray(R.array.select_logd_size_titles);
+ int index = 2; // punt to second entry if not found
+ if (SystemProperties.get("ro.config.low_ram").equals("true")) {
+ mLogdSize.setEntries(R.array.select_logd_size_lowram_titles);
+ titles = mContext.getResources()
+ .getStringArray(R.array.select_logd_size_lowram_titles);
+ index = 1;
+ }
+ String[] summaries = mContext.getResources()
+ .getStringArray(R.array.select_logd_size_summaries);
+ for (int i = 0; i < titles.length; i++) {
+ if (currentValue.equals(values[i])
+ || currentValue.equals(titles[i])) {
+ index = i;
+ break;
+ }
+ }
+ mLogdSize.setValue(values[index]);
+ mLogdSize.setSummary(summaries[index]);
+ }
+ }
+
+ public void writeLogdSizeOption(Object newValue) {
+ boolean disable = (newValue != null) &&
+ (newValue.toString().equals(SELECT_LOGD_OFF_SIZE_MARKER_VALUE));
+ String currentTag = SystemProperties.get(SELECT_LOGD_TAG_PROPERTY);
+ if (currentTag == null) {
+ currentTag = "";
+ }
+ // filter clean and unstack all references to our setting
+ String newTag = currentTag.replaceAll(
+ ",+" + SELECT_LOGD_TAG_SILENCE, "").replaceFirst(
+ "^" + SELECT_LOGD_TAG_SILENCE + ",*", "").replaceAll(
+ ",+", ",").replaceFirst(
+ ",+$", "");
+ if (disable) {
+ newValue = SELECT_LOGD_MINIMUM_SIZE_VALUE;
+ // Make sure snet_event_log get through first, but do not override
+ String snetValue = SystemProperties.get(SELECT_LOGD_SNET_TAG_PROPERTY);
+ if ((snetValue == null) || (snetValue.length() == 0)) {
+ snetValue = SystemProperties.get(SELECT_LOGD_RUNTIME_SNET_TAG_PROPERTY);
+ if ((snetValue == null) || (snetValue.length() == 0)) {
+ SystemProperties.set(SELECT_LOGD_SNET_TAG_PROPERTY, DEFAULT_SNET_TAG);
+ }
+ }
+ // Silence all log sources, security logs notwithstanding
+ if (newTag.length() != 0) {
+ newTag = "," + newTag;
+ }
+ // Stack settings, stack to help preserve original value
+ newTag = SELECT_LOGD_TAG_SILENCE + newTag;
+ }
+ if (!newTag.equals(currentTag)) {
+ SystemProperties.set(SELECT_LOGD_TAG_PROPERTY, newTag);
+ }
+ String defaultValue = defaultLogdSizeValue();
+ final String size = ((newValue != null) && (newValue.toString().length() != 0)) ?
+ newValue.toString() : defaultValue;
+ SystemProperties.set(SELECT_LOGD_SIZE_PROPERTY, defaultValue.equals(size) ? "" : size);
+ SystemProperties.set("ctl.start", "logd-reinit");
+ SystemPropPoker.getInstance().poke();
+ updateLogdSizeValues();
+ }
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/development/AbstractLogpersistPreferenceController.java b/packages/SettingsLib/src/com/android/settingslib/development/AbstractLogpersistPreferenceController.java
new file mode 100644
index 000000000000..77b2d86c445f
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/development/AbstractLogpersistPreferenceController.java
@@ -0,0 +1,258 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.development;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Bundle;
+import android.os.SystemProperties;
+import android.support.annotation.VisibleForTesting;
+import android.support.v4.content.LocalBroadcastManager;
+import android.support.v7.preference.ListPreference;
+import android.support.v7.preference.Preference;
+import android.support.v7.preference.PreferenceScreen;
+import android.text.TextUtils;
+
+import com.android.settingslib.R;
+import com.android.settingslib.core.ConfirmationDialogController;
+import com.android.settingslib.core.lifecycle.Lifecycle;
+import com.android.settingslib.core.lifecycle.LifecycleObserver;
+import com.android.settingslib.core.lifecycle.events.OnCreate;
+import com.android.settingslib.core.lifecycle.events.OnDestroy;
+
+public abstract class AbstractLogpersistPreferenceController extends
+ DeveloperOptionsPreferenceController implements Preference.OnPreferenceChangeListener,
+ LifecycleObserver, OnCreate, OnDestroy, ConfirmationDialogController {
+
+ private static final String SELECT_LOGPERSIST_KEY = "select_logpersist";
+ private static final String SELECT_LOGPERSIST_PROPERTY = "persist.logd.logpersistd";
+ @VisibleForTesting
+ static final String ACTUAL_LOGPERSIST_PROPERTY = "logd.logpersistd";
+ @VisibleForTesting
+ static final String SELECT_LOGPERSIST_PROPERTY_SERVICE = "logcatd";
+ private static final String SELECT_LOGPERSIST_PROPERTY_CLEAR = "clear";
+ private static final String SELECT_LOGPERSIST_PROPERTY_STOP = "stop";
+ private static final String SELECT_LOGPERSIST_PROPERTY_BUFFER =
+ "persist.logd.logpersistd.buffer";
+ @VisibleForTesting
+ static final String ACTUAL_LOGPERSIST_PROPERTY_BUFFER = "logd.logpersistd.buffer";
+ private static final String ACTUAL_LOGPERSIST_PROPERTY_ENABLE = "logd.logpersistd.enable";
+
+ private ListPreference mLogpersist;
+ private boolean mLogpersistCleared;
+
+ private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ final String currentValue = intent.getStringExtra(
+ AbstractLogdSizePreferenceController.EXTRA_CURRENT_LOGD_VALUE);
+ onLogdSizeSettingUpdate(currentValue);
+ }
+ };
+
+ public AbstractLogpersistPreferenceController(Context context, Lifecycle lifecycle) {
+ super(context);
+ if (isAvailable() && lifecycle != null) {
+ lifecycle.addObserver(this);
+ }
+ }
+
+ @Override
+ public boolean isAvailable() {
+ return TextUtils.equals(SystemProperties.get("ro.debuggable", "0"), "1");
+ }
+
+ @Override
+ public String getPreferenceKey() {
+ return SELECT_LOGPERSIST_KEY;
+ }
+
+ @Override
+ public void displayPreference(PreferenceScreen screen) {
+ super.displayPreference(screen);
+ if (isAvailable()) {
+ mLogpersist = (ListPreference) screen.findPreference(SELECT_LOGPERSIST_KEY);
+ }
+ }
+
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object newValue) {
+ if (preference == mLogpersist) {
+ writeLogpersistOption(newValue, false);
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ LocalBroadcastManager.getInstance(mContext).registerReceiver(mReceiver,
+ new IntentFilter(AbstractLogdSizePreferenceController.ACTION_LOGD_SIZE_UPDATED));
+ }
+
+ @Override
+ public void onDestroy() {
+ LocalBroadcastManager.getInstance(mContext).unregisterReceiver(mReceiver);
+ }
+
+ public void enablePreference(boolean enabled) {
+ if (isAvailable()) {
+ mLogpersist.setEnabled(enabled);
+ }
+ }
+
+ private void onLogdSizeSettingUpdate(String currentValue) {
+ if (mLogpersist != null) {
+ String currentLogpersistEnable
+ = SystemProperties.get(ACTUAL_LOGPERSIST_PROPERTY_ENABLE);
+ if ((currentLogpersistEnable == null)
+ || !currentLogpersistEnable.equals("true")
+ || currentValue.equals(
+ AbstractLogdSizePreferenceController.SELECT_LOGD_OFF_SIZE_MARKER_VALUE)) {
+ writeLogpersistOption(null, true);
+ mLogpersist.setEnabled(false);
+ } else if (DevelopmentSettingsEnabler.isDevelopmentSettingsEnabled(mContext)) {
+ mLogpersist.setEnabled(true);
+ }
+ }
+ }
+
+ public void updateLogpersistValues() {
+ if (mLogpersist == null) {
+ return;
+ }
+ String currentValue = SystemProperties.get(ACTUAL_LOGPERSIST_PROPERTY);
+ if (currentValue == null) {
+ currentValue = "";
+ }
+ String currentBuffers = SystemProperties.get(ACTUAL_LOGPERSIST_PROPERTY_BUFFER);
+ if ((currentBuffers == null) || (currentBuffers.length() == 0)) {
+ currentBuffers = "all";
+ }
+ int index = 0;
+ if (currentValue.equals(SELECT_LOGPERSIST_PROPERTY_SERVICE)) {
+ index = 1;
+ if (currentBuffers.equals("kernel")) {
+ index = 3;
+ } else if (!currentBuffers.equals("all") &&
+ !currentBuffers.contains("radio") &&
+ currentBuffers.contains("security") &&
+ currentBuffers.contains("kernel")) {
+ index = 2;
+ if (!currentBuffers.contains("default")) {
+ String[] contains = {"main", "events", "system", "crash"};
+ for (String type : contains) {
+ if (!currentBuffers.contains(type)) {
+ index = 1;
+ break;
+ }
+ }
+ }
+ }
+ }
+ mLogpersist.setValue(
+ mContext.getResources().getStringArray(R.array.select_logpersist_values)[index]);
+ mLogpersist.setSummary(
+ mContext.getResources().getStringArray(R.array.select_logpersist_summaries)[index]);
+ if (index != 0) {
+ mLogpersistCleared = false;
+ } else if (!mLogpersistCleared) {
+ // would File.delete() directly but need to switch uid/gid to access
+ SystemProperties.set(ACTUAL_LOGPERSIST_PROPERTY, SELECT_LOGPERSIST_PROPERTY_CLEAR);
+ SystemPropPoker.getInstance().poke();
+ mLogpersistCleared = true;
+ }
+ }
+
+ protected void setLogpersistOff(boolean update) {
+ SystemProperties.set(SELECT_LOGPERSIST_PROPERTY_BUFFER, "");
+ // deal with trampoline of empty properties
+ SystemProperties.set(ACTUAL_LOGPERSIST_PROPERTY_BUFFER, "");
+ SystemProperties.set(SELECT_LOGPERSIST_PROPERTY, "");
+ SystemProperties.set(ACTUAL_LOGPERSIST_PROPERTY,
+ update ? "" : SELECT_LOGPERSIST_PROPERTY_STOP);
+ SystemPropPoker.getInstance().poke();
+ if (update) {
+ updateLogpersistValues();
+ } else {
+ for (int i = 0; i < 3; i++) {
+ String currentValue = SystemProperties.get(ACTUAL_LOGPERSIST_PROPERTY);
+ if ((currentValue == null) || currentValue.equals("")) {
+ break;
+ }
+ try {
+ Thread.sleep(100);
+ } catch (InterruptedException e) {
+ // Ignore
+ }
+ }
+ }
+ }
+
+ public void writeLogpersistOption(Object newValue, boolean skipWarning) {
+ if (mLogpersist == null) {
+ return;
+ }
+ String currentTag = SystemProperties.get(
+ AbstractLogdSizePreferenceController.SELECT_LOGD_TAG_PROPERTY);
+ if ((currentTag != null) && currentTag.startsWith(
+ AbstractLogdSizePreferenceController.SELECT_LOGD_TAG_SILENCE)) {
+ newValue = null;
+ skipWarning = true;
+ }
+
+ if ((newValue == null) || newValue.toString().equals("")) {
+ if (skipWarning) {
+ mLogpersistCleared = false;
+ } else if (!mLogpersistCleared) {
+ // if transitioning from on to off, pop up an are you sure?
+ String currentValue = SystemProperties.get(ACTUAL_LOGPERSIST_PROPERTY);
+ if ((currentValue != null) &&
+ currentValue.equals(SELECT_LOGPERSIST_PROPERTY_SERVICE)) {
+ showConfirmationDialog(mLogpersist);
+ return;
+ }
+ }
+ setLogpersistOff(true);
+ return;
+ }
+
+ String currentBuffer = SystemProperties.get(ACTUAL_LOGPERSIST_PROPERTY_BUFFER);
+ if ((currentBuffer != null) && !currentBuffer.equals(newValue.toString())) {
+ setLogpersistOff(false);
+ }
+ SystemProperties.set(SELECT_LOGPERSIST_PROPERTY_BUFFER, newValue.toString());
+ SystemProperties.set(SELECT_LOGPERSIST_PROPERTY, SELECT_LOGPERSIST_PROPERTY_SERVICE);
+ SystemPropPoker.getInstance().poke();
+ for (int i = 0; i < 3; i++) {
+ String currentValue = SystemProperties.get(ACTUAL_LOGPERSIST_PROPERTY);
+ if ((currentValue != null)
+ && currentValue.equals(SELECT_LOGPERSIST_PROPERTY_SERVICE)) {
+ break;
+ }
+ try {
+ Thread.sleep(100);
+ } catch (InterruptedException e) {
+ // Ignore
+ }
+ }
+ updateLogpersistValues();
+ }
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/development/DeveloperOptionsPreferenceController.java b/packages/SettingsLib/src/com/android/settingslib/development/DeveloperOptionsPreferenceController.java
new file mode 100644
index 000000000000..f68c04f91dbf
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/development/DeveloperOptionsPreferenceController.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.development;
+
+import android.content.Context;
+
+import com.android.settingslib.core.AbstractPreferenceController;
+
+/**
+ * This controller is used handle changes for the master switch in the developer options page.
+ *
+ * All Preference Controllers that are a part of the developer options page should inherit this
+ * class.
+ */
+public abstract class DeveloperOptionsPreferenceController extends
+ AbstractPreferenceController {
+
+ public DeveloperOptionsPreferenceController(Context context) {
+ super(context);
+ }
+
+ /**
+ * Child classes should override this method to create custom logic for hiding preferences.
+ *
+ * @return true if the preference is to be displayed.
+ */
+ @Override
+ public boolean isAvailable() {
+ return true;
+ }
+
+ /**
+ * Called when developer options is enabled
+ */
+ public void onDeveloperOptionsEnabled() {
+ if (isAvailable()) {
+ onDeveloperOptionsSwitchEnabled();
+ }
+ }
+
+ /**
+ * Called when developer options is disabled
+ */
+ public void onDeveloperOptionsDisabled() {
+ if (isAvailable()) {
+ onDeveloperOptionsSwitchDisabled();
+ }
+ }
+
+ /**
+ * Called when developer options is enabled and the preference is available
+ */
+ protected void onDeveloperOptionsSwitchEnabled() {
+ }
+
+ /**
+ * Called when developer options is disabled and the preference is available
+ */
+ protected void onDeveloperOptionsSwitchDisabled() {
+ }
+
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/development/DevelopmentSettingsEnabler.java b/packages/SettingsLib/src/com/android/settingslib/development/DevelopmentSettingsEnabler.java
new file mode 100644
index 000000000000..85bf4e83bd55
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/development/DevelopmentSettingsEnabler.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.development;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.Build;
+import android.os.UserManager;
+import android.provider.Settings;
+import android.support.v4.content.LocalBroadcastManager;
+
+public class DevelopmentSettingsEnabler {
+
+ public static final String DEVELOPMENT_SETTINGS_CHANGED_ACTION =
+ "com.android.settingslib.development.DevelopmentSettingsEnabler.SETTINGS_CHANGED";
+
+ private DevelopmentSettingsEnabler() {
+ }
+
+ public static void setDevelopmentSettingsEnabled(Context context, boolean enable) {
+ Settings.Global.putInt(context.getContentResolver(),
+ Settings.Global.DEVELOPMENT_SETTINGS_ENABLED, enable ? 1 : 0);
+ LocalBroadcastManager.getInstance(context)
+ .sendBroadcast(new Intent(DEVELOPMENT_SETTINGS_CHANGED_ACTION));
+ }
+
+ public static boolean isDevelopmentSettingsEnabled(Context context) {
+ final UserManager um = (UserManager) context.getSystemService(Context.USER_SERVICE);
+ final boolean settingEnabled = Settings.Global.getInt(context.getContentResolver(),
+ Settings.Global.DEVELOPMENT_SETTINGS_ENABLED,
+ Build.TYPE.equals("eng") ? 1 : 0) != 0;
+ final boolean hasRestriction = um.hasUserRestriction(
+ UserManager.DISALLOW_DEBUGGING_FEATURES);
+ final boolean isAdmin = um.isAdminUser();
+
+ return isAdmin && !hasRestriction && settingEnabled;
+ }
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/development/SystemPropPoker.java b/packages/SettingsLib/src/com/android/settingslib/development/SystemPropPoker.java
new file mode 100644
index 000000000000..628d0d08b526
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/development/SystemPropPoker.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.development;
+
+import android.os.AsyncTask;
+import android.os.IBinder;
+import android.os.Parcel;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.support.annotation.NonNull;
+import android.support.annotation.VisibleForTesting;
+import android.util.Log;
+
+public class SystemPropPoker {
+ private static final String TAG = "SystemPropPoker";
+
+ private static final SystemPropPoker sInstance = new SystemPropPoker();
+
+ private boolean mBlockPokes = false;
+
+ private SystemPropPoker() {}
+
+ @NonNull
+ public static SystemPropPoker getInstance() {
+ return sInstance;
+ }
+
+ public void blockPokes() {
+ mBlockPokes = true;
+ }
+
+ public void unblockPokes() {
+ mBlockPokes = false;
+ }
+
+ public void poke() {
+ if (!mBlockPokes) {
+ createPokerTask().execute();
+ }
+ }
+
+ @VisibleForTesting
+ PokerTask createPokerTask() {
+ return new PokerTask();
+ }
+
+ public static class PokerTask extends AsyncTask<Void, Void, Void> {
+
+ @VisibleForTesting
+ String[] listServices() {
+ return ServiceManager.listServices();
+ }
+
+ @VisibleForTesting
+ IBinder checkService(String service) {
+ return ServiceManager.checkService(service);
+ }
+
+ @Override
+ protected Void doInBackground(Void... params) {
+ String[] services = listServices();
+ if (services == null) {
+ Log.e(TAG, "There are no services, how odd");
+ return null;
+ }
+ for (String service : services) {
+ IBinder obj = checkService(service);
+ if (obj != null) {
+ Parcel data = Parcel.obtain();
+ try {
+ obj.transact(IBinder.SYSPROPS_TRANSACTION, data, null, 0);
+ } catch (RemoteException e) {
+ // Ignore
+ } catch (Exception e) {
+ Log.i(TAG, "Someone wrote a bad service '" + service
+ + "' that doesn't like to be poked", e);
+ }
+ data.recycle();
+ }
+ }
+ return null;
+ }
+ }
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/deviceinfo/AbstractBluetoothAddressPreferenceController.java b/packages/SettingsLib/src/com/android/settingslib/deviceinfo/AbstractBluetoothAddressPreferenceController.java
new file mode 100644
index 000000000000..ba358f83c9d5
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/deviceinfo/AbstractBluetoothAddressPreferenceController.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.deviceinfo;
+
+import android.annotation.SuppressLint;
+import android.bluetooth.BluetoothAdapter;
+import android.content.Context;
+import android.support.annotation.VisibleForTesting;
+import android.support.v7.preference.Preference;
+import android.support.v7.preference.PreferenceScreen;
+import android.text.TextUtils;
+
+import com.android.settingslib.R;
+import com.android.settingslib.core.lifecycle.Lifecycle;
+
+/**
+ * Preference controller for bluetooth address
+ */
+public abstract class AbstractBluetoothAddressPreferenceController
+ extends AbstractConnectivityPreferenceController {
+
+ @VisibleForTesting
+ static final String KEY_BT_ADDRESS = "bt_address";
+
+ private static final String[] CONNECTIVITY_INTENTS = {
+ BluetoothAdapter.ACTION_STATE_CHANGED
+ };
+
+ private Preference mBtAddress;
+
+ public AbstractBluetoothAddressPreferenceController(Context context, Lifecycle lifecycle) {
+ super(context, lifecycle);
+ }
+
+ @Override
+ public boolean isAvailable() {
+ return BluetoothAdapter.getDefaultAdapter() != null;
+ }
+
+ @Override
+ public String getPreferenceKey() {
+ return KEY_BT_ADDRESS;
+ }
+
+ @Override
+ public void displayPreference(PreferenceScreen screen) {
+ super.displayPreference(screen);
+ mBtAddress = screen.findPreference(KEY_BT_ADDRESS);
+ updateConnectivity();
+ }
+
+ @Override
+ protected String[] getConnectivityIntents() {
+ return CONNECTIVITY_INTENTS;
+ }
+
+ @SuppressLint("HardwareIds")
+ @Override
+ protected void updateConnectivity() {
+ BluetoothAdapter bluetooth = BluetoothAdapter.getDefaultAdapter();
+ if (bluetooth != null && mBtAddress != null) {
+ String address = bluetooth.isEnabled() ? bluetooth.getAddress() : null;
+ if (!TextUtils.isEmpty(address)) {
+ // Convert the address to lowercase for consistency with the wifi MAC address.
+ mBtAddress.setSummary(address.toLowerCase());
+ } else {
+ mBtAddress.setSummary(R.string.status_unavailable);
+ }
+ }
+ }
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/deviceinfo/AbstractConnectivityPreferenceController.java b/packages/SettingsLib/src/com/android/settingslib/deviceinfo/AbstractConnectivityPreferenceController.java
new file mode 100644
index 000000000000..c6552f77a2b2
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/deviceinfo/AbstractConnectivityPreferenceController.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.deviceinfo;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Handler;
+import android.os.Message;
+
+import com.android.internal.util.ArrayUtils;
+import com.android.settingslib.core.AbstractPreferenceController;
+import com.android.settingslib.core.lifecycle.Lifecycle;
+import com.android.settingslib.core.lifecycle.LifecycleObserver;
+import com.android.settingslib.core.lifecycle.events.OnStart;
+import com.android.settingslib.core.lifecycle.events.OnStop;
+
+import java.lang.ref.WeakReference;
+
+/**
+ * Base class for preference controllers which listen to connectivity broadcasts
+ */
+public abstract class AbstractConnectivityPreferenceController
+ extends AbstractPreferenceController implements LifecycleObserver, OnStart, OnStop {
+
+ private final BroadcastReceiver mConnectivityReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String action = intent.getAction();
+ if (ArrayUtils.contains(getConnectivityIntents(), action)) {
+ getHandler().sendEmptyMessage(EVENT_UPDATE_CONNECTIVITY);
+ }
+ }
+ };
+
+ private static final int EVENT_UPDATE_CONNECTIVITY = 600;
+
+ private Handler mHandler;
+
+ public AbstractConnectivityPreferenceController(Context context, Lifecycle lifecycle) {
+ super(context);
+ if (lifecycle != null) {
+ lifecycle.addObserver(this);
+ }
+ }
+
+ @Override
+ public void onStop() {
+ mContext.unregisterReceiver(mConnectivityReceiver);
+ }
+
+ @Override
+ public void onStart() {
+ final IntentFilter connectivityIntentFilter = new IntentFilter();
+ final String[] intents = getConnectivityIntents();
+ for (String intent : intents) {
+ connectivityIntentFilter.addAction(intent);
+ }
+
+ mContext.registerReceiver(mConnectivityReceiver, connectivityIntentFilter,
+ android.Manifest.permission.CHANGE_NETWORK_STATE, null);
+ }
+
+ protected abstract String[] getConnectivityIntents();
+
+ protected abstract void updateConnectivity();
+
+ private Handler getHandler() {
+ if (mHandler == null) {
+ mHandler = new ConnectivityEventHandler(this);
+ }
+ return mHandler;
+ }
+
+ private static class ConnectivityEventHandler extends Handler {
+ private WeakReference<AbstractConnectivityPreferenceController> mPreferenceController;
+
+ public ConnectivityEventHandler(AbstractConnectivityPreferenceController activity) {
+ mPreferenceController = new WeakReference<>(activity);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ AbstractConnectivityPreferenceController preferenceController
+ = mPreferenceController.get();
+ if (preferenceController == null) {
+ return;
+ }
+
+ switch (msg.what) {
+ case EVENT_UPDATE_CONNECTIVITY:
+ preferenceController.updateConnectivity();
+ break;
+ default:
+ throw new IllegalStateException("Unknown message " + msg.what);
+ }
+ }
+ }
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/deviceinfo/AbstractImsStatusPreferenceController.java b/packages/SettingsLib/src/com/android/settingslib/deviceinfo/AbstractImsStatusPreferenceController.java
new file mode 100644
index 000000000000..bb8404b0abd5
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/deviceinfo/AbstractImsStatusPreferenceController.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.deviceinfo;
+
+import android.bluetooth.BluetoothAdapter;
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.wifi.WifiManager;
+import android.os.PersistableBundle;
+import android.support.annotation.VisibleForTesting;
+import android.support.v7.preference.Preference;
+import android.support.v7.preference.PreferenceScreen;
+import android.telephony.CarrierConfigManager;
+import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyManager;
+
+import com.android.settingslib.R;
+import com.android.settingslib.core.lifecycle.Lifecycle;
+
+/**
+ * Preference controller for IMS status
+ */
+public abstract class AbstractImsStatusPreferenceController
+ extends AbstractConnectivityPreferenceController {
+
+ @VisibleForTesting
+ static final String KEY_IMS_REGISTRATION_STATE = "ims_reg_state";
+
+ private static final String[] CONNECTIVITY_INTENTS = {
+ BluetoothAdapter.ACTION_STATE_CHANGED,
+ ConnectivityManager.CONNECTIVITY_ACTION,
+ WifiManager.LINK_CONFIGURATION_CHANGED_ACTION,
+ WifiManager.NETWORK_STATE_CHANGED_ACTION,
+ };
+
+ private Preference mImsStatus;
+
+ public AbstractImsStatusPreferenceController(Context context,
+ Lifecycle lifecycle) {
+ super(context, lifecycle);
+ }
+
+ @Override
+ public boolean isAvailable() {
+ CarrierConfigManager configManager = mContext.getSystemService(CarrierConfigManager.class);
+ int subId = SubscriptionManager.getDefaultDataSubscriptionId();
+ PersistableBundle config = null;
+ if (configManager != null) {
+ config = configManager.getConfigForSubId(subId);
+ }
+ return config != null && config.getBoolean(
+ CarrierConfigManager.KEY_SHOW_IMS_REGISTRATION_STATUS_BOOL);
+ }
+
+ @Override
+ public String getPreferenceKey() {
+ return KEY_IMS_REGISTRATION_STATE;
+ }
+
+ @Override
+ public void displayPreference(PreferenceScreen screen) {
+ super.displayPreference(screen);
+ mImsStatus = screen.findPreference(KEY_IMS_REGISTRATION_STATE);
+ updateConnectivity();
+ }
+
+ @Override
+ protected String[] getConnectivityIntents() {
+ return CONNECTIVITY_INTENTS;
+ }
+
+ @Override
+ protected void updateConnectivity() {
+ int subId = SubscriptionManager.getDefaultDataSubscriptionId();
+ if (mImsStatus != null) {
+ TelephonyManager tm = mContext.getSystemService(TelephonyManager.class);
+ mImsStatus.setSummary((tm != null && tm.isImsRegistered(subId)) ?
+ R.string.ims_reg_status_registered : R.string.ims_reg_status_not_registered);
+ }
+ }
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/deviceinfo/AbstractIpAddressPreferenceController.java b/packages/SettingsLib/src/com/android/settingslib/deviceinfo/AbstractIpAddressPreferenceController.java
new file mode 100644
index 000000000000..ded30226e2ae
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/deviceinfo/AbstractIpAddressPreferenceController.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.deviceinfo;
+
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.LinkProperties;
+import android.net.wifi.WifiManager;
+import android.support.annotation.VisibleForTesting;
+import android.support.v7.preference.Preference;
+import android.support.v7.preference.PreferenceScreen;
+
+import com.android.settingslib.R;
+import com.android.settingslib.core.lifecycle.Lifecycle;
+
+import java.net.InetAddress;
+import java.util.Iterator;
+
+/**
+ * Preference controller for IP address
+ */
+public abstract class AbstractIpAddressPreferenceController
+ extends AbstractConnectivityPreferenceController {
+
+ @VisibleForTesting
+ static final String KEY_IP_ADDRESS = "wifi_ip_address";
+
+ private static final String[] CONNECTIVITY_INTENTS = {
+ ConnectivityManager.CONNECTIVITY_ACTION,
+ WifiManager.LINK_CONFIGURATION_CHANGED_ACTION,
+ WifiManager.NETWORK_STATE_CHANGED_ACTION,
+ };
+
+ private Preference mIpAddress;
+ private final ConnectivityManager mCM;
+
+ public AbstractIpAddressPreferenceController(Context context, Lifecycle lifecycle) {
+ super(context, lifecycle);
+ mCM = context.getSystemService(ConnectivityManager.class);
+ }
+
+ @Override
+ public boolean isAvailable() {
+ return true;
+ }
+
+ @Override
+ public String getPreferenceKey() {
+ return KEY_IP_ADDRESS;
+ }
+
+ @Override
+ public void displayPreference(PreferenceScreen screen) {
+ super.displayPreference(screen);
+ mIpAddress = screen.findPreference(KEY_IP_ADDRESS);
+ updateConnectivity();
+ }
+
+ @Override
+ protected String[] getConnectivityIntents() {
+ return CONNECTIVITY_INTENTS;
+ }
+
+ @Override
+ protected void updateConnectivity() {
+ String ipAddress = getDefaultIpAddresses(mCM);
+ if (ipAddress != null) {
+ mIpAddress.setSummary(ipAddress);
+ } else {
+ mIpAddress.setSummary(R.string.status_unavailable);
+ }
+ }
+
+ /**
+ * Returns the default link's IP addresses, if any, taking into account IPv4 and IPv6 style
+ * addresses.
+ * @param cm ConnectivityManager
+ * @return the formatted and newline-separated IP addresses, or null if none.
+ */
+ private static String getDefaultIpAddresses(ConnectivityManager cm) {
+ LinkProperties prop = cm.getActiveLinkProperties();
+ return formatIpAddresses(prop);
+ }
+
+ private static String formatIpAddresses(LinkProperties prop) {
+ if (prop == null) return null;
+ Iterator<InetAddress> iter = prop.getAllAddresses().iterator();
+ // If there are no entries, return null
+ if (!iter.hasNext()) return null;
+ // Concatenate all available addresses, newline separated
+ StringBuilder addresses = new StringBuilder();
+ while (iter.hasNext()) {
+ addresses.append(iter.next().getHostAddress());
+ if (iter.hasNext()) addresses.append("\n");
+ }
+ return addresses.toString();
+ }
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/deviceinfo/AbstractSerialNumberPreferenceController.java b/packages/SettingsLib/src/com/android/settingslib/deviceinfo/AbstractSerialNumberPreferenceController.java
new file mode 100644
index 000000000000..90f14ef4c32c
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/deviceinfo/AbstractSerialNumberPreferenceController.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.deviceinfo;
+
+import android.content.Context;
+import android.os.Build;
+import android.support.annotation.VisibleForTesting;
+import android.support.v7.preference.Preference;
+import android.support.v7.preference.PreferenceScreen;
+import android.text.TextUtils;
+
+import com.android.settingslib.core.AbstractPreferenceController;
+
+/**
+ * Preference controller for displaying device serial number. Wraps {@link Build#getSerial()}.
+ */
+public class AbstractSerialNumberPreferenceController extends AbstractPreferenceController {
+
+ @VisibleForTesting
+ static final String KEY_SERIAL_NUMBER = "serial_number";
+
+ private final String mSerialNumber;
+
+ public AbstractSerialNumberPreferenceController(Context context) {
+ this(context, Build.getSerial());
+ }
+
+ @VisibleForTesting
+ AbstractSerialNumberPreferenceController(Context context, String serialNumber) {
+ super(context);
+ mSerialNumber = serialNumber;
+ }
+
+ @Override
+ public boolean isAvailable() {
+ return !TextUtils.isEmpty(mSerialNumber);
+ }
+
+ @Override
+ public void displayPreference(PreferenceScreen screen) {
+ super.displayPreference(screen);
+ final Preference pref = screen.findPreference(KEY_SERIAL_NUMBER);
+ if (pref != null) {
+ pref.setSummary(mSerialNumber);
+ }
+ }
+
+ @Override
+ public String getPreferenceKey() {
+ return KEY_SERIAL_NUMBER;
+ }
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/deviceinfo/AbstractSimStatusImeiInfoPreferenceController.java b/packages/SettingsLib/src/com/android/settingslib/deviceinfo/AbstractSimStatusImeiInfoPreferenceController.java
new file mode 100644
index 000000000000..a78440c271c9
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/deviceinfo/AbstractSimStatusImeiInfoPreferenceController.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.deviceinfo;
+
+import android.content.Context;
+import android.os.UserManager;
+
+import com.android.settingslib.Utils;
+import com.android.settingslib.core.AbstractPreferenceController;
+
+public abstract class AbstractSimStatusImeiInfoPreferenceController
+ extends AbstractPreferenceController {
+ public AbstractSimStatusImeiInfoPreferenceController(Context context) {
+ super(context);
+ }
+
+ @Override
+ public boolean isAvailable() {
+ return mContext.getSystemService(UserManager.class).isAdminUser()
+ && !Utils.isWifiOnly(mContext);
+ }
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/deviceinfo/AbstractUptimePreferenceController.java b/packages/SettingsLib/src/com/android/settingslib/deviceinfo/AbstractUptimePreferenceController.java
new file mode 100644
index 000000000000..ac61ade19222
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/deviceinfo/AbstractUptimePreferenceController.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.deviceinfo;
+
+import android.content.Context;
+import android.os.Handler;
+import android.os.Message;
+import android.os.SystemClock;
+import android.support.annotation.VisibleForTesting;
+import android.support.v7.preference.Preference;
+import android.support.v7.preference.PreferenceScreen;
+import android.text.format.DateUtils;
+
+import com.android.settingslib.core.AbstractPreferenceController;
+import com.android.settingslib.core.lifecycle.Lifecycle;
+import com.android.settingslib.core.lifecycle.LifecycleObserver;
+import com.android.settingslib.core.lifecycle.events.OnStart;
+import com.android.settingslib.core.lifecycle.events.OnStop;
+
+import java.lang.ref.WeakReference;
+
+/**
+ * Preference controller for uptime
+ */
+public abstract class AbstractUptimePreferenceController extends AbstractPreferenceController
+ implements LifecycleObserver, OnStart, OnStop {
+
+ @VisibleForTesting
+ static final String KEY_UPTIME = "up_time";
+ private static final int EVENT_UPDATE_STATS = 500;
+
+ private Preference mUptime;
+ private Handler mHandler;
+
+ public AbstractUptimePreferenceController(Context context, Lifecycle lifecycle) {
+ super(context);
+ if (lifecycle != null) {
+ lifecycle.addObserver(this);
+ }
+ }
+
+ @Override
+ public void onStart() {
+ getHandler().sendEmptyMessage(EVENT_UPDATE_STATS);
+ }
+
+ @Override
+ public void onStop() {
+ getHandler().removeMessages(EVENT_UPDATE_STATS);
+ }
+
+ @Override
+ public boolean isAvailable() {
+ return true;
+ }
+
+ @Override
+ public String getPreferenceKey() {
+ return KEY_UPTIME;
+ }
+
+ @Override
+ public void displayPreference(PreferenceScreen screen) {
+ super.displayPreference(screen);
+ mUptime = screen.findPreference(KEY_UPTIME);
+ updateTimes();
+ }
+
+ private Handler getHandler() {
+ if (mHandler == null) {
+ mHandler = new MyHandler(this);
+ }
+ return mHandler;
+ }
+
+ private void updateTimes() {
+ mUptime.setSummary(DateUtils.formatDuration(SystemClock.elapsedRealtime()));
+ }
+
+ private static class MyHandler extends Handler {
+ private WeakReference<AbstractUptimePreferenceController> mStatus;
+
+ public MyHandler(AbstractUptimePreferenceController activity) {
+ mStatus = new WeakReference<>(activity);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ AbstractUptimePreferenceController status = mStatus.get();
+ if (status == null) {
+ return;
+ }
+
+ switch (msg.what) {
+ case EVENT_UPDATE_STATS:
+ status.updateTimes();
+ sendEmptyMessageDelayed(EVENT_UPDATE_STATS, 1000);
+ break;
+
+ default:
+ throw new IllegalStateException("Unknown message " + msg.what);
+ }
+ }
+ }
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/deviceinfo/AbstractWifiMacAddressPreferenceController.java b/packages/SettingsLib/src/com/android/settingslib/deviceinfo/AbstractWifiMacAddressPreferenceController.java
new file mode 100644
index 000000000000..d57b64f0c0cb
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/deviceinfo/AbstractWifiMacAddressPreferenceController.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.deviceinfo;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.wifi.WifiInfo;
+import android.net.wifi.WifiManager;
+import android.support.annotation.VisibleForTesting;
+import android.support.v7.preference.Preference;
+import android.support.v7.preference.PreferenceScreen;
+import android.text.TextUtils;
+
+import com.android.settingslib.R;
+import com.android.settingslib.core.lifecycle.Lifecycle;
+
+/**
+ * Preference controller for WIFI MAC address
+ */
+public abstract class AbstractWifiMacAddressPreferenceController
+ extends AbstractConnectivityPreferenceController {
+
+ @VisibleForTesting
+ static final String KEY_WIFI_MAC_ADDRESS = "wifi_mac_address";
+
+ private static final String[] CONNECTIVITY_INTENTS = {
+ ConnectivityManager.CONNECTIVITY_ACTION,
+ WifiManager.LINK_CONFIGURATION_CHANGED_ACTION,
+ WifiManager.NETWORK_STATE_CHANGED_ACTION,
+ };
+
+ private Preference mWifiMacAddress;
+ private final WifiManager mWifiManager;
+
+ public AbstractWifiMacAddressPreferenceController(Context context, Lifecycle lifecycle) {
+ super(context, lifecycle);
+ mWifiManager = context.getSystemService(WifiManager.class);
+ }
+
+ @Override
+ public boolean isAvailable() {
+ return true;
+ }
+
+ @Override
+ public String getPreferenceKey() {
+ return KEY_WIFI_MAC_ADDRESS;
+ }
+
+ @Override
+ public void displayPreference(PreferenceScreen screen) {
+ super.displayPreference(screen);
+ mWifiMacAddress = screen.findPreference(KEY_WIFI_MAC_ADDRESS);
+ updateConnectivity();
+ }
+
+ @Override
+ protected String[] getConnectivityIntents() {
+ return CONNECTIVITY_INTENTS;
+ }
+
+ @SuppressLint("HardwareIds")
+ @Override
+ protected void updateConnectivity() {
+ WifiInfo wifiInfo = mWifiManager.getConnectionInfo();
+ String macAddress = wifiInfo == null ? null : wifiInfo.getMacAddress();
+ if (!TextUtils.isEmpty(macAddress)) {
+ mWifiMacAddress.setSummary(macAddress);
+ } else {
+ mWifiMacAddress.setSummary(R.string.status_unavailable);
+ }
+ }
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/drawer/CategoryManager.java b/packages/SettingsLib/src/com/android/settingslib/drawer/CategoryManager.java
index ee7885d2a077..07033304653c 100644
--- a/packages/SettingsLib/src/com/android/settingslib/drawer/CategoryManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/drawer/CategoryManager.java
@@ -18,7 +18,6 @@ package com.android.settingslib.drawer;
import android.content.ComponentName;
import android.content.Context;
import android.support.annotation.VisibleForTesting;
-import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Log;
@@ -27,7 +26,6 @@ import android.util.Pair;
import com.android.settingslib.applications.InterestingConfigChanges;
import java.util.ArrayList;
-import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -104,10 +102,10 @@ public class CategoryManager {
}
for (int i = 0; i < mCategories.size(); i++) {
DashboardCategory category = mCategories.get(i);
- for (int j = 0; j < category.tiles.size(); j++) {
- Tile tile = category.tiles.get(j);
+ for (int j = 0; j < category.getTilesCount(); j++) {
+ Tile tile = category.getTile(j);
if (tileBlacklist.contains(tile.intent.getComponent())) {
- category.tiles.remove(j--);
+ category.removeTile(j--);
}
}
}
@@ -181,7 +179,7 @@ public class CategoryManager {
newCategory = new DashboardCategory();
categoryByKeyMap.put(newCategoryKey, newCategory);
}
- newCategory.tiles.add(tile);
+ newCategory.addTile(tile);
}
}
}
@@ -198,7 +196,7 @@ public class CategoryManager {
synchronized void sortCategories(Context context,
Map<String, DashboardCategory> categoryByKeyMap) {
for (Entry<String, DashboardCategory> categoryEntry : categoryByKeyMap.entrySet()) {
- sortCategoriesForExternalTiles(context, categoryEntry.getValue());
+ categoryEntry.getValue().sortTiles(context.getPackageName());
}
}
@@ -210,16 +208,16 @@ public class CategoryManager {
synchronized void filterDuplicateTiles(Map<String, DashboardCategory> categoryByKeyMap) {
for (Entry<String, DashboardCategory> categoryEntry : categoryByKeyMap.entrySet()) {
final DashboardCategory category = categoryEntry.getValue();
- final int count = category.tiles.size();
+ final int count = category.getTilesCount();
final Set<ComponentName> components = new ArraySet<>();
for (int i = count - 1; i >= 0; i--) {
- final Tile tile = category.tiles.get(i);
+ final Tile tile = category.getTile(i);
if (tile.intent == null) {
continue;
}
final ComponentName tileComponent = tile.intent.getComponent();
if (components.contains(tileComponent)) {
- category.tiles.remove(i);
+ category.removeTile(i);
} else {
components.add(tileComponent);
}
@@ -234,28 +232,7 @@ public class CategoryManager {
*/
private synchronized void sortCategoriesForExternalTiles(Context context,
DashboardCategory dashboardCategory) {
- final String skipPackageName = context.getPackageName();
+ dashboardCategory.sortTiles(context.getPackageName());
- // Sort tiles based on [priority, package within priority]
- Collections.sort(dashboardCategory.tiles, (tile1, tile2) -> {
- final String package1 = tile1.intent.getComponent().getPackageName();
- final String package2 = tile2.intent.getComponent().getPackageName();
- final int packageCompare = CASE_INSENSITIVE_ORDER.compare(package1, package2);
- // First sort by priority
- final int priorityCompare = tile2.priority - tile1.priority;
- if (priorityCompare != 0) {
- return priorityCompare;
- }
- // Then sort by package name, skip package take precedence
- if (packageCompare != 0) {
- if (TextUtils.equals(package1, skipPackageName)) {
- return -1;
- }
- if (TextUtils.equals(package2, skipPackageName)) {
- return 1;
- }
- }
- return packageCompare;
- });
}
}
diff --git a/packages/SettingsLib/src/com/android/settingslib/drawer/DashboardCategory.java b/packages/SettingsLib/src/com/android/settingslib/drawer/DashboardCategory.java
index f6f81682ad6f..3a03644b6226 100644
--- a/packages/SettingsLib/src/com/android/settingslib/drawer/DashboardCategory.java
+++ b/packages/SettingsLib/src/com/android/settingslib/drawer/DashboardCategory.java
@@ -16,6 +16,8 @@
package com.android.settingslib.drawer;
+import static java.lang.String.CASE_INSENSITIVE_ORDER;
+
import android.content.ComponentName;
import android.os.Parcel;
import android.os.Parcelable;
@@ -23,6 +25,8 @@ import android.text.TextUtils;
import android.util.Log;
import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
import java.util.List;
public class DashboardCategory implements Parcelable {
@@ -48,39 +52,63 @@ public class DashboardCategory implements Parcelable {
/**
* List of the category's children
*/
- public List<Tile> tiles = new ArrayList<>();
-
+ private List<Tile> mTiles = new ArrayList<>();
+
+ DashboardCategory(DashboardCategory in) {
+ if (in != null) {
+ title = in.title;
+ key = in.key;
+ priority = in.priority;
+ for (Tile tile : in.mTiles) {
+ mTiles.add(tile);
+ }
+ }
+ }
public DashboardCategory() {
// Empty
}
- public void addTile(Tile tile) {
- tiles.add(tile);
+ /**
+ * Get a copy of the list of the category's children.
+ *
+ * Note: the returned list serves as a read-only list. If tiles needs to be added or removed
+ * from the actual tiles list, it should be done through {@link #addTile}, {@link #removeTile}.
+ */
+ public synchronized List<Tile> getTiles() {
+ final List<Tile> result = new ArrayList<>(mTiles.size());
+ for (Tile tile : mTiles) {
+ result.add(tile);
+ }
+ return result;
+ }
+
+ public synchronized void addTile(Tile tile) {
+ mTiles.add(tile);
}
- public void addTile(int n, Tile tile) {
- tiles.add(n, tile);
+ public synchronized void addTile(int n, Tile tile) {
+ mTiles.add(n, tile);
}
- public void removeTile(Tile tile) {
- tiles.remove(tile);
+ public synchronized void removeTile(Tile tile) {
+ mTiles.remove(tile);
}
- public void removeTile(int n) {
- tiles.remove(n);
+ public synchronized void removeTile(int n) {
+ mTiles.remove(n);
}
public int getTilesCount() {
- return tiles.size();
+ return mTiles.size();
}
public Tile getTile(int n) {
- return tiles.get(n);
+ return mTiles.get(n);
}
- public boolean containsComponent(ComponentName component) {
- for (Tile tile : tiles) {
+ public synchronized boolean containsComponent(ComponentName component) {
+ for (Tile tile : mTiles) {
if (TextUtils.equals(tile.intent.getComponent().getClassName(),
component.getClassName())) {
if (DEBUG) {
@@ -95,6 +123,40 @@ public class DashboardCategory implements Parcelable {
return false;
}
+ /**
+ * Sort priority value for tiles in this category.
+ */
+ public void sortTiles() {
+ Collections.sort(mTiles, TILE_COMPARATOR);
+ }
+
+ /**
+ * Sort priority value and package name for tiles in this category.
+ */
+ public synchronized void sortTiles(String skipPackageName) {
+ // Sort mTiles based on [priority, package within priority]
+ Collections.sort(mTiles, (tile1, tile2) -> {
+ final String package1 = tile1.intent.getComponent().getPackageName();
+ final String package2 = tile2.intent.getComponent().getPackageName();
+ final int packageCompare = CASE_INSENSITIVE_ORDER.compare(package1, package2);
+ // First sort by priority
+ final int priorityCompare = tile2.priority - tile1.priority;
+ if (priorityCompare != 0) {
+ return priorityCompare;
+ }
+ // Then sort by package name, skip package take precedence
+ if (packageCompare != 0) {
+ if (TextUtils.equals(package1, skipPackageName)) {
+ return -1;
+ }
+ if (TextUtils.equals(package2, skipPackageName)) {
+ return 1;
+ }
+ }
+ return packageCompare;
+ });
+ }
+
@Override
public int describeContents() {
return 0;
@@ -106,11 +168,11 @@ public class DashboardCategory implements Parcelable {
dest.writeString(key);
dest.writeInt(priority);
- final int count = tiles.size();
+ final int count = mTiles.size();
dest.writeInt(count);
for (int n = 0; n < count; n++) {
- Tile tile = tiles.get(n);
+ Tile tile = mTiles.get(n);
tile.writeToParcel(dest, flags);
}
}
@@ -124,7 +186,7 @@ public class DashboardCategory implements Parcelable {
for (int n = 0; n < count; n++) {
Tile tile = Tile.CREATOR.createFromParcel(in);
- tiles.add(tile);
+ mTiles.add(tile);
}
}
@@ -141,4 +203,13 @@ public class DashboardCategory implements Parcelable {
return new DashboardCategory[size];
}
};
+
+ public static final Comparator<Tile> TILE_COMPARATOR =
+ new Comparator<Tile>() {
+ @Override
+ public int compare(Tile lhs, Tile rhs) {
+ return rhs.priority - lhs.priority;
+ }
+ };
+
}
diff --git a/packages/SettingsLib/src/com/android/settingslib/drawer/SettingsDrawerActivity.java b/packages/SettingsLib/src/com/android/settingslib/drawer/SettingsDrawerActivity.java
index 190f5e6d1402..68ead09d9d57 100644
--- a/packages/SettingsLib/src/com/android/settingslib/drawer/SettingsDrawerActivity.java
+++ b/packages/SettingsLib/src/com/android/settingslib/drawer/SettingsDrawerActivity.java
@@ -17,7 +17,6 @@ package com.android.settingslib.drawer;
import android.annotation.LayoutRes;
import android.annotation.Nullable;
-import android.app.ActionBar;
import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
@@ -72,9 +71,9 @@ public class SettingsDrawerActivity extends Activity {
requestWindowFeature(Window.FEATURE_NO_TITLE);
}
super.setContentView(R.layout.settings_with_drawer);
- mContentHeaderContainer = (FrameLayout) findViewById(R.id.content_header_container);
+ mContentHeaderContainer = findViewById(R.id.content_header_container);
- Toolbar toolbar = (Toolbar) findViewById(R.id.action_bar);
+ Toolbar toolbar = findViewById(R.id.action_bar);
if (theme.getBoolean(android.R.styleable.Theme_windowNoTitle, false)) {
toolbar.setVisibility(View.GONE);
return;
@@ -89,7 +88,9 @@ public class SettingsDrawerActivity extends Activity {
@Override
public boolean onNavigateUp() {
- finish();
+ if (!super.onNavigateUp()) {
+ finish();
+ }
return true;
}
@@ -104,11 +105,6 @@ public class SettingsDrawerActivity extends Activity {
registerReceiver(mPackageReceiver, filter);
new CategoriesUpdateTask().execute();
- final Intent intent = getIntent();
- if (intent != null && intent.getBooleanExtra(EXTRA_SHOW_MENU, false)) {
- // Intent explicitly set to show menu.
- showMenuIcon();
- }
}
@Override
@@ -125,13 +121,6 @@ public class SettingsDrawerActivity extends Activity {
mCategoryListeners.remove(listener);
}
- public void setContentHeaderView(View headerView) {
- mContentHeaderContainer.removeAllViews();
- if (headerView != null) {
- mContentHeaderContainer.addView(headerView);
- }
- }
-
@Override
public void setContentView(@LayoutRes int layoutResID) {
final ViewGroup parent = findViewById(R.id.content_frame);
@@ -151,13 +140,6 @@ public class SettingsDrawerActivity extends Activity {
((ViewGroup) findViewById(R.id.content_frame)).addView(view, params);
}
- private void showMenuIcon() {
- final ActionBar actionBar = getActionBar();
- if (actionBar != null) {
- actionBar.setDisplayHomeAsUpEnabled(true);
- }
- }
-
private void onCategoriesChanged() {
final int N = mCategoryListeners.size();
for (int i = 0; i < N; i++) {
@@ -165,10 +147,6 @@ public class SettingsDrawerActivity extends Activity {
}
}
- public void onProfileTileOpen() {
- finish();
- }
-
/**
* @return whether or not the enabled state actually changed.
*/
diff --git a/packages/SettingsLib/src/com/android/settingslib/drawer/TileUtils.java b/packages/SettingsLib/src/com/android/settingslib/drawer/TileUtils.java
index 6e676bc4f460..e986e0f78de4 100644
--- a/packages/SettingsLib/src/com/android/settingslib/drawer/TileUtils.java
+++ b/packages/SettingsLib/src/com/android/settingslib/drawer/TileUtils.java
@@ -65,7 +65,7 @@ public class TileUtils {
*
* <p>A summary my be defined by meta-data named {@link #META_DATA_PREFERENCE_SUMMARY}
*/
- private static final String EXTRA_SETTINGS_ACTION =
+ public static final String EXTRA_SETTINGS_ACTION =
"com.android.settings.action.EXTRA_SETTINGS";
/**
@@ -149,13 +149,6 @@ public class TileUtils {
public static final String META_DATA_PREFERENCE_TITLE = "com.android.settings.title";
/**
- * @deprecated Use {@link #META_DATA_PREFERENCE_TITLE} with {@code android:resource}
- */
- @Deprecated
- public static final String META_DATA_PREFERENCE_TITLE_RES_ID =
- "com.android.settings.title.resid";
-
- /**
* Name of the meta-data item that should be set in the AndroidManifest.xml
* to specify the summary text that should be displayed for the preference.
*/
@@ -176,7 +169,7 @@ public class TileUtils {
* custom view which should be displayed for the preference. The custom view will be inflated
* as a remote view.
*
- * This also can be used with {@link META_DATA_PREFERENCE_SUMMARY_URI} above, by setting the id
+ * This also can be used with {@link #META_DATA_PREFERENCE_SUMMARY_URI}, by setting the id
* of the summary TextView to '@android:id/summary'.
*/
public static final String META_DATA_PREFERENCE_CUSTOM_VIEW =
@@ -260,7 +253,7 @@ public class TileUtils {
}
ArrayList<DashboardCategory> categories = new ArrayList<>(categoryMap.values());
for (DashboardCategory category : categories) {
- Collections.sort(category.tiles, TILE_COMPARATOR);
+ category.sortTiles();
}
Collections.sort(categories, CATEGORY_COMPARATOR);
if (DEBUG_TIMING) Log.d(LOG_TAG, "getCategories took "
@@ -421,14 +414,7 @@ public class TileUtils {
metaData.getBoolean(META_DATA_PREFERENCE_ICON_TINTABLE);
}
}
- int resId = 0;
- if (metaData.containsKey(META_DATA_PREFERENCE_TITLE_RES_ID)) {
- resId = metaData.getInt(META_DATA_PREFERENCE_TITLE_RES_ID);
- if (resId != 0) {
- title = res.getString(resId);
- }
- }
- if ((resId == 0) && metaData.containsKey(META_DATA_PREFERENCE_TITLE)) {
+ if (metaData.containsKey(META_DATA_PREFERENCE_TITLE)) {
if (metaData.get(META_DATA_PREFERENCE_TITLE) instanceof Integer) {
title = res.getString(metaData.getInt(META_DATA_PREFERENCE_TITLE));
} else {
@@ -466,12 +452,14 @@ public class TileUtils {
}
// Set the icon
- if (iconFromUri != null) {
- tile.icon = Icon.createWithResource(iconFromUri.first, iconFromUri.second);
- } else {
- if (icon == 0) {
+ if (icon == 0) {
+ // Only fallback to activityinfo.icon if metadata does not contain ICON_URI.
+ // ICON_URI should be loaded in app UI when need the icon object.
+ if (!tile.metaData.containsKey(META_DATA_PREFERENCE_ICON_URI)) {
icon = activityInfo.icon;
}
+ }
+ if (icon != 0) {
tile.icon = Icon.createWithResource(activityInfo.packageName, icon);
}
@@ -513,7 +501,7 @@ public class TileUtils {
/**
* Gets the icon package name and resource id from content provider.
- * @param Context context
+ * @param context context
* @param packageName package name of the target activity
* @param uriString URI for the content provider
* @param providerMap Maps URI authorities to providers
@@ -543,7 +531,7 @@ public class TileUtils {
/**
* Gets text associated with the input key from the content provider.
- * @param Context context
+ * @param context context
* @param uriString URI for the content provider
* @param providerMap Maps URI authorities to providers
* @param key Key mapping to the text in bundle returned by the content provider
@@ -607,14 +595,6 @@ public class TileUtils {
return pathSegments.get(0);
}
- public static final Comparator<Tile> TILE_COMPARATOR =
- new Comparator<Tile>() {
- @Override
- public int compare(Tile lhs, Tile rhs) {
- return rhs.priority - lhs.priority;
- }
- };
-
private static final Comparator<DashboardCategory> CATEGORY_COMPARATOR =
new Comparator<DashboardCategory>() {
@Override
diff --git a/packages/SettingsLib/src/com/android/settingslib/drawer/UserAdapter.java b/packages/SettingsLib/src/com/android/settingslib/drawer/UserAdapter.java
index 750601d82ce5..8a09df2849c9 100644
--- a/packages/SettingsLib/src/com/android/settingslib/drawer/UserAdapter.java
+++ b/packages/SettingsLib/src/com/android/settingslib/drawer/UserAdapter.java
@@ -57,14 +57,15 @@ public class UserAdapter implements SpinnerAdapter, ListAdapter {
if (userInfo.isManagedProfile()) {
mName = context.getString(R.string.managed_user_title);
icon = context.getDrawable(
- com.android.internal.R.drawable.ic_corp_icon);
+ com.android.internal.R.drawable.ic_corp_badge);
} else {
mName = userInfo.name;
final int userId = userInfo.id;
if (um.getUserIcon(userId) != null) {
icon = new BitmapDrawable(context.getResources(), um.getUserIcon(userId));
} else {
- icon = UserIcons.getDefaultUserIcon(userId, /* light= */ false);
+ icon = UserIcons.getDefaultUserIcon(
+ context.getResources(), userId, /* light= */ false);
}
}
this.mIcon = encircle(context, icon);
diff --git a/packages/SettingsLib/src/com/android/settingslib/fuelgauge/PowerWhitelistBackend.java b/packages/SettingsLib/src/com/android/settingslib/fuelgauge/PowerWhitelistBackend.java
new file mode 100644
index 000000000000..2b6d09f9b72e
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/fuelgauge/PowerWhitelistBackend.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.fuelgauge;
+
+import android.os.IDeviceIdleController;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.support.annotation.VisibleForTesting;
+import android.util.ArraySet;
+import android.util.Log;
+
+/**
+ * Handles getting/changing the whitelist for the exceptions to battery saving features.
+ */
+public class PowerWhitelistBackend {
+
+ private static final String TAG = "PowerWhitelistBackend";
+
+ private static final String DEVICE_IDLE_SERVICE = "deviceidle";
+
+ private static PowerWhitelistBackend sInstance;
+
+ private final IDeviceIdleController mDeviceIdleService;
+ private final ArraySet<String> mWhitelistedApps = new ArraySet<>();
+ private final ArraySet<String> mSysWhitelistedApps = new ArraySet<>();
+
+ public PowerWhitelistBackend() {
+ mDeviceIdleService = IDeviceIdleController.Stub.asInterface(
+ ServiceManager.getService(DEVICE_IDLE_SERVICE));
+ refreshList();
+ }
+
+ @VisibleForTesting
+ PowerWhitelistBackend(IDeviceIdleController deviceIdleService) {
+ mDeviceIdleService = deviceIdleService;
+ refreshList();
+ }
+
+ public int getWhitelistSize() {
+ return mWhitelistedApps.size();
+ }
+
+ public boolean isSysWhitelisted(String pkg) {
+ return mSysWhitelistedApps.contains(pkg);
+ }
+
+ public boolean isWhitelisted(String pkg) {
+ return mWhitelistedApps.contains(pkg);
+ }
+
+ public void addApp(String pkg) {
+ try {
+ mDeviceIdleService.addPowerSaveWhitelistApp(pkg);
+ mWhitelistedApps.add(pkg);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Unable to reach IDeviceIdleController", e);
+ }
+ }
+
+ public void removeApp(String pkg) {
+ try {
+ mDeviceIdleService.removePowerSaveWhitelistApp(pkg);
+ mWhitelistedApps.remove(pkg);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Unable to reach IDeviceIdleController", e);
+ }
+ }
+
+ @VisibleForTesting
+ public void refreshList() {
+ mSysWhitelistedApps.clear();
+ mWhitelistedApps.clear();
+ try {
+ String[] whitelistedApps = mDeviceIdleService.getFullPowerWhitelist();
+ for (String app : whitelistedApps) {
+ mWhitelistedApps.add(app);
+ }
+ String[] sysWhitelistedApps = mDeviceIdleService.getSystemPowerWhitelist();
+ for (String app : sysWhitelistedApps) {
+ mSysWhitelistedApps.add(app);
+ }
+ } catch (RemoteException e) {
+ Log.w(TAG, "Unable to reach IDeviceIdleController", e);
+ }
+ }
+
+ public static PowerWhitelistBackend getInstance() {
+ if (sInstance == null) {
+ sInstance = new PowerWhitelistBackend();
+ }
+ return sInstance;
+ }
+
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/graph/BatteryMeterDrawableBase.java b/packages/SettingsLib/src/com/android/settingslib/graph/BatteryMeterDrawableBase.java
index ec45b7e91700..4fe9d56a4179 100755
--- a/packages/SettingsLib/src/com/android/settingslib/graph/BatteryMeterDrawableBase.java
+++ b/packages/SettingsLib/src/com/android/settingslib/graph/BatteryMeterDrawableBase.java
@@ -16,7 +16,6 @@
package com.android.settingslib.graph;
-import android.animation.ArgbEvaluator;
import android.annotation.Nullable;
import android.content.Context;
import android.content.res.Resources;
@@ -90,7 +89,6 @@ public class BatteryMeterDrawableBase extends Drawable {
private final RectF mPlusFrame = new RectF();
private final Path mShapePath = new Path();
- private final Path mClipPath = new Path();
private final Path mTextPath = new Path();
public BatteryMeterDrawableBase(Context context, int frameColor) {
@@ -101,7 +99,7 @@ public class BatteryMeterDrawableBase extends Drawable {
final int N = levels.length();
mColors = new int[2 * N];
- for (int i=0; i < N; i++) {
+ for (int i = 0; i < N; i++) {
mColors[2 * i] = levels.getInt(i, 0);
if (colors.getType(i) == TypedValue.TYPE_ATTRIBUTE) {
mColors[2 * i + 1] = Utils.getColorAttr(context, colors.getThemeAttributeId(i, 0));
@@ -416,8 +414,8 @@ public class BatteryMeterDrawableBase extends Drawable {
: (mLevel == 100 ? 0.38f : 0.5f)));
mTextHeight = -mTextPaint.getFontMetrics().ascent;
pctText = String.valueOf(SINGLE_DIGIT_PERCENT ? (level / 10) : level);
- pctX = mWidth * 0.5f;
- pctY = (mHeight + mTextHeight) * 0.47f;
+ pctX = mWidth * 0.5f + left;
+ pctY = (mHeight + mTextHeight) * 0.47f + top;
pctOpaque = levelTop > pctY;
if (!pctOpaque) {
mTextPath.reset();
@@ -432,16 +430,16 @@ public class BatteryMeterDrawableBase extends Drawable {
// draw the battery shape, clipped to charging level
mFrame.top = levelTop;
- mClipPath.reset();
- mClipPath.addRect(mFrame, Path.Direction.CCW);
- mShapePath.op(mClipPath, Path.Op.INTERSECT);
+ c.save();
+ c.clipRect(mFrame);
c.drawPath(mShapePath, mBatteryPaint);
+ c.restore();
if (!mCharging && !mPowerSaveEnabled) {
if (level <= mCriticalLevel) {
// draw the warning text
- final float x = mWidth * 0.5f;
- final float y = (mHeight + mWarningTextHeight) * 0.48f;
+ final float x = mWidth * 0.5f + left;
+ final float y = (mHeight + mWarningTextHeight) * 0.48f + top;
c.drawText(mWarningString, x, y, mWarningTextPaint);
} else if (pctOpaque) {
// draw the percentage text
diff --git a/packages/SettingsLib/src/com/android/settingslib/graph/SignalDrawable.java b/packages/SettingsLib/src/com/android/settingslib/graph/SignalDrawable.java
new file mode 100644
index 000000000000..846e30d50063
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/graph/SignalDrawable.java
@@ -0,0 +1,517 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package com.android.settingslib.graph;
+
+import android.animation.ArgbEvaluator;
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.Path.Direction;
+import android.graphics.Path.FillType;
+import android.graphics.Path.Op;
+import android.graphics.PointF;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.drawable.Drawable;
+import android.os.Handler;
+import android.util.LayoutDirection;
+
+import com.android.settingslib.R;
+import com.android.settingslib.Utils;
+
+public class SignalDrawable extends Drawable {
+
+ private static final String TAG = "SignalDrawable";
+
+ private static final int NUM_DOTS = 3;
+
+ private static final float VIEWPORT = 24f;
+ private static final float PAD = 2f / VIEWPORT;
+ private static final float CUT_OUT = 7.9f / VIEWPORT;
+
+ private static final float DOT_SIZE = 3f / VIEWPORT;
+ private static final float DOT_PADDING = 1f / VIEWPORT;
+ private static final float DOT_CUT_WIDTH = (DOT_SIZE * 3) + (DOT_PADDING * 5);
+ private static final float DOT_CUT_HEIGHT = (DOT_SIZE * 1) + (DOT_PADDING * 1);
+
+ private static final float[] FIT = {2.26f, -3.02f, 1.76f};
+
+ // All of these are masks to push all of the drawable state into one int for easy callbacks
+ // and flow through sysui.
+ private static final int LEVEL_MASK = 0xff;
+ private static final int NUM_LEVEL_SHIFT = 8;
+ private static final int NUM_LEVEL_MASK = 0xff << NUM_LEVEL_SHIFT;
+ private static final int STATE_SHIFT = 16;
+ private static final int STATE_MASK = 0xff << STATE_SHIFT;
+ private static final int STATE_NONE = 0;
+ private static final int STATE_EMPTY = 1;
+ private static final int STATE_CUT = 2;
+ private static final int STATE_CARRIER_CHANGE = 3;
+ private static final int STATE_AIRPLANE = 4;
+
+ private static final long DOT_DELAY = 1000;
+
+ private static float[][] X_PATH = new float[][]{
+ {21.9f / VIEWPORT, 17.0f / VIEWPORT},
+ {-1.1f / VIEWPORT, -1.1f / VIEWPORT},
+ {-1.9f / VIEWPORT, 1.9f / VIEWPORT},
+ {-1.9f / VIEWPORT, -1.9f / VIEWPORT},
+ {-1.1f / VIEWPORT, 1.1f / VIEWPORT},
+ {1.9f / VIEWPORT, 1.9f / VIEWPORT},
+ {-1.9f / VIEWPORT, 1.9f / VIEWPORT},
+ {1.1f / VIEWPORT, 1.1f / VIEWPORT},
+ {1.9f / VIEWPORT, -1.9f / VIEWPORT},
+ {1.9f / VIEWPORT, 1.9f / VIEWPORT},
+ {1.1f / VIEWPORT, -1.1f / VIEWPORT},
+ {-1.9f / VIEWPORT, -1.9f / VIEWPORT},
+ };
+
+ // Rounded corners are achieved by arcing a circle of radius `R` from its tangent points along
+ // the curve (curve ≡ triangle). On the top and left corners of the triangle, the tangents are
+ // as follows:
+ // 1) Along the straight lines (y = 0 and x = width):
+ // Ps = circleOffset + R
+ // 2) Along the diagonal line (y = x):
+ // Pd = √((Ps^2) / 2)
+ // or (remember: sin(π/4) ≈ 0.7071)
+ // Pd = (circleOffset + R - 0.7071, height - R - 0.7071)
+ // Where Pd is the (x,y) coords of the point that intersects the circle at the bottom
+ // left of the triangle
+ private static final float RADIUS_RATIO = 0.75f / 17f;
+ private static final float DIAG_OFFSET_MULTIPLIER = 0.707107f;
+ // How far the circle defining the corners is inset from the edges
+ private final float mAppliedCornerInset;
+
+ private static final float INV_TAN = 1f / (float) Math.tan(Math.PI / 8f);
+ private static final float CUT_WIDTH_DP = 1f / 12f;
+
+ // Where the top and left points of the triangle would be if not for rounding
+ private final PointF mVirtualTop = new PointF();
+ private final PointF mVirtualLeft = new PointF();
+
+ private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ private final Paint mForegroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ private final int mDarkModeBackgroundColor;
+ private final int mDarkModeFillColor;
+ private final int mLightModeBackgroundColor;
+ private final int mLightModeFillColor;
+ private final Path mFullPath = new Path();
+ private final Path mForegroundPath = new Path();
+ private final Path mXPath = new Path();
+ // Cut out when STATE_EMPTY
+ private final Path mCutPath = new Path();
+ // Draws the slash when in airplane mode
+ private final SlashArtist mSlash = new SlashArtist();
+ private final Handler mHandler;
+ private float mOldDarkIntensity = -1;
+ private float mNumLevels = 1;
+ private int mIntrinsicSize;
+ private int mLevel;
+ private int mState;
+ private boolean mVisible;
+ private boolean mAnimating;
+ private int mCurrentDot;
+
+ public SignalDrawable(Context context) {
+ mDarkModeBackgroundColor =
+ Utils.getDefaultColor(context, R.color.dark_mode_icon_color_dual_tone_background);
+ mDarkModeFillColor =
+ Utils.getDefaultColor(context, R.color.dark_mode_icon_color_dual_tone_fill);
+ mLightModeBackgroundColor =
+ Utils.getDefaultColor(context, R.color.light_mode_icon_color_dual_tone_background);
+ mLightModeFillColor =
+ Utils.getDefaultColor(context, R.color.light_mode_icon_color_dual_tone_fill);
+ mIntrinsicSize = context.getResources().getDimensionPixelSize(R.dimen.signal_icon_size);
+
+ mHandler = new Handler();
+ setDarkIntensity(0);
+
+ mAppliedCornerInset = context.getResources()
+ .getDimensionPixelSize(R.dimen.stat_sys_mobile_signal_circle_inset);
+ }
+
+ public void setIntrinsicSize(int size) {
+ mIntrinsicSize = size;
+ }
+
+ @Override
+ public int getIntrinsicWidth() {
+ return mIntrinsicSize;
+ }
+
+ @Override
+ public int getIntrinsicHeight() {
+ return mIntrinsicSize;
+ }
+
+ public void setNumLevels(int levels) {
+ if (levels == mNumLevels) return;
+ mNumLevels = levels;
+ invalidateSelf();
+ }
+
+ private void setSignalState(int state) {
+ if (state == mState) return;
+ mState = state;
+ updateAnimation();
+ invalidateSelf();
+ }
+
+ private void updateAnimation() {
+ boolean shouldAnimate = (mState == STATE_CARRIER_CHANGE) && mVisible;
+ if (shouldAnimate == mAnimating) return;
+ mAnimating = shouldAnimate;
+ if (shouldAnimate) {
+ mChangeDot.run();
+ } else {
+ mHandler.removeCallbacks(mChangeDot);
+ }
+ }
+
+ @Override
+ protected boolean onLevelChange(int state) {
+ setNumLevels(getNumLevels(state));
+ setSignalState(getState(state));
+ int level = getLevel(state);
+ if (level != mLevel) {
+ mLevel = level;
+ invalidateSelf();
+ }
+ return true;
+ }
+
+ public void setColors(int background, int foreground) {
+ mPaint.setColor(background);
+ mForegroundPaint.setColor(foreground);
+ }
+
+ public void setDarkIntensity(float darkIntensity) {
+ if (darkIntensity == mOldDarkIntensity) {
+ return;
+ }
+ mPaint.setColor(getBackgroundColor(darkIntensity));
+ mForegroundPaint.setColor(getFillColor(darkIntensity));
+ mOldDarkIntensity = darkIntensity;
+ invalidateSelf();
+ }
+
+ private int getFillColor(float darkIntensity) {
+ return getColorForDarkIntensity(
+ darkIntensity, mLightModeFillColor, mDarkModeFillColor);
+ }
+
+ private int getBackgroundColor(float darkIntensity) {
+ return getColorForDarkIntensity(
+ darkIntensity, mLightModeBackgroundColor, mDarkModeBackgroundColor);
+ }
+
+ private int getColorForDarkIntensity(float darkIntensity, int lightColor, int darkColor) {
+ return (int) ArgbEvaluator.getInstance().evaluate(darkIntensity, lightColor, darkColor);
+ }
+
+ @Override
+ protected void onBoundsChange(Rect bounds) {
+ super.onBoundsChange(bounds);
+ invalidateSelf();
+ }
+
+ @Override
+ public void draw(@NonNull Canvas canvas) {
+ final float width = getBounds().width();
+ final float height = getBounds().height();
+
+ boolean isRtl = getLayoutDirection() == LayoutDirection.RTL;
+ if (isRtl) {
+ canvas.save();
+ // Mirror the drawable
+ canvas.translate(width, 0);
+ canvas.scale(-1.0f, 1.0f);
+ }
+ mFullPath.reset();
+ mFullPath.setFillType(FillType.WINDING);
+
+ final float padding = Math.round(PAD * width);
+ final float cornerRadius = RADIUS_RATIO * height;
+ // Offset from circle where the hypotenuse meets the circle
+ final float diagOffset = DIAG_OFFSET_MULTIPLIER * cornerRadius;
+
+ // 1 - Bottom right, above corner
+ mFullPath.moveTo(width - padding, height - padding - cornerRadius);
+ // 2 - Line to top right, below corner
+ mFullPath.lineTo(width - padding, padding + cornerRadius + mAppliedCornerInset);
+ // 3 - Arc to top right, on hypotenuse
+ mFullPath.arcTo(
+ width - padding - (2 * cornerRadius),
+ padding + mAppliedCornerInset,
+ width - padding,
+ padding + mAppliedCornerInset + (2 * cornerRadius),
+ 0.f, -135.f, false
+ );
+ // 4 - Line to bottom left, on hypotenuse
+ mFullPath.lineTo(padding + mAppliedCornerInset + cornerRadius - diagOffset,
+ height - padding - cornerRadius - diagOffset);
+ // 5 - Arc to bottom left, on leg
+ mFullPath.arcTo(
+ padding + mAppliedCornerInset,
+ height - padding - (2 * cornerRadius),
+ padding + mAppliedCornerInset + ( 2 * cornerRadius),
+ height - padding,
+ -135.f, -135.f, false
+ );
+ // 6 - Line to bottom rght, before corner
+ mFullPath.lineTo(width - padding - cornerRadius, height - padding);
+ // 7 - Arc to beginning (bottom right, above corner)
+ mFullPath.arcTo(
+ width - padding - (2 * cornerRadius),
+ height - padding - (2 * cornerRadius),
+ width - padding,
+ height - padding,
+ 90.f, -90.f, false
+ );
+
+ if (mState == STATE_CARRIER_CHANGE) {
+ float cutWidth = (DOT_CUT_WIDTH * width);
+ float cutHeight = (DOT_CUT_HEIGHT * width);
+ float dotSize = (DOT_SIZE * height);
+ float dotPadding = (DOT_PADDING * height);
+
+ mFullPath.moveTo(width - padding, height - padding);
+ mFullPath.rLineTo(-cutWidth, 0);
+ mFullPath.rLineTo(0, -cutHeight);
+ mFullPath.rLineTo(cutWidth, 0);
+ mFullPath.rLineTo(0, cutHeight);
+ float dotSpacing = dotPadding * 2 + dotSize;
+ float x = width - padding - dotSize;
+ float y = height - padding - dotSize;
+ mForegroundPath.reset();
+ drawDot(mFullPath, mForegroundPath, x, y, dotSize, 2);
+ drawDot(mFullPath, mForegroundPath, x - dotSpacing, y, dotSize, 1);
+ drawDot(mFullPath, mForegroundPath, x - dotSpacing * 2, y, dotSize, 0);
+ } else if (mState == STATE_CUT) {
+ float cut = (CUT_OUT * width);
+ mFullPath.moveTo(width - padding, height - padding);
+ mFullPath.rLineTo(-cut, 0);
+ mFullPath.rLineTo(0, -cut);
+ mFullPath.rLineTo(cut, 0);
+ mFullPath.rLineTo(0, cut);
+ }
+
+ if (mState == STATE_EMPTY) {
+ // Where the corners would be if this were a real triangle
+ mVirtualTop.set(
+ width - padding,
+ (padding + cornerRadius + mAppliedCornerInset) - (INV_TAN * cornerRadius));
+ mVirtualLeft.set(
+ (padding + cornerRadius + mAppliedCornerInset) - (INV_TAN * cornerRadius),
+ height - padding);
+
+ final float cutWidth = CUT_WIDTH_DP * height;
+ final float cutDiagInset = cutWidth * INV_TAN;
+
+ // Cut out a smaller triangle from the center of mFullPath
+ mCutPath.reset();
+ mCutPath.setFillType(FillType.WINDING);
+ mCutPath.moveTo(width - padding - cutWidth, height - padding - cutWidth);
+ mCutPath.lineTo(width - padding - cutWidth, mVirtualTop.y + cutDiagInset);
+ mCutPath.lineTo(mVirtualLeft.x + cutDiagInset, height - padding - cutWidth);
+ mCutPath.lineTo(width - padding - cutWidth, height - padding - cutWidth);
+
+ // Draw empty state as only background
+ mForegroundPath.reset();
+ mFullPath.op(mCutPath, Path.Op.DIFFERENCE);
+ } else if (mState == STATE_AIRPLANE) {
+ // Airplane mode is slashed, fully drawn background
+ mForegroundPath.reset();
+ mSlash.draw((int) height, (int) width, canvas, mPaint);
+ } else if (mState != STATE_CARRIER_CHANGE) {
+ mForegroundPath.reset();
+ int sigWidth = Math.round(calcFit(mLevel / (mNumLevels - 1)) * (width - 2 * padding));
+ mForegroundPath.addRect(padding, padding, padding + sigWidth, height - padding,
+ Direction.CW);
+ mForegroundPath.op(mFullPath, Op.INTERSECT);
+ }
+
+ canvas.drawPath(mFullPath, mPaint);
+ canvas.drawPath(mForegroundPath, mForegroundPaint);
+ if (mState == STATE_CUT) {
+ mXPath.reset();
+ mXPath.moveTo(X_PATH[0][0] * width, X_PATH[0][1] * height);
+ for (int i = 1; i < X_PATH.length; i++) {
+ mXPath.rLineTo(X_PATH[i][0] * width, X_PATH[i][1] * height);
+ }
+ canvas.drawPath(mXPath, mForegroundPaint);
+ }
+ if (isRtl) {
+ canvas.restore();
+ }
+ }
+
+ private void drawDot(Path fullPath, Path foregroundPath, float x, float y, float dotSize,
+ int i) {
+ Path p = (i == mCurrentDot) ? foregroundPath : fullPath;
+ p.addRect(x, y, x + dotSize, y + dotSize, Direction.CW);
+ }
+
+ // This is a fit line based on previous values of provided in assets, but if
+ // you look at the a plot of this actual fit, it makes a lot of sense, what it does
+ // is compress the areas that are very visually easy to see changes (the middle sections)
+ // and spread out the sections that are hard to see (each end of the icon).
+ // The current fit is cubic, but pretty easy to change the way the code is written (just add
+ // terms to the end of FIT).
+ private float calcFit(float v) {
+ float ret = 0;
+ float t = v;
+ for (int i = 0; i < FIT.length; i++) {
+ ret += FIT[i] * t;
+ t *= v;
+ }
+ return ret;
+ }
+
+ @Override
+ public int getAlpha() {
+ return mPaint.getAlpha();
+ }
+
+ @Override
+ public void setAlpha(@IntRange(from = 0, to = 255) int alpha) {
+ mPaint.setAlpha(alpha);
+ mForegroundPaint.setAlpha(alpha);
+ }
+
+ @Override
+ public void setColorFilter(@Nullable ColorFilter colorFilter) {
+ mPaint.setColorFilter(colorFilter);
+ mForegroundPaint.setColorFilter(colorFilter);
+ }
+
+ @Override
+ public int getOpacity() {
+ return 255;
+ }
+
+ @Override
+ public boolean setVisible(boolean visible, boolean restart) {
+ mVisible = visible;
+ updateAnimation();
+ return super.setVisible(visible, restart);
+ }
+
+ private final Runnable mChangeDot = new Runnable() {
+ @Override
+ public void run() {
+ if (++mCurrentDot == NUM_DOTS) {
+ mCurrentDot = 0;
+ }
+ invalidateSelf();
+ mHandler.postDelayed(mChangeDot, DOT_DELAY);
+ }
+ };
+
+ public static int getLevel(int fullState) {
+ return fullState & LEVEL_MASK;
+ }
+
+ public static int getState(int fullState) {
+ return (fullState & STATE_MASK) >> STATE_SHIFT;
+ }
+
+ public static int getNumLevels(int fullState) {
+ return (fullState & NUM_LEVEL_MASK) >> NUM_LEVEL_SHIFT;
+ }
+
+ public static int getState(int level, int numLevels, boolean cutOut) {
+ return ((cutOut ? STATE_CUT : 0) << STATE_SHIFT)
+ | (numLevels << NUM_LEVEL_SHIFT)
+ | level;
+ }
+
+ public static int getCarrierChangeState(int numLevels) {
+ return (STATE_CARRIER_CHANGE << STATE_SHIFT) | (numLevels << NUM_LEVEL_SHIFT);
+ }
+
+ public static int getEmptyState(int numLevels) {
+ return (STATE_EMPTY << STATE_SHIFT) | (numLevels << NUM_LEVEL_SHIFT);
+ }
+
+ public static int getAirplaneModeState(int numLevels) {
+ return (STATE_AIRPLANE << STATE_SHIFT) | (numLevels << NUM_LEVEL_SHIFT);
+ }
+
+ private final class SlashArtist {
+ private static final float CORNER_RADIUS = 1f;
+ // These values are derived in un-rotated (vertical) orientation
+ private static final float SLASH_WIDTH = 1.8384776f;
+ private static final float SLASH_HEIGHT = 22f;
+ private static final float CENTER_X = 10.65f;
+ private static final float CENTER_Y = 15.869239f;
+ private static final float SCALE = 24f;
+
+ // Bottom is derived during animation
+ private static final float LEFT = (CENTER_X - (SLASH_WIDTH / 2)) / SCALE;
+ private static final float TOP = (CENTER_Y - (SLASH_HEIGHT / 2)) / SCALE;
+ private static final float RIGHT = (CENTER_X + (SLASH_WIDTH / 2)) / SCALE;
+ private static final float BOTTOM = (CENTER_Y + (SLASH_HEIGHT / 2)) / SCALE;
+ // Draw the slash washington-monument style; rotate to no-u-turn style
+ private static final float ROTATION = -45f;
+
+ private final Path mPath = new Path();
+ private final RectF mSlashRect = new RectF();
+
+ void draw(int height, int width, @NonNull Canvas canvas, Paint paint) {
+ Matrix m = new Matrix();
+ final float radius = scale(CORNER_RADIUS, width);
+ updateRect(
+ scale(LEFT, width),
+ scale(TOP, height),
+ scale(RIGHT, width),
+ scale(BOTTOM, height));
+
+ mPath.reset();
+ // Draw the slash vertically
+ mPath.addRoundRect(mSlashRect, radius, radius, Direction.CW);
+ m.setRotate(ROTATION, width / 2, height / 2);
+ mPath.transform(m);
+ canvas.drawPath(mPath, paint);
+
+ // Rotate back to vertical, and draw the cut-out rect next to this one
+ m.setRotate(-ROTATION, width / 2, height / 2);
+ mPath.transform(m);
+ m.setTranslate(mSlashRect.width(), 0);
+ mPath.transform(m);
+ mPath.addRoundRect(mSlashRect, radius, radius, Direction.CW);
+ m.setRotate(ROTATION, width / 2, height / 2);
+ mPath.transform(m);
+ canvas.clipOutPath(mPath);
+ }
+
+ void updateRect(float left, float top, float right, float bottom) {
+ mSlashRect.left = left;
+ mSlashRect.top = top;
+ mSlashRect.right = right;
+ mSlashRect.bottom = bottom;
+ }
+
+ private float scale(float frac, int width) {
+ return frac * width;
+ }
+ }
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/inputmethod/InputMethodPreference.java b/packages/SettingsLib/src/com/android/settingslib/inputmethod/InputMethodPreference.java
index 1bbc878b56c9..5e25f519a130 100755..100644
--- a/packages/SettingsLib/src/com/android/settingslib/inputmethod/InputMethodPreference.java
+++ b/packages/SettingsLib/src/com/android/settingslib/inputmethod/InputMethodPreference.java
@@ -34,6 +34,7 @@ import android.view.inputmethod.InputMethodManager;
import android.view.inputmethod.InputMethodSubtype;
import android.widget.Toast;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.inputmethod.InputMethodUtils;
import com.android.settingslib.R;
import com.android.settingslib.RestrictedLockUtils;
@@ -91,20 +92,28 @@ public class InputMethodPreference extends RestrictedSwitchPreference implements
public InputMethodPreference(final Context context, final InputMethodInfo imi,
final boolean isImeEnabler, final boolean isAllowedByOrganization,
final OnSavePreferenceListener onSaveListener) {
+ this(context, imi, imi.loadLabel(context.getPackageManager()), isAllowedByOrganization,
+ onSaveListener);
+ if (!isImeEnabler) {
+ // Remove switch widget.
+ setWidgetLayoutResource(NO_WIDGET);
+ }
+ }
+
+ @VisibleForTesting
+ InputMethodPreference(final Context context, final InputMethodInfo imi,
+ final CharSequence title, final boolean isAllowedByOrganization,
+ final OnSavePreferenceListener onSaveListener) {
super(context);
setPersistent(false);
mImi = imi;
mIsAllowedByOrganization = isAllowedByOrganization;
mOnSaveListener = onSaveListener;
- if (!isImeEnabler) {
- // Remove switch widget.
- setWidgetLayoutResource(NO_WIDGET);
- }
// Disable on/off switch texts.
setSwitchTextOn(EMPTY_TEXT);
setSwitchTextOff(EMPTY_TEXT);
setKey(imi.getId());
- setTitle(imi.loadLabel(context.getPackageManager()));
+ setTitle(title);
final String settingsActivity = imi.getSettingsActivity();
if (TextUtils.isEmpty(settingsActivity)) {
setIntent(null);
@@ -283,18 +292,18 @@ public class InputMethodPreference extends RestrictedSwitchPreference implements
if (this == rhs) {
return 0;
}
- if (mHasPriorityInSorting == rhs.mHasPriorityInSorting) {
- final CharSequence t0 = getTitle();
- final CharSequence t1 = rhs.getTitle();
- if (TextUtils.isEmpty(t0)) {
- return 1;
- }
- if (TextUtils.isEmpty(t1)) {
- return -1;
- }
- return collator.compare(t0.toString(), t1.toString());
+ if (mHasPriorityInSorting != rhs.mHasPriorityInSorting) {
+ // Prefer always checked system IMEs
+ return mHasPriorityInSorting ? -1 : 1;
+ }
+ final CharSequence title = getTitle();
+ final CharSequence rhsTitle = rhs.getTitle();
+ final boolean emptyTitle = TextUtils.isEmpty(title);
+ final boolean rhsEmptyTitle = TextUtils.isEmpty(rhsTitle);
+ if (!emptyTitle && !rhsEmptyTitle) {
+ return collator.compare(title.toString(), rhsTitle.toString());
}
- // Prefer always checked system IMEs
- return mHasPriorityInSorting ? -1 : 1;
+ // For historical reasons, an empty text needs to be put at the first.
+ return (emptyTitle ? -1 : 0) - (rhsEmptyTitle ? -1 : 0);
}
}
diff --git a/packages/SettingsLib/src/com/android/settingslib/inputmethod/InputMethodSubtypePreference.java b/packages/SettingsLib/src/com/android/settingslib/inputmethod/InputMethodSubtypePreference.java
index 5fdab2967d1e..f824ec75968b 100644
--- a/packages/SettingsLib/src/com/android/settingslib/inputmethod/InputMethodSubtypePreference.java
+++ b/packages/SettingsLib/src/com/android/settingslib/inputmethod/InputMethodSubtypePreference.java
@@ -17,12 +17,12 @@
package com.android.settingslib.inputmethod;
import android.content.Context;
-import android.support.v14.preference.SwitchPreference;
import android.support.v7.preference.Preference;
import android.text.TextUtils;
import android.view.inputmethod.InputMethodInfo;
import android.view.inputmethod.InputMethodSubtype;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.inputmethod.InputMethodUtils;
import java.text.Collator;
@@ -39,18 +39,28 @@ public class InputMethodSubtypePreference extends SwitchWithNoTextPreference {
public InputMethodSubtypePreference(final Context context, final InputMethodSubtype subtype,
final InputMethodInfo imi) {
+ this(context,
+ imi.getId() + subtype.hashCode(),
+ InputMethodAndSubtypeUtil.getSubtypeLocaleNameAsSentence(subtype, context, imi),
+ subtype.getLocale(),
+ context.getResources().getConfiguration().locale);
+ }
+
+ @VisibleForTesting
+ InputMethodSubtypePreference(
+ final Context context,
+ final String prefKey,
+ final CharSequence title,
+ final String subtypeLocaleString,
+ final Locale systemLocale) {
super(context);
setPersistent(false);
- setKey(imi.getId() + subtype.hashCode());
- final CharSequence subtypeLabel =
- InputMethodAndSubtypeUtil.getSubtypeLocaleNameAsSentence(subtype, context, imi);
- setTitle(subtypeLabel);
- final String subtypeLocaleString = subtype.getLocale();
+ setKey(prefKey);
+ setTitle(title);
if (TextUtils.isEmpty(subtypeLocaleString)) {
mIsSystemLocale = false;
mIsSystemLanguage = false;
} else {
- final Locale systemLocale = context.getResources().getConfiguration().locale;
mIsSystemLocale = subtypeLocaleString.equals(systemLocale.toString());
mIsSystemLanguage = mIsSystemLocale
|| InputMethodUtils.getLanguageFromLocaleString(subtypeLocaleString)
@@ -76,15 +86,15 @@ public class InputMethodSubtypePreference extends SwitchWithNoTextPreference {
if (!mIsSystemLanguage && rhsPref.mIsSystemLanguage) {
return 1;
}
- final CharSequence t0 = getTitle();
- final CharSequence t1 = rhs.getTitle();
- if (t0 == null && t1 == null) {
- return Integer.compare(hashCode(), rhs.hashCode());
- }
- if (t0 != null && t1 != null) {
- return collator.compare(t0.toString(), t1.toString());
+ final CharSequence title = getTitle();
+ final CharSequence rhsTitle = rhs.getTitle();
+ final boolean emptyTitle = TextUtils.isEmpty(title);
+ final boolean rhsEmptyTitle = TextUtils.isEmpty(rhsTitle);
+ if (!emptyTitle && !rhsEmptyTitle) {
+ return collator.compare(title.toString(), rhsTitle.toString());
}
- return t0 == null ? -1 : 1;
+ // For historical reasons, an empty text needs to be put at the first.
+ return (emptyTitle ? -1 : 0) - (rhsEmptyTitle ? -1 : 0);
}
return super.compareTo(rhs);
}
diff --git a/packages/SettingsLib/src/com/android/settingslib/inputmethod/OWNERS b/packages/SettingsLib/src/com/android/settingslib/inputmethod/OWNERS
new file mode 100644
index 000000000000..a0e28baee3f6
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/inputmethod/OWNERS
@@ -0,0 +1,5 @@
+# Default reviewers for this and subdirectories.
+takaoka@google.com
+yukawa@google.com
+
+# Emergency approvers in case the above are not available \ No newline at end of file
diff --git a/packages/SettingsLib/src/com/android/settingslib/license/LicenseHtmlGeneratorFromXml.java b/packages/SettingsLib/src/com/android/settingslib/license/LicenseHtmlGeneratorFromXml.java
new file mode 100644
index 000000000000..e3e27ce17c9a
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/license/LicenseHtmlGeneratorFromXml.java
@@ -0,0 +1,292 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.license;
+
+import android.support.annotation.VisibleForTesting;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.Xml;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.zip.GZIPInputStream;
+
+/**
+ * The utility class that generate a license html file from xml files.
+ * All the HTML snippets and logic are copied from build/make/tools/generate-notice-files.py.
+ *
+ * TODO: Remove duplicate codes once backward support ends.
+ */
+class LicenseHtmlGeneratorFromXml {
+ private static final String TAG = "LicenseHtmlGeneratorFromXml";
+
+ private static final String TAG_ROOT = "licenses";
+ private static final String TAG_FILE_NAME = "file-name";
+ private static final String TAG_FILE_CONTENT = "file-content";
+ private static final String ATTR_CONTENT_ID = "contentId";
+
+ private static final String HTML_HEAD_STRING =
+ "<html><head>\n"
+ + "<style type=\"text/css\">\n"
+ + "body { padding: 0; font-family: sans-serif; }\n"
+ + ".same-license { background-color: #eeeeee;\n"
+ + " border-top: 20px solid white;\n"
+ + " padding: 10px; }\n"
+ + ".label { font-weight: bold; }\n"
+ + ".file-list { margin-left: 1em; color: blue; }\n"
+ + "</style>\n"
+ + "</head>"
+ + "<body topmargin=\"0\" leftmargin=\"0\" rightmargin=\"0\" bottommargin=\"0\">\n"
+ + "<div class=\"toc\">\n"
+ + "<ul>";
+
+ private static final String HTML_MIDDLE_STRING =
+ "</ul>\n"
+ + "</div><!-- table of contents -->\n"
+ + "<table cellpadding=\"0\" cellspacing=\"0\" border=\"0\">";
+
+ private static final String HTML_REAR_STRING =
+ "</table></body></html>";
+
+ private final List<File> mXmlFiles;
+
+ /*
+ * A map from a file name to a content id (MD5 sum of file content) for its license.
+ * For example, "/system/priv-app/TeleService/TeleService.apk" maps to
+ * "9645f39e9db895a4aa6e02cb57294595". Here "9645f39e9db895a4aa6e02cb57294595" is a MD5 sum
+ * of the content of packages/services/Telephony/MODULE_LICENSE_APACHE2.
+ */
+ private final Map<String, String> mFileNameToContentIdMap = new HashMap();
+
+ /*
+ * A map from a content id (MD5 sum of file content) to a license file content.
+ * For example, "9645f39e9db895a4aa6e02cb57294595" maps to the content string of
+ * packages/services/Telephony/MODULE_LICENSE_APACHE2. Here "9645f39e9db895a4aa6e02cb57294595"
+ * is a MD5 sum of the file content.
+ */
+ private final Map<String, String> mContentIdToFileContentMap = new HashMap();
+
+ static class ContentIdAndFileNames {
+ final String mContentId;
+ final List<String> mFileNameList = new ArrayList();
+
+ ContentIdAndFileNames(String contentId) {
+ mContentId = contentId;
+ }
+ }
+
+ private LicenseHtmlGeneratorFromXml(List<File> xmlFiles) {
+ mXmlFiles = xmlFiles;
+ }
+
+ public static boolean generateHtml(List<File> xmlFiles, File outputFile) {
+ LicenseHtmlGeneratorFromXml genertor = new LicenseHtmlGeneratorFromXml(xmlFiles);
+ return genertor.generateHtml(outputFile);
+ }
+
+ private boolean generateHtml(File outputFile) {
+ for (File xmlFile : mXmlFiles) {
+ parse(xmlFile);
+ }
+
+ if (mFileNameToContentIdMap.isEmpty() || mContentIdToFileContentMap.isEmpty()) {
+ return false;
+ }
+
+ PrintWriter writer = null;
+ try {
+ writer = new PrintWriter(outputFile);
+
+ generateHtml(mFileNameToContentIdMap, mContentIdToFileContentMap, writer);
+
+ writer.flush();
+ writer.close();
+ return true;
+ } catch (FileNotFoundException | SecurityException e) {
+ Log.e(TAG, "Failed to generate " + outputFile, e);
+
+ if (writer != null) {
+ writer.close();
+ }
+ return false;
+ }
+ }
+
+ private void parse(File xmlFile) {
+ if (xmlFile == null || !xmlFile.exists() || xmlFile.length() == 0) {
+ return;
+ }
+
+ InputStreamReader in = null;
+ try {
+ if (xmlFile.getName().endsWith(".gz")) {
+ in = new InputStreamReader(new GZIPInputStream(new FileInputStream(xmlFile)));
+ } else {
+ in = new FileReader(xmlFile);
+ }
+
+ parse(in, mFileNameToContentIdMap, mContentIdToFileContentMap);
+
+ in.close();
+ } catch (XmlPullParserException | IOException e) {
+ Log.e(TAG, "Failed to parse " + xmlFile, e);
+ if (in != null) {
+ try {
+ in.close();
+ } catch (IOException ie) {
+ Log.w(TAG, "Failed to close " + xmlFile);
+ }
+ }
+ }
+ }
+
+ /*
+ * Parses an input stream and fills a map from a file name to a content id for its license
+ * and a map from a content id to a license file content.
+ *
+ * Following xml format is expected from the input stream.
+ *
+ * <licenses>
+ * <file-name contentId="content_id_of_license1">file1</file-name>
+ * <file-name contentId="content_id_of_license2">file2</file-name>
+ * ...
+ * <file-content contentId="content_id_of_license1">license1 file contents</file-content>
+ * <file-content contentId="content_id_of_license2">license2 file contents</file-content>
+ * ...
+ * </licenses>
+ */
+ @VisibleForTesting
+ static void parse(InputStreamReader in, Map<String, String> outFileNameToContentIdMap,
+ Map<String, String> outContentIdToFileContentMap)
+ throws XmlPullParserException, IOException {
+ Map<String, String> fileNameToContentIdMap = new HashMap<String, String>();
+ Map<String, String> contentIdToFileContentMap = new HashMap<String, String>();
+
+ XmlPullParser parser = Xml.newPullParser();
+ parser.setInput(in);
+ parser.nextTag();
+
+ parser.require(XmlPullParser.START_TAG, "", TAG_ROOT);
+
+ int state = parser.getEventType();
+ while (state != XmlPullParser.END_DOCUMENT) {
+ if (state == XmlPullParser.START_TAG) {
+ if (TAG_FILE_NAME.equals(parser.getName())) {
+ String contentId = parser.getAttributeValue("", ATTR_CONTENT_ID);
+ if (!TextUtils.isEmpty(contentId)) {
+ String fileName = readText(parser).trim();
+ if (!TextUtils.isEmpty(fileName)) {
+ fileNameToContentIdMap.put(fileName, contentId);
+ }
+ }
+ } else if (TAG_FILE_CONTENT.equals(parser.getName())) {
+ String contentId = parser.getAttributeValue("", ATTR_CONTENT_ID);
+ if (!TextUtils.isEmpty(contentId)
+ && !outContentIdToFileContentMap.containsKey(contentId)
+ && !contentIdToFileContentMap.containsKey(contentId)) {
+ String fileContent = readText(parser);
+ if (!TextUtils.isEmpty(fileContent)) {
+ contentIdToFileContentMap.put(contentId, fileContent);
+ }
+ }
+ }
+ }
+
+ state = parser.next();
+ }
+ outFileNameToContentIdMap.putAll(fileNameToContentIdMap);
+ outContentIdToFileContentMap.putAll(contentIdToFileContentMap);
+ }
+
+ private static String readText(XmlPullParser parser)
+ throws IOException, XmlPullParserException {
+ StringBuffer result = new StringBuffer();
+ int state = parser.next();
+ while (state == XmlPullParser.TEXT) {
+ result.append(parser.getText());
+ state = parser.next();
+ }
+ return result.toString();
+ }
+
+ @VisibleForTesting
+ static void generateHtml(Map<String, String> fileNameToContentIdMap,
+ Map<String, String> contentIdToFileContentMap, PrintWriter writer) {
+ List<String> fileNameList = new ArrayList();
+ fileNameList.addAll(fileNameToContentIdMap.keySet());
+ Collections.sort(fileNameList);
+
+ writer.println(HTML_HEAD_STRING);
+
+ int count = 0;
+ Map<String, Integer> contentIdToOrderMap = new HashMap();
+ List<ContentIdAndFileNames> contentIdAndFileNamesList = new ArrayList();
+
+ // Prints all the file list with a link to its license file content.
+ for (String fileName : fileNameList) {
+ String contentId = fileNameToContentIdMap.get(fileName);
+ // Assigns an id to a newly referred license file content.
+ if (!contentIdToOrderMap.containsKey(contentId)) {
+ contentIdToOrderMap.put(contentId, count);
+
+ // An index in contentIdAndFileNamesList is the order of each element.
+ contentIdAndFileNamesList.add(new ContentIdAndFileNames(contentId));
+ count++;
+ }
+
+ int id = contentIdToOrderMap.get(contentId);
+ contentIdAndFileNamesList.get(id).mFileNameList.add(fileName);
+ writer.format("<li><a href=\"#id%d\">%s</a></li>\n", id, fileName);
+ }
+
+ writer.println(HTML_MIDDLE_STRING);
+
+ count = 0;
+ // Prints all contents of the license files in order of id.
+ for (ContentIdAndFileNames contentIdAndFileNames : contentIdAndFileNamesList) {
+ writer.format("<tr id=\"id%d\"><td class=\"same-license\">\n", count);
+ writer.println("<div class=\"label\">Notices for file(s):</div>");
+ writer.println("<div class=\"file-list\">");
+ for (String fileName : contentIdAndFileNames.mFileNameList) {
+ writer.format("%s <br/>\n", fileName);
+ }
+ writer.println("</div><!-- file-list -->");
+ writer.println("<pre class=\"license-text\">");
+ writer.println(contentIdToFileContentMap.get(
+ contentIdAndFileNames.mContentId));
+ writer.println("</pre><!-- license-text -->");
+ writer.println("</td></tr><!-- same-license -->");
+
+ count++;
+ }
+
+ writer.println(HTML_REAR_STRING);
+ }
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/license/LicenseHtmlLoader.java b/packages/SettingsLib/src/com/android/settingslib/license/LicenseHtmlLoader.java
new file mode 100644
index 000000000000..a9fb20ca9e17
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/license/LicenseHtmlLoader.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.license;
+
+import android.content.Context;
+import android.support.annotation.VisibleForTesting;
+import android.util.Log;
+
+import com.android.settingslib.utils.AsyncLoader;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * LicenseHtmlLoader is a loader which loads a license html file from default license xml files.
+ */
+public class LicenseHtmlLoader extends AsyncLoader<File> {
+ private static final String TAG = "LicenseHtmlLoader";
+
+ private static final String[] DEFAULT_LICENSE_XML_PATHS = {
+ "/system/etc/NOTICE.xml.gz",
+ "/vendor/etc/NOTICE.xml.gz",
+ "/odm/etc/NOTICE.xml.gz",
+ "/oem/etc/NOTICE.xml.gz"};
+ private static final String NOTICE_HTML_FILE_NAME = "NOTICE.html";
+
+ private Context mContext;
+
+ public LicenseHtmlLoader(Context context) {
+ super(context);
+ mContext = context;
+ }
+
+ @Override
+ public File loadInBackground() {
+ return generateHtmlFromDefaultXmlFiles();
+ }
+
+ @Override
+ protected void onDiscardResult(File f) {
+ }
+
+ private File generateHtmlFromDefaultXmlFiles() {
+ final List<File> xmlFiles = getVaildXmlFiles();
+ if (xmlFiles.isEmpty()) {
+ Log.e(TAG, "No notice file exists.");
+ return null;
+ }
+
+ File cachedHtmlFile = getCachedHtmlFile();
+ if (!isCachedHtmlFileOutdated(xmlFiles, cachedHtmlFile)
+ || generateHtmlFile(xmlFiles, cachedHtmlFile)) {
+ return cachedHtmlFile;
+ }
+
+ return null;
+ }
+
+ @VisibleForTesting
+ List<File> getVaildXmlFiles() {
+ final List<File> xmlFiles = new ArrayList();
+ for (final String xmlPath : DEFAULT_LICENSE_XML_PATHS) {
+ File file = new File(xmlPath);
+ if (file.exists() && file.length() != 0) {
+ xmlFiles.add(file);
+ }
+ }
+ return xmlFiles;
+ }
+
+ @VisibleForTesting
+ File getCachedHtmlFile() {
+ return new File(mContext.getCacheDir(), NOTICE_HTML_FILE_NAME);
+ }
+
+ @VisibleForTesting
+ boolean isCachedHtmlFileOutdated(List<File> xmlFiles, File cachedHtmlFile) {
+ boolean outdated = true;
+ if (cachedHtmlFile.exists() && cachedHtmlFile.length() != 0) {
+ outdated = false;
+ for (File file : xmlFiles) {
+ if (cachedHtmlFile.lastModified() < file.lastModified()) {
+ outdated = true;
+ break;
+ }
+ }
+ }
+ return outdated;
+ }
+
+ @VisibleForTesting
+ boolean generateHtmlFile(List<File> xmlFiles, File htmlFile) {
+ return LicenseHtmlGeneratorFromXml.generateHtml(xmlFiles, htmlFile);
+ }
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/location/RecentLocationApps.java b/packages/SettingsLib/src/com/android/settingslib/location/RecentLocationApps.java
index 3f826cc00b8b..6025d68a6d0e 100644
--- a/packages/SettingsLib/src/com/android/settingslib/location/RecentLocationApps.java
+++ b/packages/SettingsLib/src/com/android/settingslib/location/RecentLocationApps.java
@@ -16,21 +16,21 @@
package com.android.settingslib.location;
-import android.app.AppGlobals;
import android.app.AppOpsManager;
import android.content.Context;
import android.content.pm.ApplicationInfo;
-import android.content.pm.IPackageManager;
import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
import android.graphics.drawable.Drawable;
import android.os.Process;
-import android.os.RemoteException;
import android.os.UserHandle;
import android.os.UserManager;
+import android.support.annotation.VisibleForTesting;
import android.util.IconDrawableFactory;
import android.util.Log;
-
import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
import java.util.List;
/**
@@ -38,11 +38,13 @@ import java.util.List;
*/
public class RecentLocationApps {
private static final String TAG = RecentLocationApps.class.getSimpleName();
- private static final String ANDROID_SYSTEM_PACKAGE_NAME = "android";
+ @VisibleForTesting
+ static final String ANDROID_SYSTEM_PACKAGE_NAME = "android";
private static final int RECENT_TIME_INTERVAL_MILLIS = 15 * 60 * 1000;
- private static final int[] LOCATION_OPS = new int[] {
+ @VisibleForTesting
+ static final int[] LOCATION_OPS = new int[] {
AppOpsManager.OP_MONITOR_LOCATION,
AppOpsManager.OP_MONITOR_HIGH_POWER_LOCATION,
};
@@ -59,6 +61,7 @@ public class RecentLocationApps {
/**
* Fills a list of applications which queried location recently within specified time.
+ * Apps are sorted by recency. Apps with more recent location requests are in the front.
*/
public List<Request> getAppList() {
// Retrieve a location usage list from AppOps
@@ -91,7 +94,18 @@ public class RecentLocationApps {
requests.add(request);
}
}
+ return requests;
+ }
+ public List<Request> getAppListSorted() {
+ List<Request> requests = getAppList();
+ // Sort the list of Requests by recency. Most recent request first.
+ Collections.sort(requests, Collections.reverseOrder(new Comparator<Request>() {
+ @Override
+ public int compare(Request request1, Request request2) {
+ return Long.compare(request1.requestFinishTime, request2.requestFinishTime);
+ }
+ }));
return requests;
}
@@ -108,10 +122,12 @@ public class RecentLocationApps {
List<AppOpsManager.OpEntry> entries = ops.getOps();
boolean highBattery = false;
boolean normalBattery = false;
+ long locationRequestFinishTime = 0L;
// Earliest time for a location request to end and still be shown in list.
long recentLocationCutoffTime = now - RECENT_TIME_INTERVAL_MILLIS;
for (AppOpsManager.OpEntry entry : entries) {
if (entry.isRunning() || entry.getTime() >= recentLocationCutoffTime) {
+ locationRequestFinishTime = entry.getTime() + entry.getDuration();
switch (entry.getOp()) {
case AppOpsManager.OP_MONITOR_LOCATION:
normalBattery = true;
@@ -133,15 +149,13 @@ public class RecentLocationApps {
}
// The package is fresh enough, continue.
-
int uid = ops.getUid();
int userId = UserHandle.getUserId(uid);
Request request = null;
try {
- IPackageManager ipm = AppGlobals.getPackageManager();
- ApplicationInfo appInfo =
- ipm.getApplicationInfo(packageName, PackageManager.GET_META_DATA, userId);
+ ApplicationInfo appInfo = mPackageManager.getApplicationInfoAsUser(
+ packageName, PackageManager.GET_META_DATA, userId);
if (appInfo == null) {
Log.w(TAG, "Null application info retrieved for package " + packageName
+ ", userId " + userId);
@@ -158,12 +172,10 @@ public class RecentLocationApps {
badgedAppLabel = null;
}
request = new Request(packageName, userHandle, icon, appLabel, highBattery,
- badgedAppLabel);
- } catch (RemoteException e) {
- Log.w(TAG, "Error while retrieving application info for package " + packageName
- + ", userId " + userId, e);
+ badgedAppLabel, locationRequestFinishTime);
+ } catch (NameNotFoundException e) {
+ Log.w(TAG, "package name not found for " + packageName + ", userId " + userId);
}
-
return request;
}
@@ -174,15 +186,18 @@ public class RecentLocationApps {
public final CharSequence label;
public final boolean isHighBattery;
public final CharSequence contentDescription;
+ public final long requestFinishTime;
private Request(String packageName, UserHandle userHandle, Drawable icon,
- CharSequence label, boolean isHighBattery, CharSequence contentDescription) {
+ CharSequence label, boolean isHighBattery, CharSequence contentDescription,
+ long requestFinishTime) {
this.packageName = packageName;
this.userHandle = userHandle;
this.icon = icon;
this.label = label;
this.isHighBattery = isHighBattery;
this.contentDescription = contentDescription;
+ this.requestFinishTime = requestFinishTime;
}
}
}
diff --git a/packages/SettingsLib/src/com/android/settingslib/net/DataUsageController.java b/packages/SettingsLib/src/com/android/settingslib/net/DataUsageController.java
index ed3696cafa45..f7aa29796ce8 100644
--- a/packages/SettingsLib/src/com/android/settingslib/net/DataUsageController.java
+++ b/packages/SettingsLib/src/com/android/settingslib/net/DataUsageController.java
@@ -158,6 +158,9 @@ public class DataUsageController {
usage.startDate = start;
usage.usageLevel = totalBytes;
usage.period = formatDateRange(start, end);
+ usage.cycleStart = start;
+ usage.cycleEnd = end;
+
if (policy != null) {
usage.limitLevel = policy.limitBytes > 0 ? policy.limitBytes : 0;
usage.warningLevel = policy.warningBytes > 0 ? policy.warningBytes : 0;
@@ -245,6 +248,8 @@ public class DataUsageController {
public long limitLevel;
public long warningLevel;
public long usageLevel;
+ public long cycleStart;
+ public long cycleEnd;
}
public interface Callback {
diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/EnableZenModeDialog.java b/packages/SettingsLib/src/com/android/settingslib/notification/EnableZenModeDialog.java
new file mode 100644
index 000000000000..1a54d6a3396b
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/notification/EnableZenModeDialog.java
@@ -0,0 +1,505 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.notification;
+
+import android.app.ActivityManager;
+import android.app.AlarmManager;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.NotificationManager;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.net.Uri;
+import android.provider.Settings;
+import android.service.notification.Condition;
+import android.service.notification.ZenModeConfig;
+import android.text.TextUtils;
+import android.text.format.DateFormat;
+import android.util.Log;
+import android.util.Slog;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.CompoundButton;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.RadioButton;
+import android.widget.RadioGroup;
+import android.widget.ScrollView;
+import android.widget.TextView;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.logging.MetricsLogger;
+import com.android.internal.logging.nano.MetricsProto;
+import com.android.internal.policy.PhoneWindow;
+import com.android.settingslib.R;
+
+import java.util.Arrays;
+import java.util.Calendar;
+import java.util.GregorianCalendar;
+import java.util.Locale;
+import java.util.Objects;
+
+public class EnableZenModeDialog {
+ private static final String TAG = "EnableZenModeDialog";
+ private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+
+ private static final int[] MINUTE_BUCKETS = ZenModeConfig.MINUTE_BUCKETS;
+ private static final int MIN_BUCKET_MINUTES = MINUTE_BUCKETS[0];
+ private static final int MAX_BUCKET_MINUTES = MINUTE_BUCKETS[MINUTE_BUCKETS.length - 1];
+ private static final int DEFAULT_BUCKET_INDEX = Arrays.binarySearch(MINUTE_BUCKETS, 60);
+
+ @VisibleForTesting
+ protected static final int FOREVER_CONDITION_INDEX = 0;
+ @VisibleForTesting
+ protected static final int COUNTDOWN_CONDITION_INDEX = 1;
+ @VisibleForTesting
+ protected static final int COUNTDOWN_ALARM_CONDITION_INDEX = 2;
+
+ private static final int SECONDS_MS = 1000;
+ private static final int MINUTES_MS = 60 * SECONDS_MS;
+
+ @VisibleForTesting
+ protected Uri mForeverId;
+ private int mBucketIndex = -1;
+
+ private AlarmManager mAlarmManager;
+ private int mUserId;
+ private boolean mAttached;
+
+ @VisibleForTesting
+ protected Context mContext;
+ @VisibleForTesting
+ protected TextView mZenAlarmWarning;
+ @VisibleForTesting
+ protected LinearLayout mZenRadioGroupContent;
+
+ private RadioGroup mZenRadioGroup;
+ private int MAX_MANUAL_DND_OPTIONS = 3;
+
+ @VisibleForTesting
+ protected LayoutInflater mLayoutInflater;
+
+ public EnableZenModeDialog(Context context) {
+ mContext = context;
+ }
+
+ public Dialog createDialog() {
+ NotificationManager noMan = (NotificationManager) mContext.
+ getSystemService(Context.NOTIFICATION_SERVICE);
+ mForeverId = Condition.newId(mContext).appendPath("forever").build();
+ mAlarmManager = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);
+ mUserId = mContext.getUserId();
+ mAttached = false;
+
+ final AlertDialog.Builder builder = new AlertDialog.Builder(mContext)
+ .setTitle(R.string.zen_mode_settings_turn_on_dialog_title)
+ .setNegativeButton(R.string.cancel, null)
+ .setPositiveButton(R.string.zen_mode_enable_dialog_turn_on,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ int checkedId = mZenRadioGroup.getCheckedRadioButtonId();
+ ConditionTag tag = getConditionTagAt(checkedId);
+
+ if (isForever(tag.condition)) {
+ MetricsLogger.action(mContext,
+ MetricsProto.MetricsEvent.
+ NOTIFICATION_ZEN_MODE_TOGGLE_ON_FOREVER);
+ } else if (isAlarm(tag.condition)) {
+ MetricsLogger.action(mContext,
+ MetricsProto.MetricsEvent.
+ NOTIFICATION_ZEN_MODE_TOGGLE_ON_ALARM);
+ } else if (isCountdown(tag.condition)) {
+ MetricsLogger.action(mContext,
+ MetricsProto.MetricsEvent.
+ NOTIFICATION_ZEN_MODE_TOGGLE_ON_COUNTDOWN);
+ } else {
+ Slog.d(TAG, "Invalid manual condition: " + tag.condition);
+ }
+ // always triggers priority-only dnd with chosen condition
+ noMan.setZenMode(Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS,
+ getRealConditionId(tag.condition), TAG);
+ }
+ });
+
+ View contentView = getContentView();
+ bindConditions(forever());
+ builder.setView(contentView);
+ return builder.create();
+ }
+
+ private void hideAllConditions() {
+ final int N = mZenRadioGroupContent.getChildCount();
+ for (int i = 0; i < N; i++) {
+ mZenRadioGroupContent.getChildAt(i).setVisibility(View.GONE);
+ }
+
+ mZenAlarmWarning.setVisibility(View.GONE);
+ }
+
+ protected View getContentView() {
+ if (mLayoutInflater == null) {
+ mLayoutInflater = new PhoneWindow(mContext).getLayoutInflater();
+ }
+ View contentView = mLayoutInflater.inflate(R.layout.zen_mode_turn_on_dialog_container,
+ null);
+ ScrollView container = (ScrollView) contentView.findViewById(R.id.container);
+
+ mZenRadioGroup = container.findViewById(R.id.zen_radio_buttons);
+ mZenRadioGroupContent = container.findViewById(R.id.zen_radio_buttons_content);
+ mZenAlarmWarning = container.findViewById(R.id.zen_alarm_warning);
+
+ for (int i = 0; i < MAX_MANUAL_DND_OPTIONS; i++) {
+ final View radioButton = mLayoutInflater.inflate(R.layout.zen_mode_radio_button,
+ mZenRadioGroup, false);
+ mZenRadioGroup.addView(radioButton);
+ radioButton.setId(i);
+
+ final View radioButtonContent = mLayoutInflater.inflate(R.layout.zen_mode_condition,
+ mZenRadioGroupContent, false);
+ radioButtonContent.setId(i + MAX_MANUAL_DND_OPTIONS);
+ mZenRadioGroupContent.addView(radioButtonContent);
+ }
+
+ hideAllConditions();
+ return contentView;
+ }
+
+ @VisibleForTesting
+ protected void bind(final Condition condition, final View row, final int rowId) {
+ if (condition == null) throw new IllegalArgumentException("condition must not be null");
+ final boolean enabled = condition.state == Condition.STATE_TRUE;
+ final ConditionTag tag = row.getTag() != null ? (ConditionTag) row.getTag() :
+ new ConditionTag();
+ row.setTag(tag);
+ final boolean first = tag.rb == null;
+ if (tag.rb == null) {
+ tag.rb = (RadioButton) mZenRadioGroup.getChildAt(rowId);
+ }
+ tag.condition = condition;
+ final Uri conditionId = getConditionId(tag.condition);
+ if (DEBUG) Log.d(TAG, "bind i=" + mZenRadioGroupContent.indexOfChild(row) + " first="
+ + first + " condition=" + conditionId);
+ tag.rb.setEnabled(enabled);
+ tag.rb.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
+ @Override
+ public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
+ if (isChecked) {
+ tag.rb.setChecked(true);
+ if (DEBUG) Log.d(TAG, "onCheckedChanged " + conditionId);
+ MetricsLogger.action(mContext,
+ MetricsProto.MetricsEvent.QS_DND_CONDITION_SELECT);
+ updateAlarmWarningText(tag.condition);
+ announceConditionSelection(tag);
+ }
+ }
+ });
+
+ updateUi(tag, row, condition, enabled, rowId, conditionId);
+ row.setVisibility(View.VISIBLE);
+ }
+
+ @VisibleForTesting
+ protected ConditionTag getConditionTagAt(int index) {
+ return (ConditionTag) mZenRadioGroupContent.getChildAt(index).getTag();
+ }
+
+ @VisibleForTesting
+ protected void bindConditions(Condition c) {
+ // forever
+ bind(forever(), mZenRadioGroupContent.getChildAt(FOREVER_CONDITION_INDEX),
+ FOREVER_CONDITION_INDEX);
+ if (c == null) {
+ bindGenericCountdown();
+ bindNextAlarm(getTimeUntilNextAlarmCondition());
+ } else if (isForever(c)) {
+ getConditionTagAt(FOREVER_CONDITION_INDEX).rb.setChecked(true);
+ bindGenericCountdown();
+ bindNextAlarm(getTimeUntilNextAlarmCondition());
+ } else {
+ if (isAlarm(c)) {
+ bindGenericCountdown();
+ bindNextAlarm(c);
+ getConditionTagAt(COUNTDOWN_ALARM_CONDITION_INDEX).rb.setChecked(true);
+ } else if (isCountdown(c)) {
+ bindNextAlarm(getTimeUntilNextAlarmCondition());
+ bind(c, mZenRadioGroupContent.getChildAt(COUNTDOWN_CONDITION_INDEX),
+ COUNTDOWN_CONDITION_INDEX);
+ getConditionTagAt(COUNTDOWN_CONDITION_INDEX).rb.setChecked(true);
+ } else {
+ Slog.d(TAG, "Invalid manual condition: " + c);
+ }
+ }
+ }
+
+ public static Uri getConditionId(Condition condition) {
+ return condition != null ? condition.id : null;
+ }
+
+ public Condition forever() {
+ Uri foreverId = Condition.newId(mContext).appendPath("forever").build();
+ return new Condition(foreverId, foreverSummary(mContext), "", "", 0 /*icon*/,
+ Condition.STATE_TRUE, 0 /*flags*/);
+ }
+
+ public long getNextAlarm() {
+ final AlarmManager.AlarmClockInfo info = mAlarmManager.getNextAlarmClock(mUserId);
+ return info != null ? info.getTriggerTime() : 0;
+ }
+
+ @VisibleForTesting
+ protected boolean isAlarm(Condition c) {
+ return c != null && ZenModeConfig.isValidCountdownToAlarmConditionId(c.id);
+ }
+
+ @VisibleForTesting
+ protected boolean isCountdown(Condition c) {
+ return c != null && ZenModeConfig.isValidCountdownConditionId(c.id);
+ }
+
+ private boolean isForever(Condition c) {
+ return c != null && mForeverId.equals(c.id);
+ }
+
+ private Uri getRealConditionId(Condition condition) {
+ return isForever(condition) ? null : getConditionId(condition);
+ }
+
+ private String foreverSummary(Context context) {
+ return context.getString(com.android.internal.R.string.zen_mode_forever);
+ }
+
+ private static void setToMidnight(Calendar calendar) {
+ calendar.set(Calendar.HOUR_OF_DAY, 0);
+ calendar.set(Calendar.MINUTE, 0);
+ calendar.set(Calendar.SECOND, 0);
+ calendar.set(Calendar.MILLISECOND, 0);
+ }
+
+ // Returns a time condition if the next alarm is within the next week.
+ @VisibleForTesting
+ protected Condition getTimeUntilNextAlarmCondition() {
+ GregorianCalendar weekRange = new GregorianCalendar();
+ setToMidnight(weekRange);
+ weekRange.add(Calendar.DATE, 6);
+ final long nextAlarmMs = getNextAlarm();
+ if (nextAlarmMs > 0) {
+ GregorianCalendar nextAlarm = new GregorianCalendar();
+ nextAlarm.setTimeInMillis(nextAlarmMs);
+ setToMidnight(nextAlarm);
+
+ if (weekRange.compareTo(nextAlarm) >= 0) {
+ return ZenModeConfig.toNextAlarmCondition(mContext, nextAlarmMs,
+ ActivityManager.getCurrentUser());
+ }
+ }
+ return null;
+ }
+
+ @VisibleForTesting
+ protected void bindGenericCountdown() {
+ mBucketIndex = DEFAULT_BUCKET_INDEX;
+ Condition countdown = ZenModeConfig.toTimeCondition(mContext,
+ MINUTE_BUCKETS[mBucketIndex], ActivityManager.getCurrentUser());
+ if (!mAttached || getConditionTagAt(COUNTDOWN_CONDITION_INDEX).condition == null) {
+ bind(countdown, mZenRadioGroupContent.getChildAt(COUNTDOWN_CONDITION_INDEX),
+ COUNTDOWN_CONDITION_INDEX);
+ }
+ }
+
+ private void updateUi(ConditionTag tag, View row, Condition condition,
+ boolean enabled, int rowId, Uri conditionId) {
+ if (tag.lines == null) {
+ tag.lines = row.findViewById(android.R.id.content);
+ }
+ if (tag.line1 == null) {
+ tag.line1 = (TextView) row.findViewById(android.R.id.text1);
+ }
+
+ if (tag.line2 == null) {
+ tag.line2 = (TextView) row.findViewById(android.R.id.text2);
+ }
+
+ final String line1 = !TextUtils.isEmpty(condition.line1) ? condition.line1
+ : condition.summary;
+ final String line2 = condition.line2;
+ tag.line1.setText(line1);
+ if (TextUtils.isEmpty(line2)) {
+ tag.line2.setVisibility(View.GONE);
+ } else {
+ tag.line2.setVisibility(View.VISIBLE);
+ tag.line2.setText(line2);
+ }
+ tag.lines.setEnabled(enabled);
+ tag.lines.setAlpha(enabled ? 1 : .4f);
+
+ tag.lines.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ tag.rb.setChecked(true);
+ }
+ });
+
+ // minus button
+ final ImageView button1 = (ImageView) row.findViewById(android.R.id.button1);
+ button1.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ onClickTimeButton(row, tag, false /*down*/, rowId);
+ }
+ });
+
+ // plus button
+ final ImageView button2 = (ImageView) row.findViewById(android.R.id.button2);
+ button2.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ onClickTimeButton(row, tag, true /*up*/, rowId);
+ }
+ });
+
+ final long time = ZenModeConfig.tryParseCountdownConditionId(conditionId);
+ if (rowId == COUNTDOWN_CONDITION_INDEX && time > 0) {
+ button1.setVisibility(View.VISIBLE);
+ button2.setVisibility(View.VISIBLE);
+ if (mBucketIndex > -1) {
+ button1.setEnabled(mBucketIndex > 0);
+ button2.setEnabled(mBucketIndex < MINUTE_BUCKETS.length - 1);
+ } else {
+ final long span = time - System.currentTimeMillis();
+ button1.setEnabled(span > MIN_BUCKET_MINUTES * MINUTES_MS);
+ final Condition maxCondition = ZenModeConfig.toTimeCondition(mContext,
+ MAX_BUCKET_MINUTES, ActivityManager.getCurrentUser());
+ button2.setEnabled(!Objects.equals(condition.summary, maxCondition.summary));
+ }
+
+ button1.setAlpha(button1.isEnabled() ? 1f : .5f);
+ button2.setAlpha(button2.isEnabled() ? 1f : .5f);
+ } else {
+ button1.setVisibility(View.GONE);
+ button2.setVisibility(View.GONE);
+ }
+ }
+
+ @VisibleForTesting
+ protected void bindNextAlarm(Condition c) {
+ View alarmContent = mZenRadioGroupContent.getChildAt(COUNTDOWN_ALARM_CONDITION_INDEX);
+ ConditionTag tag = (ConditionTag) alarmContent.getTag();
+
+ if (c != null && (!mAttached || tag == null || tag.condition == null)) {
+ bind(c, alarmContent, COUNTDOWN_ALARM_CONDITION_INDEX);
+ }
+
+ // hide the alarm radio button if there isn't a "next alarm condition"
+ tag = (ConditionTag) alarmContent.getTag();
+ boolean showAlarm = tag != null && tag.condition != null;
+ mZenRadioGroup.getChildAt(COUNTDOWN_ALARM_CONDITION_INDEX).setVisibility(
+ showAlarm ? View.VISIBLE : View.GONE);
+ alarmContent.setVisibility(showAlarm ? View.VISIBLE : View.GONE);
+ }
+
+ private void onClickTimeButton(View row, ConditionTag tag, boolean up, int rowId) {
+ MetricsLogger.action(mContext, MetricsProto.MetricsEvent.QS_DND_TIME, up);
+ Condition newCondition = null;
+ final int N = MINUTE_BUCKETS.length;
+ if (mBucketIndex == -1) {
+ // not on a known index, search for the next or prev bucket by time
+ final Uri conditionId = getConditionId(tag.condition);
+ final long time = ZenModeConfig.tryParseCountdownConditionId(conditionId);
+ final long now = System.currentTimeMillis();
+ for (int i = 0; i < N; i++) {
+ int j = up ? i : N - 1 - i;
+ final int bucketMinutes = MINUTE_BUCKETS[j];
+ final long bucketTime = now + bucketMinutes * MINUTES_MS;
+ if (up && bucketTime > time || !up && bucketTime < time) {
+ mBucketIndex = j;
+ newCondition = ZenModeConfig.toTimeCondition(mContext,
+ bucketTime, bucketMinutes, ActivityManager.getCurrentUser(),
+ false /*shortVersion*/);
+ break;
+ }
+ }
+ if (newCondition == null) {
+ mBucketIndex = DEFAULT_BUCKET_INDEX;
+ newCondition = ZenModeConfig.toTimeCondition(mContext,
+ MINUTE_BUCKETS[mBucketIndex], ActivityManager.getCurrentUser());
+ }
+ } else {
+ // on a known index, simply increment or decrement
+ mBucketIndex = Math.max(0, Math.min(N - 1, mBucketIndex + (up ? 1 : -1)));
+ newCondition = ZenModeConfig.toTimeCondition(mContext,
+ MINUTE_BUCKETS[mBucketIndex], ActivityManager.getCurrentUser());
+ }
+ bind(newCondition, row, rowId);
+ updateAlarmWarningText(tag.condition);
+ tag.rb.setChecked(true);
+ announceConditionSelection(tag);
+ }
+
+ private void announceConditionSelection(ConditionTag tag) {
+ // condition will always be priority-only
+ String modeText = mContext.getString(R.string.zen_interruption_level_priority);
+ if (tag.line1 != null) {
+ mZenRadioGroupContent.announceForAccessibility(mContext.getString(
+ R.string.zen_mode_and_condition, modeText, tag.line1.getText()));
+ }
+ }
+
+ private void updateAlarmWarningText(Condition condition) {
+ String warningText = computeAlarmWarningText(condition);
+ mZenAlarmWarning.setText(warningText);
+ mZenAlarmWarning.setVisibility(warningText == null ? View.GONE : View.VISIBLE);
+ }
+
+ private String computeAlarmWarningText(Condition condition) {
+ final long now = System.currentTimeMillis();
+ final long nextAlarm = getNextAlarm();
+ if (nextAlarm < now) {
+ return null;
+ }
+ int warningRes = 0;
+ if (condition == null || isForever(condition)) {
+ warningRes = R.string.zen_alarm_warning_indef;
+ } else {
+ final long time = ZenModeConfig.tryParseCountdownConditionId(condition.id);
+ if (time > now && nextAlarm < time) {
+ warningRes = R.string.zen_alarm_warning;
+ }
+ }
+ if (warningRes == 0) {
+ return null;
+ }
+ final boolean soon = (nextAlarm - now) < 24 * 60 * 60 * 1000;
+ final boolean is24 = DateFormat.is24HourFormat(mContext, ActivityManager.getCurrentUser());
+ final String skeleton = soon ? (is24 ? "Hm" : "hma") : (is24 ? "EEEHm" : "EEEhma");
+ final String pattern = DateFormat.getBestDateTimePattern(Locale.getDefault(), skeleton);
+ final CharSequence formattedTime = DateFormat.format(pattern, nextAlarm);
+ final int templateRes = soon ? R.string.alarm_template : R.string.alarm_template_far;
+ final String template = mContext.getResources().getString(templateRes, formattedTime);
+ return mContext.getResources().getString(warningRes, template);
+ }
+
+ // used as the view tag on condition rows
+ @VisibleForTesting
+ protected static class ConditionTag {
+ public RadioButton rb;
+ public View lines;
+ public TextView line1;
+ public TextView line2;
+ public Condition condition;
+ }
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/ZenRadioLayout.java b/packages/SettingsLib/src/com/android/settingslib/notification/ZenRadioLayout.java
new file mode 100644
index 000000000000..1140028b1ca4
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/notification/ZenRadioLayout.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package com.android.settingslib.notification;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.LinearLayout;
+
+/**
+ * Specialized layout for zen mode that allows the radio buttons to reside within
+ * a RadioGroup, but also makes sure that all the heights of the radio buttons align
+ * with the corresponding content in the second child of this view.
+ */
+public class ZenRadioLayout extends LinearLayout {
+
+ public ZenRadioLayout(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ /**
+ * Run 2 measurement passes, 1 that figures out the size of the content, and another
+ * that sets the size of the radio buttons to the heights of the corresponding content.
+ */
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+
+ ViewGroup radioGroup = (ViewGroup) getChildAt(0);
+ ViewGroup radioContent = (ViewGroup) getChildAt(1);
+ int size = radioGroup.getChildCount();
+ if (size != radioContent.getChildCount()) {
+ throw new IllegalStateException("Expected matching children");
+ }
+ boolean hasChanges = false;
+ View lastView = null;
+ for (int i = 0; i < size; i++) {
+ View radio = radioGroup.getChildAt(i);
+ View content = radioContent.getChildAt(i);
+ if (lastView != null) {
+ radio.setAccessibilityTraversalAfter(lastView.getId());
+ }
+ View contentClick = findFirstClickable(content);
+ if (contentClick != null) contentClick.setAccessibilityTraversalAfter(radio.getId());
+ lastView = findLastClickable(content);
+ if (radio.getLayoutParams().height != content.getMeasuredHeight()) {
+ hasChanges = true;
+ radio.getLayoutParams().height = content.getMeasuredHeight();
+ }
+ }
+ // Measure again if any heights changed.
+ if (hasChanges) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ }
+ }
+
+ private View findFirstClickable(View content) {
+ if (content.isClickable()) return content;
+ if (content instanceof ViewGroup) {
+ ViewGroup group = (ViewGroup) content;
+ for (int i = 0; i < group.getChildCount(); i++) {
+ View v = findFirstClickable(group.getChildAt(i));
+ if (v != null) return v;
+ }
+ }
+ return null;
+ }
+
+ private View findLastClickable(View content) {
+ if (content.isClickable()) return content;
+ if (content instanceof ViewGroup) {
+ ViewGroup group = (ViewGroup) content;
+ for (int i = group.getChildCount() - 1; i >= 0; i--) {
+ View v = findLastClickable(group.getChildAt(i));
+ if (v != null) return v;
+ }
+ }
+ return null;
+ }
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/suggestions/SuggestionController.java b/packages/SettingsLib/src/com/android/settingslib/suggestions/SuggestionController.java
new file mode 100644
index 000000000000..f740f7c01ce1
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/suggestions/SuggestionController.java
@@ -0,0 +1,174 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.suggestions;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.service.settings.suggestions.ISuggestionService;
+import android.service.settings.suggestions.Suggestion;
+import android.support.annotation.Nullable;
+import android.support.annotation.WorkerThread;
+import android.util.Log;
+
+import java.util.List;
+
+/**
+ * A controller class to access suggestion data.
+ */
+public class SuggestionController {
+
+ /**
+ * Callback interface when service is connected/disconnected.
+ */
+ public interface ServiceConnectionListener {
+ /**
+ * Called when service is connected.
+ */
+ void onServiceConnected();
+
+ /**
+ * Called when service is disconnected.
+ */
+ void onServiceDisconnected();
+ }
+
+ private static final String TAG = "SuggestionController";
+ private static final boolean DEBUG = false;
+
+ private final Context mContext;
+ private final Intent mServiceIntent;
+
+ private ServiceConnection mServiceConnection;
+ private ISuggestionService mRemoteService;
+ private ServiceConnectionListener mConnectionListener;
+
+ /**
+ * Create a new controller instance.
+ *
+ * @param context caller context
+ * @param service The component name for service.
+ * @param listener listener to receive service connected/disconnected event.
+ */
+ public SuggestionController(Context context, ComponentName service,
+ ServiceConnectionListener listener) {
+ mContext = context.getApplicationContext();
+ mConnectionListener = listener;
+ mServiceIntent = new Intent().setComponent(service);
+ mServiceConnection = createServiceConnection();
+ }
+
+ /**
+ * Start the controller.
+ */
+ public void start() {
+ mContext.bindServiceAsUser(mServiceIntent, mServiceConnection, Context.BIND_AUTO_CREATE,
+ android.os.Process.myUserHandle());
+ }
+
+ /**
+ * Stop the controller.
+ */
+ public void stop() {
+ if (mRemoteService != null) {
+ mRemoteService = null;
+ mContext.unbindService(mServiceConnection);
+ }
+ }
+
+ /**
+ * Get setting suggestions.
+ */
+ @Nullable
+ @WorkerThread
+ public List<Suggestion> getSuggestions() {
+ if (!isReady()) {
+ return null;
+ }
+ try {
+ return mRemoteService.getSuggestions();
+ } catch (NullPointerException e) {
+ Log.w(TAG, "mRemote service detached before able to query", e);
+ return null;
+ } catch (RemoteException e) {
+ Log.w(TAG, "Error when calling getSuggestion()", e);
+ return null;
+ }
+ }
+
+ public void dismissSuggestions(Suggestion suggestion) {
+ if (!isReady()) {
+ Log.w(TAG, "SuggestionController not ready, cannot dismiss " + suggestion.getId());
+ return;
+ }
+ try {
+ mRemoteService.dismissSuggestion(suggestion);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Error when calling dismissSuggestion()", e);
+ }
+ }
+
+ public void launchSuggestion(Suggestion suggestion) {
+ if (!isReady()) {
+ Log.w(TAG, "SuggestionController not ready, cannot launch " + suggestion.getId());
+ return;
+ }
+
+ try {
+ mRemoteService.launchSuggestion(suggestion);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Error when calling launchSuggestion()", e);
+ }
+ }
+
+ /**
+ * Whether or not the manager is ready
+ */
+ private boolean isReady() {
+ return mRemoteService != null;
+ }
+
+ /**
+ * Create a new {@link ServiceConnection} object to handle service connect/disconnect event.
+ */
+ private ServiceConnection createServiceConnection() {
+ return new ServiceConnection() {
+
+ @Override
+ public void onServiceConnected(ComponentName name, IBinder service) {
+ if (DEBUG) {
+ Log.d(TAG, "Service is connected");
+ }
+ mRemoteService = ISuggestionService.Stub.asInterface(service);
+ if (mConnectionListener != null) {
+ mConnectionListener.onServiceConnected();
+ }
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ if (mConnectionListener != null) {
+ mRemoteService = null;
+ mConnectionListener.onServiceDisconnected();
+ }
+ }
+ };
+ }
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/suggestions/SuggestionControllerMixin.java b/packages/SettingsLib/src/com/android/settingslib/suggestions/SuggestionControllerMixin.java
new file mode 100644
index 000000000000..46fc32fa43cc
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/suggestions/SuggestionControllerMixin.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.suggestions;
+
+import android.app.LoaderManager;
+import android.arch.lifecycle.OnLifecycleEvent;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Loader;
+import android.os.Bundle;
+import android.service.settings.suggestions.Suggestion;
+import android.support.annotation.Nullable;
+import android.util.Log;
+
+import com.android.settingslib.core.lifecycle.Lifecycle;
+
+import java.util.List;
+
+/**
+ * Manages IPC communication to SettingsIntelligence for suggestion related services.
+ */
+public class SuggestionControllerMixin implements SuggestionController.ServiceConnectionListener,
+ android.arch.lifecycle.LifecycleObserver, LoaderManager.LoaderCallbacks<List<Suggestion>> {
+
+ public interface SuggestionControllerHost {
+ /**
+ * Called when suggestion data fetching is ready.
+ */
+ void onSuggestionReady(List<Suggestion> data);
+
+ /**
+ * Returns {@link LoaderManager} associated with the host. If host is not attached to
+ * activity then return null.
+ */
+ @Nullable
+ LoaderManager getLoaderManager();
+ }
+
+ private static final String TAG = "SuggestionCtrlMixin";
+ private static final boolean DEBUG = false;
+
+ private final Context mContext;
+ private final SuggestionController mSuggestionController;
+ private final SuggestionControllerHost mHost;
+
+ private boolean mSuggestionLoaded;
+
+ public SuggestionControllerMixin(Context context, SuggestionControllerHost host,
+ Lifecycle lifecycle, ComponentName componentName) {
+ mContext = context.getApplicationContext();
+ mHost = host;
+ mSuggestionController = new SuggestionController(mContext, componentName,
+ this /* serviceConnectionListener */);
+ if (lifecycle != null) {
+ lifecycle.addObserver(this);
+ }
+ }
+
+ @OnLifecycleEvent(Lifecycle.Event.ON_START)
+ public void onStart() {
+ if (DEBUG) {
+ Log.d(TAG, "SuggestionController started");
+ }
+ mSuggestionController.start();
+ }
+
+ @OnLifecycleEvent(Lifecycle.Event.ON_STOP)
+ public void onStop() {
+ if (DEBUG) {
+ Log.d(TAG, "SuggestionController stopped.");
+ }
+ mSuggestionController.stop();
+ }
+
+ @Override
+ public void onServiceConnected() {
+ final LoaderManager loaderManager = mHost.getLoaderManager();
+ if (loaderManager != null) {
+ loaderManager.restartLoader(SuggestionLoader.LOADER_ID_SUGGESTIONS,
+ null /* args */, this /* callback */);
+ }
+ }
+
+ @Override
+ public void onServiceDisconnected() {
+ if (DEBUG) {
+ Log.d(TAG, "SuggestionService disconnected");
+ }
+ final LoaderManager loaderManager = mHost.getLoaderManager();
+ if (loaderManager != null) {
+ loaderManager.destroyLoader(SuggestionLoader.LOADER_ID_SUGGESTIONS);
+ }
+ }
+
+ @Override
+ public Loader<List<Suggestion>> onCreateLoader(int id, Bundle args) {
+ if (id == SuggestionLoader.LOADER_ID_SUGGESTIONS) {
+ mSuggestionLoaded = false;
+ return new SuggestionLoader(mContext, mSuggestionController);
+ }
+ throw new IllegalArgumentException("This loader id is not supported " + id);
+ }
+
+ @Override
+ public void onLoadFinished(Loader<List<Suggestion>> loader, List<Suggestion> data) {
+ mSuggestionLoaded = true;
+ mHost.onSuggestionReady(data);
+ }
+
+ @Override
+ public void onLoaderReset(Loader<List<Suggestion>> loader) {
+ mSuggestionLoaded = false;
+ }
+
+ public boolean isSuggestionLoaded() {
+ return mSuggestionLoaded;
+ }
+
+ public void dismissSuggestion(Suggestion suggestion) {
+ mSuggestionController.dismissSuggestions(suggestion);
+ }
+
+ public void launchSuggestion(Suggestion suggestion) {
+ mSuggestionController.launchSuggestion(suggestion);
+ }
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/suggestions/SuggestionList.java b/packages/SettingsLib/src/com/android/settingslib/suggestions/SuggestionList.java
index 2151ba096c62..a89092040e71 100644
--- a/packages/SettingsLib/src/com/android/settingslib/suggestions/SuggestionList.java
+++ b/packages/SettingsLib/src/com/android/settingslib/suggestions/SuggestionList.java
@@ -17,7 +17,6 @@
package com.android.settingslib.suggestions;
import android.content.Intent;
-import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.ArraySet;
@@ -68,15 +67,6 @@ public class SuggestionList {
return false;
}
- public List<Tile> getSuggestionForCategory(String category) {
- for (Map.Entry<SuggestionCategory, List<Tile>> entry : mSuggestions.entrySet()) {
- if (TextUtils.equals(entry.getKey().category, category)) {
- return entry.getValue();
- }
- }
- return null;
- }
-
/**
* Filter suggestions list so they are all unique.
*/
diff --git a/packages/SettingsLib/src/com/android/settingslib/suggestions/SuggestionLoader.java b/packages/SettingsLib/src/com/android/settingslib/suggestions/SuggestionLoader.java
new file mode 100644
index 000000000000..9c1af1edc778
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/suggestions/SuggestionLoader.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.suggestions;
+
+import android.content.Context;
+import android.service.settings.suggestions.Suggestion;
+import android.util.Log;
+
+import com.android.settingslib.utils.AsyncLoader;
+
+import java.util.List;
+
+public class SuggestionLoader extends AsyncLoader<List<Suggestion>> {
+
+ public static final int LOADER_ID_SUGGESTIONS = 42;
+ private static final String TAG = "SuggestionLoader";
+
+ private final SuggestionController mSuggestionController;
+
+ public SuggestionLoader(Context context, SuggestionController controller) {
+ super(context);
+ mSuggestionController = controller;
+ }
+
+ @Override
+ protected void onDiscardResult(List<Suggestion> result) {
+
+ }
+
+ @Override
+ public List<Suggestion> loadInBackground() {
+ final List<Suggestion> data = mSuggestionController.getSuggestions();
+ if (data == null) {
+ Log.d(TAG, "data is null");
+ } else {
+ Log.d(TAG, "data size " + data.size());
+ }
+ return data;
+ }
+} \ No newline at end of file
diff --git a/packages/SettingsLib/src/com/android/settingslib/suggestions/SuggestionParser.java b/packages/SettingsLib/src/com/android/settingslib/suggestions/SuggestionParser.java
index 56b84415e907..9c347631d817 100644
--- a/packages/SettingsLib/src/com/android/settingslib/suggestions/SuggestionParser.java
+++ b/packages/SettingsLib/src/com/android/settingslib/suggestions/SuggestionParser.java
@@ -261,7 +261,7 @@ public class SuggestionParser {
if (requiredAccountType == null) {
return true;
}
- AccountManager accountManager = AccountManager.get(mContext);
+ AccountManager accountManager = mContext.getSystemService(AccountManager.class);
Account[] accounts = accountManager.getAccountsByType(requiredAccountType);
boolean satisfiesRequiredAccount = accounts.length > 0;
if (!satisfiesRequiredAccount) {
diff --git a/packages/SettingsLib/src/com/android/settingslib/utils/AsyncLoader.java b/packages/SettingsLib/src/com/android/settingslib/utils/AsyncLoader.java
new file mode 100644
index 000000000000..06770ac09558
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/utils/AsyncLoader.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.utils;
+
+import android.content.AsyncTaskLoader;
+import android.content.Context;
+
+/**
+ * This class fills in some boilerplate for AsyncTaskLoader to actually load things.
+ *
+ * Subclasses need to implement {@link AsyncLoader#loadInBackground()} to perform the actual
+ * background task, and {@link AsyncLoader#onDiscardResult(T)} to clean up previously loaded
+ * results.
+ *
+ * This loader is based on the MailAsyncTaskLoader from the AOSP EmailUnified repo.
+ *
+ * @param <T> the data type to be loaded.
+ */
+public abstract class AsyncLoader<T> extends AsyncTaskLoader<T> {
+ private T mResult;
+
+ public AsyncLoader(final Context context) {
+ super(context);
+ }
+
+ @Override
+ protected void onStartLoading() {
+ if (mResult != null) {
+ deliverResult(mResult);
+ }
+
+ if (takeContentChanged() || mResult == null) {
+ forceLoad();
+ }
+ }
+
+ @Override
+ protected void onStopLoading() {
+ cancelLoad();
+ }
+
+ @Override
+ public void deliverResult(final T data) {
+ if (isReset()) {
+ if (data != null) {
+ onDiscardResult(data);
+ }
+ return;
+ }
+
+ final T oldResult = mResult;
+ mResult = data;
+
+ if (isStarted()) {
+ super.deliverResult(data);
+ }
+
+ if (oldResult != null && oldResult != mResult) {
+ onDiscardResult(oldResult);
+ }
+ }
+
+ @Override
+ protected void onReset() {
+ super.onReset();
+
+ onStopLoading();
+
+ if (mResult != null) {
+ onDiscardResult(mResult);
+ }
+ mResult = null;
+ }
+
+ @Override
+ public void onCanceled(final T data) {
+ super.onCanceled(data);
+
+ if (data != null) {
+ onDiscardResult(data);
+ }
+ }
+
+ /**
+ * Called when discarding the load results so subclasses can take care of clean-up or
+ * recycling tasks. This is not called if the same result (by way of pointer equality) is
+ * returned again by a subsequent call to loadInBackground, or if result is null.
+ *
+ * Note that this may be called concurrently with loadInBackground(), and in some circumstances
+ * may be called more than once for a given object.
+ *
+ * @param result The value returned from {@link AsyncLoader#loadInBackground()} which
+ * is to be discarded.
+ */
+ protected abstract void onDiscardResult(T result);
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/utils/PowerUtil.java b/packages/SettingsLib/src/com/android/settingslib/utils/PowerUtil.java
new file mode 100644
index 000000000000..346ca66bcb13
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/utils/PowerUtil.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.utils;
+
+import android.content.Context;
+import android.icu.text.MeasureFormat;
+import android.icu.text.MeasureFormat.FormatWidth;
+import android.icu.util.Measure;
+import android.icu.util.MeasureUnit;
+import android.support.annotation.Nullable;
+import android.text.TextUtils;
+import com.android.settingslib.R;
+import com.android.settingslib.utils.StringUtil;
+import java.util.Locale;
+import java.util.concurrent.TimeUnit;
+
+/** Utility class for keeping power related strings consistent**/
+public class PowerUtil {
+ private static final long SEVEN_MINUTES_MILLIS = TimeUnit.MINUTES.toMillis(7);
+ private static final long FIFTEEN_MINUTES_MILLIS = TimeUnit.MINUTES.toMillis(15);
+ private static final long ONE_DAY_MILLIS = TimeUnit.DAYS.toMillis(1);
+
+ /**
+ * This method produces the text used in various places throughout the system to describe the
+ * remaining battery life of the phone in a consistent manner.
+ *
+ * @param context
+ * @param drainTimeMs The estimated time remaining before the phone dies in milliseconds.
+ * @param percentageString An optional percentage of battery remaining string.
+ * @param basedOnUsage Whether this estimate is based on usage or simple extrapolation.
+ * @return a properly formatted and localized string describing how much time remains
+ * before the battery runs out.
+ */
+ public static String getBatteryRemainingStringFormatted(Context context, long drainTimeMs,
+ @Nullable String percentageString, boolean basedOnUsage) {
+ if (drainTimeMs > 0) {
+ if (drainTimeMs <= SEVEN_MINUTES_MILLIS) {
+ // show a imminent shutdown warning if less than 7 minutes remain
+ return getShutdownImminentString(context, percentageString);
+ } else if (drainTimeMs <= FIFTEEN_MINUTES_MILLIS) {
+ // show a less than 15 min remaining warning if appropriate
+ CharSequence timeString = StringUtil.formatElapsedTime(context,
+ FIFTEEN_MINUTES_MILLIS,
+ false /* withSeconds */);
+ return getUnderFifteenString(context, timeString, percentageString);
+ } else if (drainTimeMs >= ONE_DAY_MILLIS) {
+ // just say more than one day if over 24 hours
+ return getMoreThanOneDayString(context, percentageString);
+ } else {
+ // show a regular time remaining string
+ return getRegularTimeRemainingString(context, drainTimeMs,
+ percentageString, basedOnUsage);
+ }
+ }
+ return null;
+ }
+
+ private static String getShutdownImminentString(Context context, String percentageString) {
+ return TextUtils.isEmpty(percentageString)
+ ? context.getString(R.string.power_remaining_duration_only_shutdown_imminent)
+ : context.getString(
+ R.string.power_remaining_duration_shutdown_imminent,
+ percentageString);
+ }
+
+ private static String getUnderFifteenString(Context context, CharSequence timeString,
+ String percentageString) {
+ return TextUtils.isEmpty(percentageString)
+ ? context.getString(R.string.power_remaining_less_than_duration_only, timeString)
+ : context.getString(
+ R.string.power_remaining_less_than_duration,
+ percentageString,
+ timeString);
+
+ }
+
+ private static String getMoreThanOneDayString(Context context, String percentageString) {
+ final Locale currentLocale = context.getResources().getConfiguration().getLocales().get(0);
+ final MeasureFormat frmt = MeasureFormat.getInstance(currentLocale, FormatWidth.SHORT);
+
+ final Measure daysMeasure = new Measure(1, MeasureUnit.DAY);
+
+ return TextUtils.isEmpty(percentageString)
+ ? context.getString(R.string.power_remaining_only_more_than_subtext,
+ frmt.formatMeasures(daysMeasure))
+ : context.getString(
+ R.string.power_remaining_more_than_subtext,
+ percentageString,
+ frmt.formatMeasures(daysMeasure));
+ }
+
+ private static String getRegularTimeRemainingString(Context context, long drainTimeMs,
+ String percentageString, boolean basedOnUsage) {
+ // round to the nearest 15 min to not appear oversly precise
+ final long roundedTimeMs = roundToNearestThreshold(drainTimeMs,
+ FIFTEEN_MINUTES_MILLIS);
+ CharSequence timeString = StringUtil.formatElapsedTime(context,
+ roundedTimeMs,
+ false /* withSeconds */);
+ if (TextUtils.isEmpty(percentageString)) {
+ int id = basedOnUsage
+ ? R.string.power_remaining_duration_only_enhanced
+ : R.string.power_remaining_duration_only;
+ return context.getString(id, timeString);
+ } else {
+ int id = basedOnUsage
+ ? R.string.power_discharging_duration_enhanced
+ : R.string.power_discharging_duration;
+ return context.getString(id, percentageString, timeString);
+ }
+ }
+
+ public static long convertUsToMs(long timeUs) {
+ return timeUs / 1000;
+ }
+
+ public static long convertMsToUs(long timeMs) {
+ return timeMs * 1000;
+ }
+
+ private static long roundToNearestThreshold(long drainTime, long threshold) {
+ final long remainder = drainTime % threshold;
+ if (remainder < threshold / 2) {
+ return drainTime - remainder;
+ } else {
+ return drainTime - remainder + threshold;
+ }
+ }
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/utils/StringUtil.java b/packages/SettingsLib/src/com/android/settingslib/utils/StringUtil.java
new file mode 100644
index 000000000000..45fdd7860836
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/utils/StringUtil.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.utils;
+
+import android.content.Context;
+import android.icu.text.MeasureFormat;
+import android.icu.text.MeasureFormat.FormatWidth;
+import android.icu.text.RelativeDateTimeFormatter;
+import android.icu.text.RelativeDateTimeFormatter.RelativeUnit;
+import android.icu.util.Measure;
+import android.icu.util.MeasureUnit;
+import android.icu.util.ULocale;
+import android.text.SpannableStringBuilder;
+import android.text.Spanned;
+import android.text.style.TtsSpan;
+import java.util.ArrayList;
+import java.util.Locale;
+
+/** Utility class for generally useful string methods **/
+public class StringUtil {
+
+ public static final int SECONDS_PER_MINUTE = 60;
+ public static final int SECONDS_PER_HOUR = 60 * 60;
+ public static final int SECONDS_PER_DAY = 24 * 60 * 60;
+
+ /**
+ * Returns elapsed time for the given millis, in the following format:
+ * 2d 5h 40m 29s
+ * @param context the application context
+ * @param millis the elapsed time in milli seconds
+ * @param withSeconds include seconds?
+ * @return the formatted elapsed time
+ */
+ public static CharSequence formatElapsedTime(Context context, double millis,
+ boolean withSeconds) {
+ SpannableStringBuilder sb = new SpannableStringBuilder();
+ int seconds = (int) Math.floor(millis / 1000);
+ if (!withSeconds) {
+ // Round up.
+ seconds += 30;
+ }
+
+ int days = 0, hours = 0, minutes = 0;
+ if (seconds >= SECONDS_PER_DAY) {
+ days = seconds / SECONDS_PER_DAY;
+ seconds -= days * SECONDS_PER_DAY;
+ }
+ if (seconds >= SECONDS_PER_HOUR) {
+ hours = seconds / SECONDS_PER_HOUR;
+ seconds -= hours * SECONDS_PER_HOUR;
+ }
+ if (seconds >= SECONDS_PER_MINUTE) {
+ minutes = seconds / SECONDS_PER_MINUTE;
+ seconds -= minutes * SECONDS_PER_MINUTE;
+ }
+
+ final ArrayList<Measure> measureList = new ArrayList(4);
+ if (days > 0) {
+ measureList.add(new Measure(days, MeasureUnit.DAY));
+ }
+ if (hours > 0) {
+ measureList.add(new Measure(hours, MeasureUnit.HOUR));
+ }
+ if (minutes > 0) {
+ measureList.add(new Measure(minutes, MeasureUnit.MINUTE));
+ }
+ if (withSeconds && seconds > 0) {
+ measureList.add(new Measure(seconds, MeasureUnit.SECOND));
+ }
+ if (measureList.size() == 0) {
+ // Everything addable was zero, so nothing was added. We add a zero.
+ measureList.add(new Measure(0, withSeconds ? MeasureUnit.SECOND : MeasureUnit.MINUTE));
+ }
+ final Measure[] measureArray = measureList.toArray(new Measure[measureList.size()]);
+
+ final Locale locale = context.getResources().getConfiguration().locale;
+ final MeasureFormat measureFormat = MeasureFormat.getInstance(
+ locale, FormatWidth.NARROW);
+ sb.append(measureFormat.formatMeasures(measureArray));
+
+ if (measureArray.length == 1 && MeasureUnit.MINUTE.equals(measureArray[0].getUnit())) {
+ // Add ttsSpan if it only have minute value, because it will be read as "meters"
+ final TtsSpan ttsSpan = new TtsSpan.MeasureBuilder().setNumber(minutes)
+ .setUnit("minute").build();
+ sb.setSpan(ttsSpan, 0, sb.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+
+ return sb;
+ }
+
+ /**
+ * Returns relative time for the given millis in the past, in a short format such as "2 days
+ * ago", "5 hr. ago", "40 min. ago", or "29 sec. ago".
+ *
+ * <p>The unit is chosen to have good information value while only using one unit. So 27 hours
+ * and 50 minutes would be formatted as "28 hr. ago", while 50 hours would be formatted as
+ * "2 days ago".
+ *
+ * @param context the application context
+ * @param millis the elapsed time in milli seconds
+ * @param withSeconds include seconds?
+ * @return the formatted elapsed time
+ */
+ public static CharSequence formatRelativeTime(Context context, double millis,
+ boolean withSeconds) {
+ final int seconds = (int) Math.floor(millis / 1000);
+ final RelativeUnit unit;
+ final int value;
+ if (withSeconds && seconds < 2 * SECONDS_PER_MINUTE) {
+ unit = RelativeUnit.SECONDS;
+ value = seconds;
+ } else if (seconds < 2 * SECONDS_PER_HOUR) {
+ unit = RelativeUnit.MINUTES;
+ value = (seconds + SECONDS_PER_MINUTE / 2)
+ / SECONDS_PER_MINUTE;
+ } else if (seconds < 2 * SECONDS_PER_DAY) {
+ unit = RelativeUnit.HOURS;
+ value = (seconds + SECONDS_PER_HOUR / 2)
+ / SECONDS_PER_HOUR;
+ } else {
+ unit = RelativeUnit.DAYS;
+ value = (seconds + SECONDS_PER_DAY / 2)
+ / SECONDS_PER_DAY;
+ }
+
+ final Locale locale = context.getResources().getConfiguration().locale;
+ final RelativeDateTimeFormatter formatter = RelativeDateTimeFormatter.getInstance(
+ ULocale.forLocale(locale),
+ null /* default NumberFormat */,
+ RelativeDateTimeFormatter.Style.SHORT,
+ android.icu.text.DisplayContext.CAPITALIZATION_FOR_MIDDLE_OF_SENTENCE);
+
+ return formatter.format(value, RelativeDateTimeFormatter.Direction.LAST, unit);
+ }
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/utils/ThreadUtils.java b/packages/SettingsLib/src/com/android/settingslib/utils/ThreadUtils.java
index 2024991b3d58..88adcdb16edc 100644
--- a/packages/SettingsLib/src/com/android/settingslib/utils/ThreadUtils.java
+++ b/packages/SettingsLib/src/com/android/settingslib/utils/ThreadUtils.java
@@ -15,10 +15,17 @@
*/
package com.android.settingslib.utils;
+import android.os.Handler;
import android.os.Looper;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
public class ThreadUtils {
+
private static volatile Thread sMainThread;
+ private static volatile Handler sMainThreadHandler;
+ private static volatile ExecutorService sSingleThreadExecutor;
/**
* Returns true if the current thread is the UI thread.
@@ -31,6 +38,17 @@ public class ThreadUtils {
}
/**
+ * Returns a shared UI thread handler.
+ */
+ public static Handler getUiThreadHandler() {
+ if (sMainThreadHandler == null) {
+ sMainThreadHandler = new Handler(Looper.getMainLooper());
+ }
+
+ return sMainThreadHandler;
+ }
+
+ /**
* Checks that the current thread is the UI thread. Otherwise throws an exception.
*/
public static void ensureMainThread() {
@@ -39,4 +57,21 @@ public class ThreadUtils {
}
}
+ /**
+ * Posts runnable in background using shared background thread pool.
+ */
+ public static void postOnBackgroundThread(Runnable runnable) {
+ if (sSingleThreadExecutor == null) {
+ sSingleThreadExecutor = Executors.newSingleThreadExecutor();
+ }
+ sSingleThreadExecutor.execute(runnable);
+ }
+
+ /**
+ * Posts the runnable on the main thread.
+ */
+ public static void postOnMainThread(Runnable runnable) {
+ getUiThreadHandler().post(runnable);
+ }
+
}
diff --git a/packages/SettingsLib/src/com/android/settingslib/wifi/AccessPoint.java b/packages/SettingsLib/src/com/android/settingslib/wifi/AccessPoint.java
index 682b85e46165..f69944006a87 100644
--- a/packages/SettingsLib/src/com/android/settingslib/wifi/AccessPoint.java
+++ b/packages/SettingsLib/src/com/android/settingslib/wifi/AccessPoint.java
@@ -32,7 +32,6 @@ import android.net.NetworkKey;
import android.net.NetworkScoreManager;
import android.net.NetworkScorerAppData;
import android.net.ScoredNetwork;
-import android.net.WifiKey;
import android.net.wifi.IWifiManager;
import android.net.wifi.ScanResult;
import android.net.wifi.WifiConfiguration;
@@ -43,6 +42,7 @@ import android.net.wifi.WifiManager;
import android.net.wifi.WifiNetworkScoreCache;
import android.net.wifi.hotspot2.PasspointConfiguration;
import android.os.Bundle;
+import android.os.Parcelable;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.SystemClock;
@@ -52,6 +52,7 @@ import android.text.Spannable;
import android.text.SpannableString;
import android.text.TextUtils;
import android.text.style.TtsSpan;
+import android.util.ArraySet;
import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
@@ -60,10 +61,11 @@ import com.android.settingslib.R;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
+import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
-import java.util.concurrent.ConcurrentHashMap;
+import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
@@ -90,6 +92,9 @@ public class AccessPoint implements Comparable<AccessPoint> {
*/
public static final int HIGHER_FREQ_5GHZ = 5900;
+ /** The key which identifies this AccessPoint grouping. */
+ private String mKey;
+
@IntDef({Speed.NONE, Speed.SLOW, Speed.MODERATE, Speed.FAST, Speed.VERY_FAST})
@Retention(RetentionPolicy.SOURCE)
public @interface Speed {
@@ -115,14 +120,8 @@ public class AccessPoint implements Comparable<AccessPoint> {
int VERY_FAST = 30;
}
- /**
- * Experimental: we should be able to show the user the list of BSSIDs and bands
- * for that SSID.
- * For now this data is used only with Verbose Logging so as to show the band and number
- * of BSSIDs on which that network is seen.
- */
- private final ConcurrentHashMap<String, ScanResult> mScanResultCache =
- new ConcurrentHashMap<String, ScanResult>(32);
+ /** The underlying set of scan results comprising this AccessPoint. */
+ private final ArraySet<ScanResult> mScanResults = new ArraySet<>();
/**
* Map of BSSIDs to scored networks for individual bssids.
@@ -132,17 +131,13 @@ public class AccessPoint implements Comparable<AccessPoint> {
*/
private final Map<String, TimestampedScoredNetwork> mScoredNetworkCache = new HashMap<>();
- /** Maximum age of scan results to hold onto while actively scanning. **/
- private static final long MAX_SCAN_RESULT_AGE_MILLIS = 25000;
-
static final String KEY_NETWORKINFO = "key_networkinfo";
static final String KEY_WIFIINFO = "key_wifiinfo";
- static final String KEY_SCANRESULT = "key_scanresult";
static final String KEY_SSID = "key_ssid";
static final String KEY_SECURITY = "key_security";
static final String KEY_SPEED = "key_speed";
static final String KEY_PSKTYPE = "key_psktype";
- static final String KEY_SCANRESULTCACHE = "key_scanresultcache";
+ static final String KEY_SCANRESULTS = "key_scanresults";
static final String KEY_SCOREDNETWORKCACHE = "key_scorednetworkcache";
static final String KEY_CONFIG = "key_config";
static final String KEY_FQDN = "key_fqdn";
@@ -186,7 +181,6 @@ public class AccessPoint implements Comparable<AccessPoint> {
private WifiConfiguration mConfig;
private int mRssi = UNREACHABLE_RSSI;
- private long mSeen = 0;
private WifiInfo mInfo;
private NetworkInfo mNetworkInfo;
@@ -216,7 +210,10 @@ public class AccessPoint implements Comparable<AccessPoint> {
public AccessPoint(Context context, Bundle savedState) {
mContext = context;
- mConfig = savedState.getParcelable(KEY_CONFIG);
+
+ if (savedState.containsKey(KEY_CONFIG)) {
+ mConfig = savedState.getParcelable(KEY_CONFIG);
+ }
if (mConfig != null) {
loadConfig(mConfig);
}
@@ -236,12 +233,11 @@ public class AccessPoint implements Comparable<AccessPoint> {
if (savedState.containsKey(KEY_NETWORKINFO)) {
mNetworkInfo = savedState.getParcelable(KEY_NETWORKINFO);
}
- if (savedState.containsKey(KEY_SCANRESULTCACHE)) {
- ArrayList<ScanResult> scanResultArrayList =
- savedState.getParcelableArrayList(KEY_SCANRESULTCACHE);
- mScanResultCache.clear();
- for (ScanResult result : scanResultArrayList) {
- mScanResultCache.put(result.BSSID, result);
+ if (savedState.containsKey(KEY_SCANRESULTS)) {
+ Parcelable[] scanResults = savedState.getParcelableArray(KEY_SCANRESULTS);
+ mScanResults.clear();
+ for (Parcelable result : scanResults) {
+ mScanResults.add((ScanResult) result);
}
}
if (savedState.containsKey(KEY_SCOREDNETWORKCACHE)) {
@@ -268,9 +264,10 @@ public class AccessPoint implements Comparable<AccessPoint> {
}
update(mConfig, mInfo, mNetworkInfo);
- // Do not evict old scan results on initial creation
+ // Calculate required fields
+ updateKey();
updateRssi();
- updateSeen();
+
mId = sLastId.incrementAndGet();
}
@@ -296,31 +293,75 @@ public class AccessPoint implements Comparable<AccessPoint> {
copyFrom(other);
}
- AccessPoint(Context context, ScanResult result) {
+ AccessPoint(Context context, Collection<ScanResult> results) {
mContext = context;
- initWithScanResult(result);
+
+ if (results.isEmpty()) {
+ throw new IllegalArgumentException("Cannot construct with an empty ScanResult list");
+ }
+ mScanResults.addAll(results);
+
+ // Information derived from scan results
+ ScanResult firstResult = results.iterator().next();
+ ssid = firstResult.SSID;
+ bssid = firstResult.BSSID;
+ security = getSecurity(firstResult);
+ if (security == SECURITY_PSK) {
+ pskType = getPskType(firstResult);
+ }
+ updateKey();
+ updateRssi();
+
+ // Passpoint Info
+ mIsCarrierAp = firstResult.isCarrierAp;
+ mCarrierApEapType = firstResult.carrierApEapType;
+ mCarrierName = firstResult.carrierName;
+
mId = sLastId.incrementAndGet();
}
+ @VisibleForTesting void loadConfig(WifiConfiguration config) {
+ ssid = (config.SSID == null ? "" : removeDoubleQuotes(config.SSID));
+ bssid = config.BSSID;
+ security = getSecurity(config);
+ updateKey();
+ networkId = config.networkId;
+ mConfig = config;
+ }
+
+ /** Updates {@link #mKey} and should only called upon object creation/initialization. */
+ private void updateKey() {
+ // TODO(sghuman): Consolidate Key logic on ScanResultMatchInfo
+
+ StringBuilder builder = new StringBuilder();
+
+ if (TextUtils.isEmpty(getSsidStr())) {
+ builder.append(getBssid());
+ } else {
+ builder.append(getSsidStr());
+ }
+
+ builder.append(',').append(getSecurity());
+ mKey = builder.toString();
+ }
+
/**
* Copy accesspoint information. NOTE: We do not copy tag information because that is never
* set on the internal copy.
- * @param that
*/
void copyFrom(AccessPoint that) {
- that.evictOldScanResults();
this.ssid = that.ssid;
this.bssid = that.bssid;
this.security = that.security;
+ this.mKey = that.mKey;
this.networkId = that.networkId;
this.pskType = that.pskType;
this.mConfig = that.mConfig; //TODO: Watch out, this object is mutated.
this.mRssi = that.mRssi;
- this.mSeen = that.mSeen;
this.mInfo = that.mInfo;
this.mNetworkInfo = that.mNetworkInfo;
- this.mScanResultCache.clear();
- this.mScanResultCache.putAll(that.mScanResultCache);
+ this.mScanResults.clear();
+ this.mScanResults.addAll(that.mScanResults);
this.mScoredNetworkCache.clear();
this.mScoredNetworkCache.putAll(that.mScoredNetworkCache);
this.mId = that.mId;
@@ -428,7 +469,7 @@ public class AccessPoint implements Comparable<AccessPoint> {
if (WifiTracker.sVerboseLogging) {
builder.append(",rssi=").append(mRssi);
- builder.append(",scan cache size=").append(mScanResultCache.size());
+ builder.append(",scan cache size=").append(mScanResults.size());
}
return builder.append(')').toString();
@@ -470,7 +511,7 @@ public class AccessPoint implements Comparable<AccessPoint> {
*/
private boolean updateScores(WifiNetworkScoreCache scoreCache, long maxScoreCacheAgeMillis) {
long nowMillis = SystemClock.elapsedRealtime();
- for (ScanResult result : mScanResultCache.values()) {
+ for (ScanResult result : mScanResults) {
ScoredNetwork score = scoreCache.getScoredNetwork(result);
if (score == null) {
continue;
@@ -551,14 +592,13 @@ public class AccessPoint implements Comparable<AccessPoint> {
mIsScoredNetworkMetered = false;
if (isActive() && mInfo != null) {
- NetworkKey key = new NetworkKey(new WifiKey(
- AccessPoint.convertToQuotedString(ssid), mInfo.getBSSID()));
+ NetworkKey key = NetworkKey.createFromWifiInfo(mInfo);
ScoredNetwork score = scoreCache.getScoredNetwork(key);
if (score != null) {
mIsScoredNetworkMetered |= score.meteredHint;
}
} else {
- for (ScanResult result : mScanResultCache.values()) {
+ for (ScanResult result : mScanResults) {
ScoredNetwork score = scoreCache.getScoredNetwork(result);
if (score == null) {
continue;
@@ -569,19 +609,34 @@ public class AccessPoint implements Comparable<AccessPoint> {
return oldMetering == mIsScoredNetworkMetered;
}
- private void evictOldScanResults() {
- long nowMs = SystemClock.elapsedRealtime();
- for (Iterator<ScanResult> iter = mScanResultCache.values().iterator(); iter.hasNext(); ) {
- ScanResult result = iter.next();
- // result timestamp is in microseconds
- if (nowMs - result.timestamp / 1000 > MAX_SCAN_RESULT_AGE_MILLIS) {
- iter.remove();
- }
+ public static String getKey(ScanResult result) {
+ StringBuilder builder = new StringBuilder();
+
+ if (TextUtils.isEmpty(result.SSID)) {
+ builder.append(result.BSSID);
+ } else {
+ builder.append(result.SSID);
+ }
+
+ builder.append(',').append(getSecurity(result));
+ return builder.toString();
+ }
+
+ public static String getKey(WifiConfiguration config) {
+ StringBuilder builder = new StringBuilder();
+
+ if (TextUtils.isEmpty(config.SSID)) {
+ builder.append(config.BSSID);
+ } else {
+ builder.append(removeDoubleQuotes(config.SSID));
}
+
+ builder.append(',').append(getSecurity(config));
+ return builder.toString();
}
- public boolean matches(ScanResult result) {
- return ssid.equals(result.SSID) && security == getSecurity(result);
+ public String getKey() {
+ return mKey;
}
public boolean matches(WifiConfiguration config) {
@@ -626,6 +681,17 @@ public class AccessPoint implements Comparable<AccessPoint> {
}
/**
+ * Returns the underlying scan result set.
+ *
+ * <p>Callers should not modify this set.
+ */
+ public Set<ScanResult> getScanResults() { return mScanResults; }
+
+ public Map<String, TimestampedScoredNetwork> getScoredNetworkCache() {
+ return mScoredNetworkCache;
+ }
+
+ /**
* Updates {@link #mRssi}.
*
* <p>If the given connection is active, the existing value of {@link #mRssi} will be returned.
@@ -640,7 +706,7 @@ public class AccessPoint implements Comparable<AccessPoint> {
}
int rssi = UNREACHABLE_RSSI;
- for (ScanResult result : mScanResultCache.values()) {
+ for (ScanResult result : mScanResults) {
if (result.level > rssi) {
rssi = result.level;
}
@@ -653,21 +719,6 @@ public class AccessPoint implements Comparable<AccessPoint> {
}
}
- /** Updates {@link #mSeen} based on the scan result cache. */
- private void updateSeen() {
- long seen = 0;
- for (ScanResult result : mScanResultCache.values()) {
- if (result.timestamp > seen) {
- seen = result.timestamp;
- }
- }
-
- // Only replace the previous value if we have a recent scan result to use
- if (seen != 0) {
- mSeen = seen;
- }
- }
-
/**
* Returns if the network should be considered metered.
*/
@@ -863,41 +914,7 @@ public class AccessPoint implements Comparable<AccessPoint> {
}
if (WifiTracker.sVerboseLogging) {
- // Add RSSI/band information for this config, what was seen up to 6 seconds ago
- // verbose WiFi Logging is only turned on thru developers settings
- if (isActive() && mInfo != null) {
- summary.append(" f=" + Integer.toString(mInfo.getFrequency()));
- }
- summary.append(" " + getVisibilityStatus());
- if (config != null && !config.getNetworkSelectionStatus().isNetworkEnabled()) {
- summary.append(" (" + config.getNetworkSelectionStatus().getNetworkStatusString());
- if (config.getNetworkSelectionStatus().getDisableTime() > 0) {
- long now = System.currentTimeMillis();
- long diff = (now - config.getNetworkSelectionStatus().getDisableTime()) / 1000;
- long sec = diff%60; //seconds
- long min = (diff/60)%60; //minutes
- long hour = (min/60)%60; //hours
- summary.append(", ");
- if (hour > 0) summary.append(Long.toString(hour) + "h ");
- summary.append( Long.toString(min) + "m ");
- summary.append( Long.toString(sec) + "s ");
- }
- summary.append(")");
- }
-
- if (config != null) {
- WifiConfiguration.NetworkSelectionStatus networkStatus =
- config.getNetworkSelectionStatus();
- for (int index = WifiConfiguration.NetworkSelectionStatus.NETWORK_SELECTION_ENABLE;
- index < WifiConfiguration.NetworkSelectionStatus
- .NETWORK_SELECTION_DISABLED_MAX; index++) {
- if (networkStatus.getDisableReasonCounter(index) != 0) {
- summary.append(" " + WifiConfiguration.NetworkSelectionStatus
- .getNetworkDisableReasonString(index) + "="
- + networkStatus.getDisableReasonCounter(index));
- }
- }
- }
+ summary.append(WifiUtils.buildLoggingSummary(this, config));
}
// If Speed label and summary are both present, use the preference combination to combine
@@ -915,127 +932,6 @@ public class AccessPoint implements Comparable<AccessPoint> {
}
/**
- * Returns the visibility status of the WifiConfiguration.
- *
- * @return autojoin debugging information
- * TODO: use a string formatter
- * ["rssi 5Ghz", "num results on 5GHz" / "rssi 5Ghz", "num results on 5GHz"]
- * For instance [-40,5/-30,2]
- */
- private String getVisibilityStatus() {
- StringBuilder visibility = new StringBuilder();
- StringBuilder scans24GHz = new StringBuilder();
- StringBuilder scans5GHz = new StringBuilder();
- String bssid = null;
-
- long now = System.currentTimeMillis();
-
- if (isActive() && mInfo != null) {
- bssid = mInfo.getBSSID();
- if (bssid != null) {
- visibility.append(" ").append(bssid);
- }
- visibility.append(" rssi=").append(mInfo.getRssi());
- visibility.append(" ");
- visibility.append(" score=").append(mInfo.score);
- if (mSpeed != Speed.NONE) {
- visibility.append(" speed=").append(getSpeedLabel());
- }
- visibility.append(String.format(" tx=%.1f,", mInfo.txSuccessRate));
- visibility.append(String.format("%.1f,", mInfo.txRetriesRate));
- visibility.append(String.format("%.1f ", mInfo.txBadRate));
- visibility.append(String.format("rx=%.1f", mInfo.rxSuccessRate));
- }
-
- int maxRssi5 = WifiConfiguration.INVALID_RSSI;
- int maxRssi24 = WifiConfiguration.INVALID_RSSI;
- final int maxDisplayedScans = 4;
- int num5 = 0; // number of scanned BSSID on 5GHz band
- int num24 = 0; // number of scanned BSSID on 2.4Ghz band
- int numBlackListed = 0;
- evictOldScanResults();
-
- // TODO: sort list by RSSI or age
- long nowMs = SystemClock.elapsedRealtime();
- for (ScanResult result : mScanResultCache.values()) {
- if (result.frequency >= LOWER_FREQ_5GHZ
- && result.frequency <= HIGHER_FREQ_5GHZ) {
- // Strictly speaking: [4915, 5825]
- num5++;
-
- if (result.level > maxRssi5) {
- maxRssi5 = result.level;
- }
- if (num5 <= maxDisplayedScans) {
- scans5GHz.append(verboseScanResultSummary(result, bssid, nowMs));
- }
- } else if (result.frequency >= LOWER_FREQ_24GHZ
- && result.frequency <= HIGHER_FREQ_24GHZ) {
- // Strictly speaking: [2412, 2482]
- num24++;
-
- if (result.level > maxRssi24) {
- maxRssi24 = result.level;
- }
- if (num24 <= maxDisplayedScans) {
- scans24GHz.append(verboseScanResultSummary(result, bssid, nowMs));
- }
- }
- }
- visibility.append(" [");
- if (num24 > 0) {
- visibility.append("(").append(num24).append(")");
- if (num24 > maxDisplayedScans) {
- visibility.append("max=").append(maxRssi24).append(",");
- }
- visibility.append(scans24GHz.toString());
- }
- visibility.append(";");
- if (num5 > 0) {
- visibility.append("(").append(num5).append(")");
- if (num5 > maxDisplayedScans) {
- visibility.append("max=").append(maxRssi5).append(",");
- }
- visibility.append(scans5GHz.toString());
- }
- if (numBlackListed > 0)
- visibility.append("!").append(numBlackListed);
- visibility.append("]");
-
- return visibility.toString();
- }
-
- @VisibleForTesting
- /* package */ String verboseScanResultSummary(ScanResult result, String bssid, long nowMs) {
- StringBuilder stringBuilder = new StringBuilder();
- stringBuilder.append(" \n{").append(result.BSSID);
- if (result.BSSID.equals(bssid)) {
- stringBuilder.append("*");
- }
- stringBuilder.append("=").append(result.frequency);
- stringBuilder.append(",").append(result.level);
- int speed = getSpecificApSpeed(result);
- if (speed != Speed.NONE) {
- stringBuilder.append(",")
- .append(getSpeedLabel(speed));
- }
- int ageSeconds = (int) (nowMs - result.timestamp / 1000) / 1000;
- stringBuilder.append(",").append(ageSeconds).append("s");
- stringBuilder.append("}");
- return stringBuilder.toString();
- }
-
- @Speed private int getSpecificApSpeed(ScanResult result) {
- TimestampedScoredNetwork timedScore = mScoredNetworkCache.get(result.BSSID);
- if (timedScore == null) {
- return Speed.NONE;
- }
- // For debugging purposes we may want to use mRssi rather than result.level as the average
- // speed wil be determined by mRssi
- return timedScore.getScore().calculateBadge(result.level);
- }
-
- /**
* Return whether this is the active connection.
* For ephemeral connections (networkId is invalid), this returns false if the network is
* disconnected.
@@ -1114,29 +1010,6 @@ public class AccessPoint implements Comparable<AccessPoint> {
mConfig.allowedKeyManagement.set(KeyMgmt.NONE);
}
- void loadConfig(WifiConfiguration config) {
- ssid = (config.SSID == null ? "" : removeDoubleQuotes(config.SSID));
- bssid = config.BSSID;
- security = getSecurity(config);
- networkId = config.networkId;
- mConfig = config;
- }
-
- private void initWithScanResult(ScanResult result) {
- ssid = result.SSID;
- bssid = result.BSSID;
- security = getSecurity(result);
- if (security == SECURITY_PSK)
- pskType = getPskType(result);
-
- mScanResultCache.put(result.BSSID, result);
- updateRssi();
- mSeen = result.timestamp; // even if the timestamp is old it is still valid
- mIsCarrierAp = result.isCarrierAp;
- mCarrierApEapType = result.carrierApEapType;
- mCarrierName = result.carrierName;
- }
-
public void saveWifiState(Bundle savedState) {
if (ssid != null) savedState.putString(KEY_SSID, getSsidStr());
savedState.putInt(KEY_SECURITY, security);
@@ -1144,9 +1017,8 @@ public class AccessPoint implements Comparable<AccessPoint> {
savedState.putInt(KEY_PSKTYPE, pskType);
if (mConfig != null) savedState.putParcelable(KEY_CONFIG, mConfig);
savedState.putParcelable(KEY_WIFIINFO, mInfo);
- evictOldScanResults();
- savedState.putParcelableArrayList(KEY_SCANRESULTCACHE,
- new ArrayList<ScanResult>(mScanResultCache.values()));
+ savedState.putParcelableArray(KEY_SCANRESULTS,
+ mScanResults.toArray(new Parcelable[mScanResults.size()]));
savedState.putParcelableArrayList(KEY_SCOREDNETWORKCACHE,
new ArrayList<>(mScoredNetworkCache.values()));
if (mNetworkInfo != null) {
@@ -1168,50 +1040,58 @@ public class AccessPoint implements Comparable<AccessPoint> {
}
/**
- * Update the AP with the given scan result.
+ * Sets {@link #mScanResults} to the given collection.
*
- * @param result the ScanResult to add to the AccessPoint scan cache
- * @param evictOldScanResults whether stale scan results should be removed
- * from the cache during this update process
- * @return true if the scan result update caused a change in state which would impact ranking
- * or AccessPoint rendering (e.g. wifi level, security)
+ * @param scanResults a collection of scan results to add to the internal set
+ * @throws IllegalArgumentException if any of the given ScanResults did not belong to this AP
*/
- boolean update(ScanResult result, boolean evictOldScanResults) {
- if (matches(result)) {
- int oldLevel = getLevel();
-
- /* Add or update the scan result for the BSSID */
- mScanResultCache.put(result.BSSID, result);
- if (evictOldScanResults) evictOldScanResults();
- updateSeen();
- updateRssi();
- int newLevel = getLevel();
-
- if (newLevel > 0 && newLevel != oldLevel) {
- // Only update labels on visible rssi changes
- updateSpeed();
- if (mAccessPointListener != null) {
- mAccessPointListener.onLevelChanged(this);
- }
+ void setScanResults(Collection<ScanResult> scanResults) {
+
+ // Validate scan results are for current AP only
+ String key = getKey();
+ for (ScanResult result : scanResults) {
+ String scanResultKey = AccessPoint.getKey(result);
+ if (!mKey.equals(scanResultKey)) {
+ throw new IllegalArgumentException(
+ String.format("ScanResult %s\nkey of %s did not match current AP key %s",
+ result, scanResultKey, key));
+ }
+ }
+
+
+ int oldLevel = getLevel();
+ mScanResults.clear();
+ mScanResults.addAll(scanResults);
+ updateRssi();
+ int newLevel = getLevel();
+
+ // If newLevel is 0, there will be no displayed Preference since the AP is unreachable
+ if (newLevel > 0 && newLevel != oldLevel) {
+ // Only update labels on visible rssi changes
+ updateSpeed();
+ if (mAccessPointListener != null) {
+ mAccessPointListener.onLevelChanged(this);
}
+ }
+
+ if (mAccessPointListener != null) {
+ mAccessPointListener.onAccessPointChanged(this);
+ }
+
+ if (!scanResults.isEmpty()) {
+ ScanResult result = scanResults.iterator().next();
+
// This flag only comes from scans, is not easily saved in config
if (security == SECURITY_PSK) {
pskType = getPskType(result);
}
- if (mAccessPointListener != null) {
- mAccessPointListener.onAccessPointChanged(this);
- }
-
// The carrier info in the ScanResult is set by the platform based on the SSID and will
// always be the same for all matching scan results.
mIsCarrierAp = result.isCarrierAp;
mCarrierApEapType = result.carrierApEapType;
mCarrierName = result.carrierName;
-
- return true;
}
- return false;
}
/** Attempt to update the AccessPoint and return true if an update occurred. */
@@ -1295,7 +1175,7 @@ public class AccessPoint implements Comparable<AccessPoint> {
}
@Nullable
- private String getSpeedLabel(@Speed int speed) {
+ String getSpeedLabel(@Speed int speed) {
switch (speed) {
case Speed.VERY_FAST:
return mContext.getString(R.string.speed_label_very_fast);
diff --git a/packages/SettingsLib/src/com/android/settingslib/wifi/AccessPointPreference.java b/packages/SettingsLib/src/com/android/settingslib/wifi/AccessPointPreference.java
index 9ed8de059996..8115ede2729e 100644
--- a/packages/SettingsLib/src/com/android/settingslib/wifi/AccessPointPreference.java
+++ b/packages/SettingsLib/src/com/android/settingslib/wifi/AccessPointPreference.java
@@ -15,13 +15,13 @@
*/
package com.android.settingslib.wifi;
+import android.annotation.Nullable;
import android.content.Context;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.StateListDrawable;
-import android.net.NetworkBadging;
import android.net.wifi.WifiConfiguration;
import android.os.Looper;
import android.os.UserHandle;
@@ -31,12 +31,15 @@ import android.support.v7.preference.PreferenceViewHolder;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.SparseArray;
+import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import com.android.settingslib.R;
import com.android.settingslib.TronUtils;
+import com.android.settingslib.TwoTargetPreference;
import com.android.settingslib.Utils;
+import com.android.settingslib.wifi.AccessPoint.Speed;
public class AccessPointPreference extends Preference {
@@ -60,9 +63,10 @@ public class AccessPointPreference extends Preference {
R.string.accessibility_wifi_signal_full
};
- private final StateListDrawable mFrictionSld;
+ @Nullable private final StateListDrawable mFrictionSld;
private final int mBadgePadding;
private final UserBadgeCache mBadgeCache;
+ private final IconInjector mIconInjector;
private TextView mTitleView;
private boolean mForSavedNetworks = false;
@@ -71,19 +75,18 @@ public class AccessPointPreference extends Preference {
private int mLevel;
private CharSequence mContentDescription;
private int mDefaultIconResId;
- private int mWifiSpeed = NetworkBadging.BADGING_NONE;
+ private int mWifiSpeed = Speed.NONE;
- public static String generatePreferenceKey(AccessPoint accessPoint) {
- StringBuilder builder = new StringBuilder();
-
- if (TextUtils.isEmpty(accessPoint.getSsidStr())) {
- builder.append(accessPoint.getBssid());
- } else {
- builder.append(accessPoint.getSsidStr());
+ @Nullable
+ private static StateListDrawable getFrictionStateListDrawable(Context context) {
+ TypedArray frictionSld;
+ try {
+ frictionSld = context.getTheme().obtainStyledAttributes(FRICTION_ATTRS);
+ } catch (Resources.NotFoundException e) {
+ // Fallback for platforms that do not need friction icon resources.
+ frictionSld = null;
}
-
- builder.append(',').append(accessPoint.getSecurity());
- return builder.toString();
+ return frictionSld != null ? (StateListDrawable) frictionSld.getDrawable(0) : null;
}
// Used for dummy pref.
@@ -92,54 +95,35 @@ public class AccessPointPreference extends Preference {
mFrictionSld = null;
mBadgePadding = 0;
mBadgeCache = null;
+ mIconInjector = new IconInjector(context);
}
public AccessPointPreference(AccessPoint accessPoint, Context context, UserBadgeCache cache,
boolean forSavedNetworks) {
- super(context);
- setWidgetLayoutResource(R.layout.access_point_friction_widget);
- mBadgeCache = cache;
- mAccessPoint = accessPoint;
- mForSavedNetworks = forSavedNetworks;
- mAccessPoint.setTag(this);
- mLevel = -1;
-
- TypedArray frictionSld;
- try {
- frictionSld = context.getTheme().obtainStyledAttributes(FRICTION_ATTRS);
- } catch (Resources.NotFoundException e) {
- // Fallback for platforms that do not need friction icon resources.
- frictionSld = null;
- }
- mFrictionSld = frictionSld != null ? (StateListDrawable) frictionSld.getDrawable(0) : null;
-
- // Distance from the end of the title at which this AP's user badge should sit.
- mBadgePadding = context.getResources()
- .getDimensionPixelSize(R.dimen.wifi_preference_badge_padding);
+ this(accessPoint, context, cache, 0 /* iconResId */, forSavedNetworks);
refresh();
}
public AccessPointPreference(AccessPoint accessPoint, Context context, UserBadgeCache cache,
int iconResId, boolean forSavedNetworks) {
+ this(accessPoint, context, cache, iconResId, forSavedNetworks,
+ getFrictionStateListDrawable(context), -1 /* level */, new IconInjector(context));
+ }
+
+ @VisibleForTesting
+ AccessPointPreference(AccessPoint accessPoint, Context context, UserBadgeCache cache,
+ int iconResId, boolean forSavedNetworks, StateListDrawable frictionSld,
+ int level, IconInjector iconInjector) {
super(context);
setWidgetLayoutResource(R.layout.access_point_friction_widget);
mBadgeCache = cache;
mAccessPoint = accessPoint;
mForSavedNetworks = forSavedNetworks;
mAccessPoint.setTag(this);
- mLevel = -1;
+ mLevel = level;
mDefaultIconResId = iconResId;
-
- TypedArray frictionSld;
- try {
- frictionSld = context.getTheme().obtainStyledAttributes(FRICTION_ATTRS);
- } catch (Resources.NotFoundException e) {
- // Fallback for platforms that do not need friction icon resources.
- frictionSld = null;
- }
- mFrictionSld = frictionSld != null ? (StateListDrawable) frictionSld.getDrawable(0) : null;
-
- // Distance from the end of the title at which this AP's user badge should sit.
+ mFrictionSld = frictionSld;
+ mIconInjector = iconInjector;
mBadgePadding = context.getResources()
.getDimensionPixelSize(R.dimen.wifi_preference_badge_padding);
}
@@ -179,9 +163,7 @@ public class AccessPointPreference extends Preference {
}
TronUtils.logWifiSettingsSpeed(context, mWifiSpeed);
- // TODO(b/62355275): Revert this to N code after deleting NetworkBadging API
- Drawable drawable = NetworkBadging.getWifiIcon(
- level, NetworkBadging.BADGING_NONE, getContext().getTheme());
+ Drawable drawable = mIconInjector.getIcon(level);
if (!mForSavedNetworks && drawable != null) {
drawable.setTint(Utils.getColorAttr(context, android.R.attr.colorControlNormal));
setIcon(drawable);
@@ -325,4 +307,16 @@ public class AccessPointPreference extends Preference {
return mBadges.valueAt(index);
}
}
+
+ static class IconInjector {
+ private final Context mContext;
+
+ public IconInjector(Context context) {
+ mContext = context;
+ }
+
+ public Drawable getIcon(int level) {
+ return mContext.getDrawable(Utils.getWifiIconResource(level));
+ }
+ }
}
diff --git a/packages/SettingsLib/src/com/android/settingslib/wifi/OWNERS b/packages/SettingsLib/src/com/android/settingslib/wifi/OWNERS
new file mode 100644
index 000000000000..d5d2e9e8c146
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/wifi/OWNERS
@@ -0,0 +1,7 @@
+# Default reviewers for this and subdirectories.
+asapperstein@google.com
+asargent@google.com
+dling@google.com
+zhfan@google.com
+
+# Emergency approvers in case the above are not available \ No newline at end of file
diff --git a/packages/SettingsLib/src/com/android/settingslib/wifi/TestAccessPointBuilder.java b/packages/SettingsLib/src/com/android/settingslib/wifi/TestAccessPointBuilder.java
index 3dec1d382026..2993a0de0658 100644
--- a/packages/SettingsLib/src/com/android/settingslib/wifi/TestAccessPointBuilder.java
+++ b/packages/SettingsLib/src/com/android/settingslib/wifi/TestAccessPointBuilder.java
@@ -23,6 +23,7 @@ import android.net.wifi.ScanResult;
import android.net.wifi.WifiConfiguration;
import android.net.wifi.WifiInfo;
import android.os.Bundle;
+import android.os.Parcelable;
import android.support.annotation.Keep;
import com.android.settingslib.wifi.AccessPoint.Speed;
@@ -58,7 +59,7 @@ public class TestAccessPointBuilder {
private String mCarrierName = null;
Context mContext;
- private ArrayList<ScanResult> mScanResultCache;
+ private ArrayList<ScanResult> mScanResults;
private ArrayList<TimestampedScoredNetwork> mScoredNetworkCache;
@Keep
@@ -84,8 +85,9 @@ public class TestAccessPointBuilder {
if (mProviderFriendlyName != null) {
bundle.putString(AccessPoint.KEY_PROVIDER_FRIENDLY_NAME, mProviderFriendlyName);
}
- if (mScanResultCache != null) {
- bundle.putParcelableArrayList(AccessPoint.KEY_SCANRESULTCACHE, mScanResultCache);
+ if (mScanResults != null) {
+ bundle.putParcelableArray(AccessPoint.KEY_SCANRESULTS,
+ mScanResults.toArray(new Parcelable[mScanResults.size()]));
}
if (mScoredNetworkCache != null) {
bundle.putParcelableArrayList(AccessPoint.KEY_SCOREDNETWORKCACHE, mScoredNetworkCache);
@@ -229,8 +231,8 @@ public class TestAccessPointBuilder {
return this;
}
- public TestAccessPointBuilder setScanResultCache(ArrayList<ScanResult> scanResultCache) {
- mScanResultCache = scanResultCache;
+ public TestAccessPointBuilder setScanResults(ArrayList<ScanResult> scanResults) {
+ mScanResults = scanResults;
return this;
}
diff --git a/packages/SettingsLib/src/com/android/settingslib/wifi/WifiStatusTracker.java b/packages/SettingsLib/src/com/android/settingslib/wifi/WifiStatusTracker.java
index 0d67ad03cb10..e8f5282007ae 100644
--- a/packages/SettingsLib/src/com/android/settingslib/wifi/WifiStatusTracker.java
+++ b/packages/SettingsLib/src/com/android/settingslib/wifi/WifiStatusTracker.java
@@ -51,10 +51,7 @@ public class WifiStatusTracker {
connected = networkInfo != null && networkInfo.isConnected();
// If Connected grab the signal strength and ssid.
if (connected) {
- // try getting it out of the intent first
- WifiInfo info = intent.getParcelableExtra(WifiManager.EXTRA_WIFI_INFO) != null
- ? (WifiInfo) intent.getParcelableExtra(WifiManager.EXTRA_WIFI_INFO)
- : mWifiManager.getConnectionInfo();
+ WifiInfo info = mWifiManager.getConnectionInfo();
if (info != null) {
ssid = getSsid(info);
} else {
diff --git a/packages/SettingsLib/src/com/android/settingslib/wifi/WifiTracker.java b/packages/SettingsLib/src/com/android/settingslib/wifi/WifiTracker.java
index 3c664b0b01a9..fac585e06306 100644
--- a/packages/SettingsLib/src/com/android/settingslib/wifi/WifiTracker.java
+++ b/packages/SettingsLib/src/com/android/settingslib/wifi/WifiTracker.java
@@ -24,7 +24,6 @@ import android.net.ConnectivityManager;
import android.net.Network;
import android.net.NetworkCapabilities;
import android.net.NetworkInfo;
-import android.net.NetworkInfo.DetailedState;
import android.net.NetworkKey;
import android.net.NetworkRequest;
import android.net.NetworkScoreManager;
@@ -36,19 +35,29 @@ import android.net.wifi.WifiManager;
import android.net.wifi.WifiNetworkScoreCache;
import android.net.wifi.WifiNetworkScoreCache.CacheListener;
import android.os.Handler;
+import android.os.HandlerThread;
import android.os.Looper;
import android.os.Message;
+import android.os.Process;
+import android.os.SystemClock;
import android.provider.Settings;
import android.support.annotation.GuardedBy;
+import android.support.annotation.NonNull;
+import android.support.annotation.VisibleForTesting;
import android.text.format.DateUtils;
+import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Log;
import android.util.SparseArray;
import android.util.SparseIntArray;
import android.widget.Toast;
-import com.android.internal.annotations.VisibleForTesting;
import com.android.settingslib.R;
+import com.android.settingslib.core.lifecycle.Lifecycle;
+import com.android.settingslib.core.lifecycle.LifecycleObserver;
+import com.android.settingslib.core.lifecycle.events.OnDestroy;
+import com.android.settingslib.core.lifecycle.events.OnStart;
+import com.android.settingslib.core.lifecycle.events.OnStop;
import java.io.PrintWriter;
import java.util.ArrayList;
@@ -64,13 +73,16 @@ import java.util.concurrent.atomic.AtomicBoolean;
/**
* Tracks saved or available wifi networks and their state.
*/
-public class WifiTracker {
+public class WifiTracker implements LifecycleObserver, OnStart, OnStop, OnDestroy {
/**
* Default maximum age in millis of cached scored networks in
* {@link AccessPoint#mScoredNetworkCache} to be used for speed label generation.
*/
private static final long DEFAULT_MAX_CACHED_SCORE_AGE_MILLIS = 20 * DateUtils.MINUTE_IN_MILLIS;
+ /** Maximum age of scan results to hold onto while actively scanning. **/
+ private static final long MAX_SCAN_RESULT_AGE_MILLIS = 25000;
+
private static final String TAG = "WifiTracker";
private static final boolean DBG() {
return Log.isLoggable(TAG, Log.DEBUG);
@@ -80,8 +92,6 @@ public class WifiTracker {
* and used so as to assist with in-the-field WiFi connectivity debugging */
public static boolean sVerboseLogging;
- // TODO(b/36733768): Remove flag includeSaved and includePasspoints.
-
// TODO: Allow control of this?
// Combo scans can take 5-6s to complete - set to 10s.
private static final int WIFI_RESCAN_INTERVAL_MS = 10 * 1000;
@@ -94,11 +104,9 @@ public class WifiTracker {
private final NetworkRequest mNetworkRequest;
private final AtomicBoolean mConnected = new AtomicBoolean(false);
private final WifiListener mListener;
- private final boolean mIncludeSaved;
- private final boolean mIncludeScans;
- private final boolean mIncludePasspoints;
- @VisibleForTesting final MainHandler mMainHandler;
- @VisibleForTesting final WorkHandler mWorkHandler;
+ @VisibleForTesting MainHandler mMainHandler;
+ @VisibleForTesting WorkHandler mWorkHandler;
+ private HandlerThread mWorkThread;
private WifiTrackerNetworkCallback mNetworkCallback;
@@ -135,14 +143,15 @@ public class WifiTracker {
= new AccessPointListenerAdapter();
private final HashMap<String, Integer> mSeenBssids = new HashMap<>();
+
+ // TODO(sghuman): Change this to be keyed on AccessPoint.getKey
private final HashMap<String, ScanResult> mScanResultCache = new HashMap<>();
- private Integer mScanId = 0;
private NetworkInfo mLastNetworkInfo;
private WifiInfo mLastInfo;
private final NetworkScoreManager mNetworkScoreManager;
- private final WifiNetworkScoreCache mScoreCache;
+ private WifiNetworkScoreCache mScoreCache;
private boolean mNetworkScoringUiEnabled;
private long mMaxSpeedLabelScoreCacheAge;
@@ -155,65 +164,60 @@ public class WifiTracker {
@GuardedBy("mLock")
private boolean mStaleScanResults = true;
- public WifiTracker(Context context, WifiListener wifiListener,
- boolean includeSaved, boolean includeScans) {
- this(context, wifiListener, null, includeSaved, includeScans);
+ private static IntentFilter newIntentFilter() {
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(WifiManager.WIFI_STATE_CHANGED_ACTION);
+ filter.addAction(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION);
+ filter.addAction(WifiManager.NETWORK_IDS_CHANGED_ACTION);
+ filter.addAction(WifiManager.SUPPLICANT_STATE_CHANGED_ACTION);
+ filter.addAction(WifiManager.CONFIGURED_NETWORKS_CHANGED_ACTION);
+ filter.addAction(WifiManager.LINK_CONFIGURATION_CHANGED_ACTION);
+ filter.addAction(WifiManager.NETWORK_STATE_CHANGED_ACTION);
+ filter.addAction(WifiManager.RSSI_CHANGED_ACTION);
+
+ return filter;
}
- public WifiTracker(Context context, WifiListener wifiListener, Looper workerLooper,
+ /**
+ * Use the lifecycle constructor below whenever possible
+ */
+ @Deprecated
+ public WifiTracker(Context context, WifiListener wifiListener,
boolean includeSaved, boolean includeScans) {
- this(context, wifiListener, workerLooper, includeSaved, includeScans, false);
+ this(context, wifiListener,
+ context.getSystemService(WifiManager.class),
+ context.getSystemService(ConnectivityManager.class),
+ context.getSystemService(NetworkScoreManager.class),
+ newIntentFilter());
}
+ // TODO(Sghuman): Clean up includeSaved and includeScans from all constructors and linked
+ // calling apps once IC window is complete
public WifiTracker(Context context, WifiListener wifiListener,
- boolean includeSaved, boolean includeScans, boolean includePasspoints) {
- this(context, wifiListener, null, includeSaved, includeScans, includePasspoints);
- }
-
- public WifiTracker(Context context, WifiListener wifiListener, Looper workerLooper,
- boolean includeSaved, boolean includeScans, boolean includePasspoints) {
- this(context, wifiListener, workerLooper, includeSaved, includeScans, includePasspoints,
+ @NonNull Lifecycle lifecycle, boolean includeSaved, boolean includeScans) {
+ this(context, wifiListener,
context.getSystemService(WifiManager.class),
context.getSystemService(ConnectivityManager.class),
- context.getSystemService(NetworkScoreManager.class), Looper.myLooper()
- );
+ context.getSystemService(NetworkScoreManager.class),
+ newIntentFilter());
+ lifecycle.addObserver(this);
}
@VisibleForTesting
- WifiTracker(Context context, WifiListener wifiListener, Looper workerLooper,
- boolean includeSaved, boolean includeScans, boolean includePasspoints,
+ WifiTracker(Context context, WifiListener wifiListener,
WifiManager wifiManager, ConnectivityManager connectivityManager,
- NetworkScoreManager networkScoreManager, Looper currentLooper) {
- if (!includeSaved && !includeScans) {
- throw new IllegalArgumentException("Must include either saved or scans");
- }
+ NetworkScoreManager networkScoreManager,
+ IntentFilter filter) {
mContext = context;
- if (currentLooper == null) {
- // When we aren't on a looper thread, default to the main.
- currentLooper = Looper.getMainLooper();
- }
- mMainHandler = new MainHandler(currentLooper);
- mWorkHandler = new WorkHandler(
- workerLooper != null ? workerLooper : currentLooper);
+ mMainHandler = new MainHandler(Looper.getMainLooper());
mWifiManager = wifiManager;
- mIncludeSaved = includeSaved;
- mIncludeScans = includeScans;
- mIncludePasspoints = includePasspoints;
- mListener = wifiListener;
+ mListener = new WifiListenerWrapper(wifiListener);
mConnectivityManager = connectivityManager;
// check if verbose logging has been turned on or off
sVerboseLogging = (mWifiManager.getVerboseLoggingLevel() > 0);
- mFilter = new IntentFilter();
- mFilter.addAction(WifiManager.WIFI_STATE_CHANGED_ACTION);
- mFilter.addAction(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION);
- mFilter.addAction(WifiManager.NETWORK_IDS_CHANGED_ACTION);
- mFilter.addAction(WifiManager.SUPPLICANT_STATE_CHANGED_ACTION);
- mFilter.addAction(WifiManager.CONFIGURED_NETWORKS_CHANGED_ACTION);
- mFilter.addAction(WifiManager.LINK_CONFIGURATION_CHANGED_ACTION);
- mFilter.addAction(WifiManager.NETWORK_STATE_CHANGED_ACTION);
- mFilter.addAction(WifiManager.RSSI_CHANGED_ACTION);
+ mFilter = filter;
mNetworkRequest = new NetworkRequest.Builder()
.clearCapabilities()
@@ -222,7 +226,22 @@ public class WifiTracker {
mNetworkScoreManager = networkScoreManager;
- mScoreCache = new WifiNetworkScoreCache(context, new CacheListener(mWorkHandler) {
+ final HandlerThread workThread = new HandlerThread(TAG
+ + "{" + Integer.toHexString(System.identityHashCode(this)) + "}",
+ Process.THREAD_PRIORITY_BACKGROUND);
+ workThread.start();
+ setWorkThread(workThread);
+ }
+
+ /**
+ * Sanity warning: this wipes out mScoreCache, so use with extreme caution
+ * @param workThread substitute Handler thread, for testing purposes only
+ */
+ @VisibleForTesting
+ void setWorkThread(HandlerThread workThread) {
+ mWorkThread = workThread;
+ mWorkHandler = new WorkHandler(workThread.getLooper());
+ mScoreCache = new WifiNetworkScoreCache(mContext, new CacheListener(mWorkHandler) {
@Override
public void networkCacheUpdated(List<ScoredNetwork> networks) {
synchronized (mLock) {
@@ -237,6 +256,11 @@ public class WifiTracker {
});
}
+ @Override
+ public void onDestroy() {
+ mWorkThread.quit();
+ }
+
/** Synchronously update the list of access points with the latest information. */
@MainThread
public void forceUpdate() {
@@ -265,15 +289,6 @@ public class WifiTracker {
}
/**
- * Force a scan for wifi networks to happen now.
- */
- public void forceScan() {
- if (mWifiManager.isWifiEnabled() && mScanner != null) {
- mScanner.forceScan();
- }
- }
-
- /**
* Temporarily stop scanning for wifi networks.
*/
public void pauseScanning() {
@@ -305,8 +320,9 @@ public class WifiTracker {
* <p>Registers listeners and starts scanning for wifi networks. If this is not called
* then forceUpdate() must be called to populate getAccessPoints().
*/
+ @Override
@MainThread
- public void startTracking() {
+ public void onStart() {
synchronized (mLock) {
registerScoreCache();
@@ -354,15 +370,16 @@ public class WifiTracker {
/**
* Stop tracking wifi networks and scores.
*
- * <p>This should always be called when done with a WifiTracker (if startTracking was called) to
+ * <p>This should always be called when done with a WifiTracker (if onStart was called) to
* ensure proper cleanup and prevent any further callbacks from occurring.
*
* <p>Calling this method will set the {@link #mStaleScanResults} bit, which prevents
* {@link WifiListener#onAccessPointsChanged()} callbacks from being invoked (until the bit
* is unset on the next SCAN_RESULTS_AVAILABLE_ACTION).
*/
+ @Override
@MainThread
- public void stopTracking() {
+ public void onStop() {
synchronized (mLock) {
if (mRegistered) {
mContext.unregisterReceiver(mReceiver);
@@ -432,38 +449,45 @@ public class WifiTracker {
private void handleResume() {
mScanResultCache.clear();
mSeenBssids.clear();
- mScanId = 0;
}
private Collection<ScanResult> updateScanResultCache(final List<ScanResult> newResults) {
- mScanId++;
+ // TODO(sghuman): Delete this and replace it with the Map of Ap Keys to ScanResults
for (ScanResult newResult : newResults) {
if (newResult.SSID == null || newResult.SSID.isEmpty()) {
continue;
}
mScanResultCache.put(newResult.BSSID, newResult);
- mSeenBssids.put(newResult.BSSID, mScanId);
}
- if (mScanId > NUM_SCANS_TO_CONFIRM_AP_LOSS) {
- if (DBG()) Log.d(TAG, "------ Dumping SSIDs that were expired on this scan ------");
- Integer threshold = mScanId - NUM_SCANS_TO_CONFIRM_AP_LOSS;
- for (Iterator<Map.Entry<String, Integer>> it = mSeenBssids.entrySet().iterator();
- it.hasNext(); /* nothing */) {
- Map.Entry<String, Integer> e = it.next();
- if (e.getValue() < threshold) {
- ScanResult result = mScanResultCache.get(e.getKey());
- if (DBG()) Log.d(TAG, "Removing " + e.getKey() + ":(" + result.SSID + ")");
- mScanResultCache.remove(e.getKey());
- it.remove();
- }
- }
- if (DBG()) Log.d(TAG, "---- Done Dumping SSIDs that were expired on this scan ----");
+ // Don't evict old results if no new scan results
+ if (!mStaleScanResults) {
+ evictOldScans();
}
+ // TODO(sghuman): Update a Map<ApKey, List<ScanResults>> variable to be reused later after
+ // double threads have been removed.
+
return mScanResultCache.values();
}
+ /**
+ * Remove old scan results from the cache.
+ *
+ * <p>Should only ever be invoked from {@link #updateScanResultCache(List)} when
+ * {@link #mStaleScanResults} is false.
+ */
+ private void evictOldScans() {
+ long nowMs = SystemClock.elapsedRealtime();
+ for (Iterator<ScanResult> iter = mScanResultCache.values().iterator(); iter.hasNext(); ) {
+ ScanResult result = iter.next();
+ // result timestamp is in microseconds
+ if (nowMs - result.timestamp / 1000 > MAX_SCAN_RESULT_AGE_MILLIS) {
+ iter.remove();
+ }
+ }
+ }
+
private WifiConfiguration getWifiConfigurationForNetworkId(
int networkId, final List<WifiConfiguration> configs) {
if (configs != null) {
@@ -499,7 +523,7 @@ public class WifiTracker {
/**
* Update the internal list of access points.
*
- * <p>Do not called directly (except for forceUpdate), use {@link #updateAccessPoints()} which
+ * <p>Do not call directly (except for forceUpdate), use {@link #updateAccessPoints()} which
* respects {@link #mStaleScanResults}.
*/
@GuardedBy("mLock")
@@ -508,7 +532,7 @@ public class WifiTracker {
WifiConfiguration connectionConfig = null;
if (mLastInfo != null) {
connectionConfig = getWifiConfigurationForNetworkId(
- mLastInfo.getNetworkId(), mWifiManager.getConfiguredNetworks());
+ mLastInfo.getNetworkId(), configs);
}
// Swap the current access points into a cached list.
@@ -520,46 +544,20 @@ public class WifiTracker {
accessPoint.clearConfig();
}
- /* Lookup table to more quickly update AccessPoints by only considering objects with the
- * correct SSID. Maps SSID -> List of AccessPoints with the given SSID. */
- Multimap<String, AccessPoint> apMap = new Multimap<String, AccessPoint>();
-
final Collection<ScanResult> results = updateScanResultCache(newScanResults);
+ final Map<String, WifiConfiguration> configsByKey = new ArrayMap(configs.size());
if (configs != null) {
for (WifiConfiguration config : configs) {
- if (config.selfAdded && config.numAssociation == 0) {
- continue;
- }
- AccessPoint accessPoint = getCachedOrCreate(config, cachedAccessPoints);
- if (mLastInfo != null && mLastNetworkInfo != null) {
- accessPoint.update(connectionConfig, mLastInfo, mLastNetworkInfo);
- }
- if (mIncludeSaved) {
- // If saved network not present in scan result then set its Rssi to
- // UNREACHABLE_RSSI
- boolean apFound = false;
- for (ScanResult result : results) {
- if (result.SSID.equals(accessPoint.getSsidStr())) {
- apFound = true;
- break;
- }
- }
- if (!apFound) {
- accessPoint.setUnreachable();
- }
- accessPoints.add(accessPoint);
- apMap.put(accessPoint.getSsidStr(), accessPoint);
- } else {
- // If we aren't using saved networks, drop them into the cache so that
- // we have access to their saved info.
- cachedAccessPoints.add(accessPoint);
- }
+ configsByKey.put(AccessPoint.getKey(config), config);
}
}
final List<NetworkKey> scoresToRequest = new ArrayList<>();
if (results != null) {
+ // TODO(sghuman): Move this loop to updateScanResultCache and make instance variable
+ // after double handlers are removed.
+ ArrayMap<String, List<ScanResult>> scanResultsByApKey = new ArrayMap<>();
for (ScanResult result : results) {
// Ignore hidden and ad-hoc networks.
if (result.SSID == null || result.SSID.length() == 0 ||
@@ -572,38 +570,35 @@ public class WifiTracker {
scoresToRequest.add(key);
}
- boolean found = false;
- for (AccessPoint accessPoint : apMap.getAll(result.SSID)) {
- // We want to evict old scan results if are current results are not stale
- if (accessPoint.update(result, !mStaleScanResults)) {
- found = true;
- break;
- }
+ String apKey = AccessPoint.getKey(result);
+ List<ScanResult> resultList;
+ if (scanResultsByApKey.containsKey(apKey)) {
+ resultList = scanResultsByApKey.get(apKey);
+ } else {
+ resultList = new ArrayList<>();
+ scanResultsByApKey.put(apKey, resultList);
}
- if (!found && mIncludeScans) {
- AccessPoint accessPoint = getCachedOrCreate(result, cachedAccessPoints);
- if (mLastInfo != null && mLastNetworkInfo != null) {
- accessPoint.update(connectionConfig, mLastInfo, mLastNetworkInfo);
- }
- if (result.isPasspointNetwork()) {
- // Retrieve a WifiConfiguration for a Passpoint provider that matches
- // the given ScanResult. This is used for showing that a given AP
- // (ScanResult) is available via a Passpoint provider (provider friendly
- // name).
- try {
- WifiConfiguration config = mWifiManager.getMatchingWifiConfig(result);
- if (config != null) {
- accessPoint.update(config);
- }
- } catch (UnsupportedOperationException e) {
- // Passpoint not supported on the device.
- }
- }
+ resultList.add(result);
+ }
+
+ for (Map.Entry<String, List<ScanResult>> entry : scanResultsByApKey.entrySet()) {
+ // List can not be empty as it is dynamically constructed on each iteration
+ ScanResult firstResult = entry.getValue().get(0);
+
+ AccessPoint accessPoint =
+ getCachedOrCreate(entry.getValue(), cachedAccessPoints);
+ if (mLastInfo != null && mLastNetworkInfo != null) {
+ accessPoint.update(connectionConfig, mLastInfo, mLastNetworkInfo);
+ }
- accessPoints.add(accessPoint);
- apMap.put(accessPoint.getSsidStr(), accessPoint);
+ // Update the matching config if there is one, to populate saved network info
+ WifiConfiguration config = configsByKey.get(entry.getKey());
+ if (config != null) {
+ accessPoint.update(config);
}
+
+ accessPoints.add(accessPoint);
}
}
@@ -643,17 +638,18 @@ public class WifiTracker {
}
@VisibleForTesting
- AccessPoint getCachedOrCreate(ScanResult result, List<AccessPoint> cache) {
+ AccessPoint getCachedOrCreate(
+ List<ScanResult> scanResults,
+ List<AccessPoint> cache) {
final int N = cache.size();
for (int i = 0; i < N; i++) {
- if (cache.get(i).matches(result)) {
+ if (cache.get(i).getKey().equals(AccessPoint.getKey(scanResults.get(0)))) {
AccessPoint ret = cache.remove(i);
- // evict old scan results only if we have fresh results
- ret.update(result, !mStaleScanResults);
+ ret.setScanResults(scanResults);
return ret;
}
}
- final AccessPoint accessPoint = new AccessPoint(mContext, result);
+ final AccessPoint accessPoint = new AccessPoint(mContext, scanResults);
accessPoint.setListener(mAccessPointListenerAdapter);
return accessPoint;
}
@@ -761,15 +757,6 @@ public class WifiTracker {
}
}
- public static List<AccessPoint> getCurrentAccessPoints(Context context, boolean includeSaved,
- boolean includeScans, boolean includePasspoints) {
- WifiTracker tracker = new WifiTracker(context,
- null, null, includeSaved, includeScans, includePasspoints);
- tracker.forceUpdate();
- tracker.copyAndNotifyListeners(false /*notifyListeners*/);
- return tracker.getAccessPoints();
- }
-
@VisibleForTesting
final BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
@@ -959,11 +946,6 @@ public class WifiTracker {
}
}
- void forceScan() {
- removeMessages(MSG_SCAN);
- sendEmptyMessage(MSG_SCAN);
- }
-
void pause() {
mRetry = 0;
removeMessages(MSG_SCAN);
@@ -1009,6 +991,39 @@ public class WifiTracker {
}
}
+ /**
+ * Wraps the given {@link WifiListener} instance and executes it's methods on the Main Thread.
+ *
+ * <p>This mechanism allows us to no longer need a separate MainHandler and WorkHandler, which
+ * were previously both performing work, while avoiding errors which occur from executing
+ * callbacks which manipulate UI elements from a different thread than the MainThread.
+ */
+ private static class WifiListenerWrapper implements WifiListener {
+
+ private final Handler mHandler;
+ private final WifiListener mDelegatee;
+
+ public WifiListenerWrapper(WifiListener listener) {
+ mHandler = new Handler(Looper.getMainLooper());
+ mDelegatee = listener;
+ }
+
+ @Override
+ public void onWifiStateChanged(int state) {
+ mHandler.post(() -> mDelegatee.onWifiStateChanged(state));
+ }
+
+ @Override
+ public void onConnectedChanged() {
+ mHandler.post(() -> mDelegatee.onConnectedChanged());
+ }
+
+ @Override
+ public void onAccessPointsChanged() {
+ mHandler.post(() -> mDelegatee.onAccessPointsChanged());
+ }
+ }
+
public interface WifiListener {
/**
* Called when the state of Wifi has changed, the state will be one of
diff --git a/packages/SettingsLib/src/com/android/settingslib/wifi/WifiTrackerFactory.java b/packages/SettingsLib/src/com/android/settingslib/wifi/WifiTrackerFactory.java
index 79cee046140d..8b5863aee91f 100644
--- a/packages/SettingsLib/src/com/android/settingslib/wifi/WifiTrackerFactory.java
+++ b/packages/SettingsLib/src/com/android/settingslib/wifi/WifiTrackerFactory.java
@@ -16,8 +16,10 @@
package com.android.settingslib.wifi;
import android.content.Context;
-import android.os.Looper;
import android.support.annotation.Keep;
+import android.support.annotation.NonNull;
+
+import com.android.settingslib.core.lifecycle.Lifecycle;
/**
* Factory method used to inject WifiTracker instances.
@@ -31,12 +33,11 @@ public class WifiTrackerFactory {
}
public static WifiTracker create(
- Context context, WifiTracker.WifiListener wifiListener, Looper workerLooper,
- boolean includeSaved, boolean includeScans, boolean includePasspoints) {
+ Context context, WifiTracker.WifiListener wifiListener, @NonNull Lifecycle lifecycle,
+ boolean includeSaved, boolean includeScans) {
if(sTestingWifiTracker != null) {
return sTestingWifiTracker;
}
- return new WifiTracker(
- context, wifiListener, workerLooper, includeSaved, includeScans, includePasspoints);
+ return new WifiTracker(context, wifiListener, lifecycle, includeSaved, includeScans);
}
}
diff --git a/packages/SettingsLib/src/com/android/settingslib/wifi/WifiUtils.java b/packages/SettingsLib/src/com/android/settingslib/wifi/WifiUtils.java
new file mode 100644
index 000000000000..fd48eea25e00
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/wifi/WifiUtils.java
@@ -0,0 +1,197 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.wifi;
+
+import android.net.wifi.ScanResult;
+import android.net.wifi.WifiConfiguration;
+import android.net.wifi.WifiInfo;
+import android.os.SystemClock;
+import android.support.annotation.VisibleForTesting;
+
+import java.util.Map;
+
+public class WifiUtils {
+
+ public static String buildLoggingSummary(AccessPoint accessPoint, WifiConfiguration config) {
+ final StringBuilder summary = new StringBuilder();
+ final WifiInfo info = accessPoint.getInfo();
+ // Add RSSI/band information for this config, what was seen up to 6 seconds ago
+ // verbose WiFi Logging is only turned on thru developers settings
+ if (accessPoint.isActive() && info != null) {
+ summary.append(" f=" + Integer.toString(info.getFrequency()));
+ }
+ summary.append(" " + getVisibilityStatus(accessPoint));
+ if (config != null && !config.getNetworkSelectionStatus().isNetworkEnabled()) {
+ summary.append(" (" + config.getNetworkSelectionStatus().getNetworkStatusString());
+ if (config.getNetworkSelectionStatus().getDisableTime() > 0) {
+ long now = System.currentTimeMillis();
+ long diff = (now - config.getNetworkSelectionStatus().getDisableTime()) / 1000;
+ long sec = diff % 60; //seconds
+ long min = (diff / 60) % 60; //minutes
+ long hour = (min / 60) % 60; //hours
+ summary.append(", ");
+ if (hour > 0) summary.append(Long.toString(hour) + "h ");
+ summary.append(Long.toString(min) + "m ");
+ summary.append(Long.toString(sec) + "s ");
+ }
+ summary.append(")");
+ }
+
+ if (config != null) {
+ WifiConfiguration.NetworkSelectionStatus networkStatus =
+ config.getNetworkSelectionStatus();
+ for (int index = WifiConfiguration.NetworkSelectionStatus.NETWORK_SELECTION_ENABLE;
+ index < WifiConfiguration.NetworkSelectionStatus
+ .NETWORK_SELECTION_DISABLED_MAX; index++) {
+ if (networkStatus.getDisableReasonCounter(index) != 0) {
+ summary.append(" " + WifiConfiguration.NetworkSelectionStatus
+ .getNetworkDisableReasonString(index) + "="
+ + networkStatus.getDisableReasonCounter(index));
+ }
+ }
+ }
+
+ return summary.toString();
+ }
+
+ /**
+ * Returns the visibility status of the WifiConfiguration.
+ *
+ * @return autojoin debugging information
+ * TODO: use a string formatter
+ * ["rssi 5Ghz", "num results on 5GHz" / "rssi 5Ghz", "num results on 5GHz"]
+ * For instance [-40,5/-30,2]
+ */
+ private static String getVisibilityStatus(AccessPoint accessPoint) {
+ final WifiInfo info = accessPoint.getInfo();
+ StringBuilder visibility = new StringBuilder();
+ StringBuilder scans24GHz = new StringBuilder();
+ StringBuilder scans5GHz = new StringBuilder();
+ String bssid = null;
+
+ if (accessPoint.isActive() && info != null) {
+ bssid = info.getBSSID();
+ if (bssid != null) {
+ visibility.append(" ").append(bssid);
+ }
+ visibility.append(" rssi=").append(info.getRssi());
+ visibility.append(" ");
+ visibility.append(" score=").append(info.score);
+ if (accessPoint.getSpeed() != AccessPoint.Speed.NONE) {
+ visibility.append(" speed=").append(accessPoint.getSpeedLabel());
+ }
+ visibility.append(String.format(" tx=%.1f,", info.txSuccessRate));
+ visibility.append(String.format("%.1f,", info.txRetriesRate));
+ visibility.append(String.format("%.1f ", info.txBadRate));
+ visibility.append(String.format("rx=%.1f", info.rxSuccessRate));
+ }
+
+ int maxRssi5 = WifiConfiguration.INVALID_RSSI;
+ int maxRssi24 = WifiConfiguration.INVALID_RSSI;
+ final int maxDisplayedScans = 4;
+ int num5 = 0; // number of scanned BSSID on 5GHz band
+ int num24 = 0; // number of scanned BSSID on 2.4Ghz band
+ int numBlackListed = 0;
+
+ // TODO: sort list by RSSI or age
+ long nowMs = SystemClock.elapsedRealtime();
+ for (ScanResult result : accessPoint.getScanResults()) {
+ if (result.frequency >= AccessPoint.LOWER_FREQ_5GHZ
+ && result.frequency <= AccessPoint.HIGHER_FREQ_5GHZ) {
+ // Strictly speaking: [4915, 5825]
+ num5++;
+
+ if (result.level > maxRssi5) {
+ maxRssi5 = result.level;
+ }
+ if (num5 <= maxDisplayedScans) {
+ scans5GHz.append(
+ verboseScanResultSummary(accessPoint, result, bssid,
+ nowMs));
+ }
+ } else if (result.frequency >= AccessPoint.LOWER_FREQ_24GHZ
+ && result.frequency <= AccessPoint.HIGHER_FREQ_24GHZ) {
+ // Strictly speaking: [2412, 2482]
+ num24++;
+
+ if (result.level > maxRssi24) {
+ maxRssi24 = result.level;
+ }
+ if (num24 <= maxDisplayedScans) {
+ scans24GHz.append(
+ verboseScanResultSummary(accessPoint, result, bssid,
+ nowMs));
+ }
+ }
+ }
+ visibility.append(" [");
+ if (num24 > 0) {
+ visibility.append("(").append(num24).append(")");
+ if (num24 > maxDisplayedScans) {
+ visibility.append("max=").append(maxRssi24).append(",");
+ }
+ visibility.append(scans24GHz.toString());
+ }
+ visibility.append(";");
+ if (num5 > 0) {
+ visibility.append("(").append(num5).append(")");
+ if (num5 > maxDisplayedScans) {
+ visibility.append("max=").append(maxRssi5).append(",");
+ }
+ visibility.append(scans5GHz.toString());
+ }
+ if (numBlackListed > 0) {
+ visibility.append("!").append(numBlackListed);
+ }
+ visibility.append("]");
+
+ return visibility.toString();
+ }
+
+ @VisibleForTesting
+ /* package */ static String verboseScanResultSummary(AccessPoint accessPoint, ScanResult result,
+ String bssid, long nowMs) {
+ StringBuilder stringBuilder = new StringBuilder();
+ stringBuilder.append(" \n{").append(result.BSSID);
+ if (result.BSSID.equals(bssid)) {
+ stringBuilder.append("*");
+ }
+ stringBuilder.append("=").append(result.frequency);
+ stringBuilder.append(",").append(result.level);
+ int speed = getSpecificApSpeed(result, accessPoint.getScoredNetworkCache());
+ if (speed != AccessPoint.Speed.NONE) {
+ stringBuilder.append(",")
+ .append(accessPoint.getSpeedLabel(speed));
+ }
+ int ageSeconds = (int) (nowMs - result.timestamp / 1000) / 1000;
+ stringBuilder.append(",").append(ageSeconds).append("s");
+ stringBuilder.append("}");
+ return stringBuilder.toString();
+ }
+
+ @AccessPoint.Speed
+ private static int getSpecificApSpeed(ScanResult result,
+ Map<String, TimestampedScoredNetwork> scoredNetworkCache) {
+ TimestampedScoredNetwork timedScore = scoredNetworkCache.get(result.BSSID);
+ if (timedScore == null) {
+ return AccessPoint.Speed.NONE;
+ }
+ // For debugging purposes we may want to use mRssi rather than result.level as the average
+ // speed wil be determined by mRssi
+ return timedScore.getScore().calculateBadge(result.level);
+ }
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothA2dpWrapperImpl.java b/packages/SettingsLib/src/com/android/settingslib/wrapper/BluetoothA2dpWrapper.java
index c49bb98d3878..17e34016a53b 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothA2dpWrapperImpl.java
+++ b/packages/SettingsLib/src/com/android/settingslib/wrapper/BluetoothA2dpWrapper.java
@@ -14,48 +14,56 @@
* limitations under the License.
*/
-package com.android.settingslib.bluetooth;
+package com.android.settingslib.wrapper;
import android.bluetooth.BluetoothA2dp;
import android.bluetooth.BluetoothCodecStatus;
import android.bluetooth.BluetoothDevice;
-public class BluetoothA2dpWrapperImpl implements BluetoothA2dpWrapper {
-
- public static class Factory implements BluetoothA2dpWrapper.Factory {
- @Override
- public BluetoothA2dpWrapper getInstance(BluetoothA2dp service) {
- return new BluetoothA2dpWrapperImpl(service);
- }
- }
+/**
+ * This class replicates some methods of android.bluetooth.BluetoothA2dp that are new and not
+ * yet available in our current version of Robolectric. It provides a thin wrapper to call the real
+ * methods in production and a mock in tests.
+ */
+public class BluetoothA2dpWrapper {
private BluetoothA2dp mService;
- public BluetoothA2dpWrapperImpl(BluetoothA2dp service) {
+ public BluetoothA2dpWrapper(BluetoothA2dp service) {
mService = service;
}
- @Override
+ /**
+ * @return the real {@code BluetoothA2dp} object
+ */
public BluetoothA2dp getService() {
return mService;
}
- @Override
+ /**
+ * Wraps {@code BluetoothA2dp.getCodecStatus}
+ */
public BluetoothCodecStatus getCodecStatus(BluetoothDevice device) {
return mService.getCodecStatus(device);
}
- @Override
+ /**
+ * Wraps {@code BluetoothA2dp.supportsOptionalCodecs}
+ */
public int supportsOptionalCodecs(BluetoothDevice device) {
return mService.supportsOptionalCodecs(device);
}
- @Override
+ /**
+ * Wraps {@code BluetoothA2dp.getOptionalCodecsEnabled}
+ */
public int getOptionalCodecsEnabled(BluetoothDevice device) {
return mService.getOptionalCodecsEnabled(device);
}
- @Override
+ /**
+ * Wraps {@code BluetoothA2dp.setOptionalCodecsEnabled}
+ */
public void setOptionalCodecsEnabled(BluetoothDevice device, int value) {
mService.setOptionalCodecsEnabled(device, value);
}
diff --git a/packages/SettingsLib/src/com/android/settingslib/wrapper/LocationManagerWrapper.java b/packages/SettingsLib/src/com/android/settingslib/wrapper/LocationManagerWrapper.java
new file mode 100644
index 000000000000..1a268a608e5d
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/wrapper/LocationManagerWrapper.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.wrapper;
+
+import android.location.LocationManager;
+import android.os.UserHandle;
+
+/**
+ * This class replicates some methods of android.location.LocationManager that are new and not
+ * yet available in our current version of Robolectric. It provides a thin wrapper to call the real
+ * methods in production and a mock in tests.
+ */
+public class LocationManagerWrapper {
+
+ private LocationManager mLocationManager;
+
+ public LocationManagerWrapper(LocationManager locationManager) {
+ mLocationManager = locationManager;
+ }
+
+ /** Returns the real {@code LocationManager} object */
+ public LocationManager getLocationManager() {
+ return mLocationManager;
+ }
+
+ /** Wraps {@code LocationManager.isProviderEnabled} method */
+ public boolean isProviderEnabled(String provider) {
+ return mLocationManager.isProviderEnabled(provider);
+ }
+
+ /** Wraps {@code LocationManager.setProviderEnabledForUser} method */
+ public void setProviderEnabledForUser(String provider, boolean enabled, UserHandle userHandle) {
+ mLocationManager.setProviderEnabledForUser(provider, enabled, userHandle);
+ }
+
+ /** Wraps {@code LocationManager.isLocationEnabled} method */
+ public boolean isLocationEnabled() {
+ return mLocationManager.isLocationEnabled();
+ }
+
+ /** Wraps {@code LocationManager.isLocationEnabledForUser} method */
+ public boolean isLocationEnabledForUser(UserHandle userHandle) {
+ return mLocationManager.isLocationEnabledForUser(userHandle);
+ }
+
+ /** Wraps {@code LocationManager.setLocationEnabledForUser} method */
+ public void setLocationEnabledForUser(boolean enabled, UserHandle userHandle) {
+ mLocationManager.setLocationEnabledForUser(enabled, userHandle);
+ }
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/wrapper/PackageManagerWrapper.java b/packages/SettingsLib/src/com/android/settingslib/wrapper/PackageManagerWrapper.java
new file mode 100644
index 000000000000..b1f3f3ce4300
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/wrapper/PackageManagerWrapper.java
@@ -0,0 +1,246 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.settingslib.wrapper;
+
+import android.content.ComponentName;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.IPackageDeleteObserver;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.ResolveInfo;
+import android.graphics.drawable.Drawable;
+import android.os.UserHandle;
+import android.os.storage.VolumeInfo;
+
+import java.util.List;
+
+/**
+ * A thin wrapper class that simplifies testing by putting a mockable layer between the application
+ * and the PackageManager. This class only provides access to the minimum number of functions from
+ * the PackageManager needed for DeletionHelper to work.
+ */
+public class PackageManagerWrapper {
+
+ private final PackageManager mPm;
+
+ public PackageManagerWrapper(PackageManager pm) {
+ mPm = pm;
+ }
+
+ /**
+ * Returns the real {@code PackageManager} object.
+ */
+ public PackageManager getPackageManager() {
+ return mPm;
+ }
+
+ /**
+ * Calls {@code PackageManager.getInstalledApplicationsAsUser()}.
+ *
+ * @see android.content.pm.PackageManager#getInstalledApplicationsAsUser
+ */
+ public List<ApplicationInfo> getInstalledApplicationsAsUser(int flags, int userId) {
+ return mPm.getInstalledApplicationsAsUser(flags, userId);
+ }
+
+ /**
+ * Calls {@code PackageManager.getInstalledPackagesAsUser}
+ */
+ public List<PackageInfo> getInstalledPackagesAsUser(int flags, int userId) {
+ return mPm.getInstalledPackagesAsUser(flags, userId);
+ }
+
+ /**
+ * Calls {@code PackageManager.hasSystemFeature()}.
+ *
+ * @see android.content.pm.PackageManager#hasSystemFeature
+ */
+ public boolean hasSystemFeature(String name) {
+ return mPm.hasSystemFeature(name);
+ }
+
+ /**
+ * Calls {@code PackageManager.queryIntentActivitiesAsUser()}.
+ *
+ * @see android.content.pm.PackageManager#queryIntentActivitiesAsUser
+ */
+ public List<ResolveInfo> queryIntentActivitiesAsUser(Intent intent, int flags, int userId) {
+ return mPm.queryIntentActivitiesAsUser(intent, flags, userId);
+ }
+
+ /**
+ * Calls {@code PackageManager.getInstallReason()}.
+ *
+ * @see android.content.pm.PackageManager#getInstallReason
+ */
+ public int getInstallReason(String packageName, UserHandle user) {
+ return mPm.getInstallReason(packageName, user);
+ }
+
+ /**
+ * Calls {@code PackageManager.getApplicationInfoAsUser}
+ */
+ public ApplicationInfo getApplicationInfoAsUser(String packageName, int i, int userId)
+ throws PackageManager.NameNotFoundException {
+ return mPm.getApplicationInfoAsUser(packageName, i, userId);
+ }
+
+ /**
+ * Calls {@code PackageManager.setDefaultBrowserPackageNameAsUser}
+ */
+ public boolean setDefaultBrowserPackageNameAsUser(String packageName, int userId) {
+ return mPm.setDefaultBrowserPackageNameAsUser(packageName, userId);
+ }
+
+ /**
+ * Calls {@code PackageManager.getDefaultBrowserPackageNameAsUser}
+ */
+ public String getDefaultBrowserPackageNameAsUser(int userId) {
+ return mPm.getDefaultBrowserPackageNameAsUser(userId);
+ }
+
+ /**
+ * Calls {@code PackageManager.getHomeActivities}
+ */
+ public ComponentName getHomeActivities(List<ResolveInfo> homeActivities) {
+ return mPm.getHomeActivities(homeActivities);
+ }
+
+ /**
+ * Calls {@code PackageManager.queryIntentServicesAsUser}
+ */
+ public List<ResolveInfo> queryIntentServicesAsUser(Intent intent, int i, int user) {
+ return mPm.queryIntentServicesAsUser(intent, i, user);
+ }
+
+ /**
+ * Calls {@code PackageManager.replacePreferredActivity}
+ */
+ public void replacePreferredActivity(IntentFilter homeFilter, int matchCategoryEmpty,
+ ComponentName[] componentNames, ComponentName component) {
+ mPm.replacePreferredActivity(homeFilter, matchCategoryEmpty, componentNames, component);
+ }
+
+ /**
+ * Gets information about a particular package from the package manager.
+ *
+ * @param packageName The name of the package we would like information about.
+ * @param i additional options flags. see javadoc for
+ * {@link PackageManager#getPackageInfo(String, int)}
+ * @return The PackageInfo for the requested package
+ */
+ public PackageInfo getPackageInfo(String packageName, int i) throws NameNotFoundException {
+ return mPm.getPackageInfo(packageName, i);
+ }
+
+ /**
+ * Retrieves the icon associated with this particular set of ApplicationInfo
+ *
+ * @param info The ApplicationInfo to retrieve the icon for
+ * @return The icon as a drawable.
+ */
+ public Drawable getUserBadgedIcon(ApplicationInfo info) {
+ return mPm.getUserBadgedIcon(mPm.loadUnbadgedItemIcon(info, info),
+ new UserHandle(UserHandle.getUserId(info.uid)));
+ }
+
+ /**
+ * Retrieves the label associated with the particular set of ApplicationInfo
+ *
+ * @param app The ApplicationInfo to retrieve the label for
+ * @return the label as a CharSequence
+ */
+ public CharSequence loadLabel(ApplicationInfo app) {
+ return app.loadLabel(mPm);
+ }
+
+ /**
+ * Retrieve all activities that can be performed for the given intent.
+ */
+ public List<ResolveInfo> queryIntentActivities(Intent intent, int flags) {
+ return mPm.queryIntentActivities(intent, flags);
+ }
+
+ /**
+ * Calls {@code PackageManager.getPrimaryStorageCurrentVolume}
+ */
+ public VolumeInfo getPrimaryStorageCurrentVolume() {
+ return mPm.getPrimaryStorageCurrentVolume();
+ }
+
+ /**
+ * Calls {@code PackageManager.deletePackageAsUser}
+ */
+ public void deletePackageAsUser(String packageName, IPackageDeleteObserver observer, int flags,
+ int userId) {
+ mPm.deletePackageAsUser(packageName, observer, flags, userId);
+ }
+
+ /**
+ * Calls {@code PackageManager.getPackageUidAsUser}
+ */
+ public int getPackageUidAsUser(String pkg, int userId)
+ throws PackageManager.NameNotFoundException {
+ return mPm.getPackageUidAsUser(pkg, userId);
+ }
+
+ /**
+ * Calls {@code PackageManager.setApplicationEnabledSetting}
+ */
+ public void setApplicationEnabledSetting(String packageName, int newState, int flags) {
+ mPm.setApplicationEnabledSetting(packageName, newState, flags);
+ }
+
+ /**
+ * Calls {@code PackageManager.getApplicationEnabledSetting}
+ */
+ public int getApplicationEnabledSetting(String packageName) {
+ return mPm.getApplicationEnabledSetting(packageName);
+ }
+
+ /**
+ * Calls {@code PackageManager.setComponentEnabledSetting}
+ */
+ public void setComponentEnabledSetting(ComponentName componentName, int newState, int flags) {
+ mPm.setComponentEnabledSetting(componentName, newState, flags);
+ }
+
+ /**
+ * Calls {@code PackageManager.getApplicationInfo}
+ */
+ public ApplicationInfo getApplicationInfo(String packageName, int flags)
+ throws NameNotFoundException {
+ return mPm.getApplicationInfo(packageName, flags);
+ }
+
+ /**
+ * Calls {@code PackageManager.getApplicationLabel}
+ */
+ public CharSequence getApplicationLabel(ApplicationInfo info) {
+ return mPm.getApplicationLabel(info);
+ }
+
+ /**
+ * Calls {@code PackageManager.queryBroadcastReceivers}
+ */
+ public List<ResolveInfo> queryBroadcastReceivers(Intent intent, int flags) {
+ return mPm.queryBroadcastReceivers(intent, flags);
+ }
+}
+