diff options
Diffstat (limited to 'packages/SettingsLib/src')
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); + } +} + |