diff --git a/res/layout/battery_chart_graph.xml b/res/layout/battery_chart_graph.xml index 6187d07b66c..2984f43d5bc 100644 --- a/res/layout/battery_chart_graph.xml +++ b/res/layout/battery_chart_graph.xml @@ -26,7 +26,7 @@ android:id="@+id/chart_summary" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginBottom="45dp" + android:layout_marginBottom="30dp" android:textAppearance="?android:attr/textAppearanceSmall" settings:textColor="?android:attr/textColorSecondary" android:text="@string/battery_usage_chart_graph_hint" /> @@ -34,9 +34,16 @@ + diff --git a/res/layout/companion_apps_remove_button_widget.xml b/res/layout/companion_apps_remove_button_widget.xml new file mode 100644 index 00000000000..a3c229537ac --- /dev/null +++ b/res/layout/companion_apps_remove_button_widget.xml @@ -0,0 +1,24 @@ + + + + \ No newline at end of file diff --git a/res/layout/preference_companion_app.xml b/res/layout/preference_companion_app.xml new file mode 100644 index 00000000000..22712753b2f --- /dev/null +++ b/res/layout/preference_companion_app.xml @@ -0,0 +1,26 @@ + + + + + diff --git a/res/values/dimens.xml b/res/values/dimens.xml index 0aaf930e112..7bc760a9c6f 100755 --- a/res/values/dimens.xml +++ b/res/values/dimens.xml @@ -205,6 +205,9 @@ 21dp 16dp + + 20dp + 1px 16dp @@ -441,6 +444,7 @@ 24dp + 3dp 1dp 4dp 3dp diff --git a/res/values/strings.xml b/res/values/strings.xml index 19f892e5525..41f76e8f974 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -1815,6 +1815,10 @@ Device\'s Bluetooth address: %1$s Forget device? + + Remove association + + Disconnect App? Your phone will no longer be paired with %1$s @@ -1822,12 +1826,16 @@ Your tablet will no longer be paired with %1$s Your device will no longer be paired with %1$s + + %1$s app will no longer connect to your %2$s %1$s will no longer be paired with any device linked to this account Forget device + + Disconnect app Connect to\u2026 diff --git a/res/xml/accessibility_button_settings.xml b/res/xml/accessibility_button_settings.xml index 5e81616bffc..bb16f0397cd 100644 --- a/res/xml/accessibility_button_settings.xml +++ b/res/xml/accessibility_button_settings.xml @@ -56,7 +56,6 @@ diff --git a/res/xml/bluetooth_device_details_fragment.xml b/res/xml/bluetooth_device_details_fragment.xml index 94052b67213..5084d4d481a 100644 --- a/res/xml/bluetooth_device_details_fragment.xml +++ b/res/xml/bluetooth_device_details_fragment.xml @@ -44,6 +44,9 @@ settings:allowDividerBelow="true" settings:allowDividerAbove="true"/> + + diff --git a/res/xml/date_time_prefs.xml b/res/xml/date_time_prefs.xml index 47056dfb5a8..27311322e28 100644 --- a/res/xml/date_time_prefs.xml +++ b/res/xml/date_time_prefs.xml @@ -38,26 +38,31 @@ android:summary="@string/summary_placeholder" settings:userRestriction="no_config_date_time"/> - + - + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/xml/privacy_dashboard_settings.xml b/res/xml/privacy_dashboard_settings.xml index fcd0de493f8..a1a608d20ac 100644 --- a/res/xml/privacy_dashboard_settings.xml +++ b/res/xml/privacy_dashboard_settings.xml @@ -78,7 +78,7 @@ diff --git a/src/com/android/settings/Utils.java b/src/com/android/settings/Utils.java index 84183154236..031fb8a9d92 100644 --- a/src/com/android/settings/Utils.java +++ b/src/com/android/settings/Utils.java @@ -48,6 +48,7 @@ import android.content.res.TypedArray; import android.database.Cursor; import android.graphics.Bitmap; import android.graphics.Canvas; +import android.graphics.drawable.AdaptiveIconDrawable; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.graphics.drawable.VectorDrawable; @@ -94,6 +95,7 @@ import android.widget.EditText; import android.widget.ListView; import android.widget.TabWidget; +import androidx.annotation.ColorInt; import androidx.annotation.NonNull; import androidx.annotation.StringRes; import androidx.core.graphics.drawable.IconCompat; @@ -111,6 +113,7 @@ import com.android.settings.dashboard.profileselector.ProfileFragmentBridge; import com.android.settings.dashboard.profileselector.ProfileSelectFragment; import com.android.settings.password.ChooseLockSettingsHelper; import com.android.settingslib.widget.ActionBarShadowController; +import com.android.settingslib.widget.AdaptiveIcon; import java.util.Iterator; import java.util.List; @@ -967,15 +970,36 @@ public final class Utils extends com.android.settingslib.Utils { } /** - * Sets the preference icon with a drawable that is scaled down to to avoid crashing Settings if - * it's too big. + * Gets the adaptive icon with a drawable that wrapped with an adaptive background using {@code + * backgroundColor} if it is not a {@link AdaptiveIconDrawable} + * + * If the given {@code icon} is too big, it will be auto scaled down to to avoid crashing + * Settings. */ - public static void setSafeIcon(Preference pref, Drawable icon) { + public static Drawable getAdaptiveIcon(Context context, Drawable icon, + @ColorInt int backgroundColor) { + Drawable adaptiveIcon = getSafeIcon(icon); + + if (!(adaptiveIcon instanceof AdaptiveIconDrawable)) { + adaptiveIcon = new AdaptiveIcon(context, adaptiveIcon); + ((AdaptiveIcon) adaptiveIcon).setBackgroundColor(backgroundColor); + } + + return adaptiveIcon; + } + + /** + * Gets the icon with a drawable that is scaled down to to avoid crashing Settings if it's too + * big and not a {@link VectorDrawable}. + */ + public static Drawable getSafeIcon(Drawable icon) { Drawable safeIcon = icon; + if ((icon != null) && !(icon instanceof VectorDrawable)) { safeIcon = getSafeDrawable(icon, 500, 500); } - pref.setIcon(safeIcon); + + return safeIcon; } /** @@ -985,7 +1009,7 @@ public final class Utils extends com.android.settingslib.Utils { * @param maxWidth maximum width, in pixels. * @param maxHeight maximum height, in pixels. */ - public static Drawable getSafeDrawable(Drawable original, int maxWidth, int maxHeight) { + private static Drawable getSafeDrawable(Drawable original, int maxWidth, int maxHeight) { final int actualWidth = original.getMinimumWidth(); final int actualHeight = original.getMinimumHeight(); diff --git a/src/com/android/settings/accessibility/AccessibilitySettings.java b/src/com/android/settings/accessibility/AccessibilitySettings.java index af8bf47036f..d4db3953f79 100644 --- a/src/com/android/settings/accessibility/AccessibilitySettings.java +++ b/src/com/android/settings/accessibility/AccessibilitySettings.java @@ -28,6 +28,7 @@ import android.content.pm.ActivityInfo; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.pm.ServiceInfo; +import android.graphics.Color; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Bundle; @@ -663,7 +664,7 @@ public class AccessibilitySettings extends DashboardFragment { preference.setKey(key); preference.setTitle(title); preference.setSummary(summary); - Utils.setSafeIcon(preference, icon); + preference.setIcon(Utils.getAdaptiveIcon(mContext, icon, Color.WHITE)); preference.setFragment(fragment); preference.setIconSize(ICON_SIZE_MEDIUM); preference.setPersistent(false); // Disable SharedPreferences. diff --git a/src/com/android/settings/applications/autofill/PasswordsPreferenceController.java b/src/com/android/settings/applications/autofill/PasswordsPreferenceController.java index 47882ded46e..a9d1572b7e5 100644 --- a/src/com/android/settings/applications/autofill/PasswordsPreferenceController.java +++ b/src/com/android/settings/applications/autofill/PasswordsPreferenceController.java @@ -124,7 +124,7 @@ public class PasswordsPreferenceController extends BasePreferenceController serviceInfo, serviceInfo.applicationInfo, user); - Utils.setSafeIcon(pref, icon); + pref.setIcon(Utils.getSafeIcon(icon)); pref.setIntent( new Intent(Intent.ACTION_MAIN) .setClassName(serviceInfo.packageName, service.getPasswordsActivity())); diff --git a/src/com/android/settings/applications/defaultapps/DefaultAppPreferenceController.java b/src/com/android/settings/applications/defaultapps/DefaultAppPreferenceController.java index 8e8e072a017..fd6f4c243e7 100644 --- a/src/com/android/settings/applications/defaultapps/DefaultAppPreferenceController.java +++ b/src/com/android/settings/applications/defaultapps/DefaultAppPreferenceController.java @@ -66,7 +66,7 @@ public abstract class DefaultAppPreferenceController extends AbstractPreferenceC } if (!TextUtils.isEmpty(defaultAppLabel)) { preference.setSummary(defaultAppLabel); - Utils.setSafeIcon(preference, getDefaultAppIcon()); + preference.setIcon(Utils.getSafeIcon(getDefaultAppIcon())); } else { Log.d(TAG, "No default app"); preference.setSummary(R.string.app_list_preference_none); diff --git a/src/com/android/settings/biometrics/face/FaceSettingsAttentionPreferenceController.java b/src/com/android/settings/biometrics/face/FaceSettingsAttentionPreferenceController.java index 200c0b949ec..0eb823bc4d9 100644 --- a/src/com/android/settings/biometrics/face/FaceSettingsAttentionPreferenceController.java +++ b/src/com/android/settings/biometrics/face/FaceSettingsAttentionPreferenceController.java @@ -57,15 +57,15 @@ public class FaceSettingsAttentionPreferenceController extends FaceSettingsPrefe private final GetFeatureCallback mGetFeatureCallback = new GetFeatureCallback() { @Override - public void onCompleted(boolean success, int feature, boolean value) { - if (feature == FaceManager.FEATURE_REQUIRE_ATTENTION && success) { - if (!mFaceManager.hasEnrolledTemplates(getUserId())) { - mPreference.setEnabled(false); - } else { - mPreference.setEnabled(true); - mPreference.setChecked(value); + public void onCompleted(boolean success, int[] features, boolean[] featureState) { + boolean requireAttentionEnabled = false; + for (int i = 0; i < features.length; i++) { + if (features[i] == FaceManager.FEATURE_REQUIRE_ATTENTION) { + requireAttentionEnabled = featureState[i]; } } + mPreference.setEnabled(success); + mPreference.setChecked(requireAttentionEnabled); } }; diff --git a/src/com/android/settings/bluetooth/BluetoothDetailsCompanionAppsController.java b/src/com/android/settings/bluetooth/BluetoothDetailsCompanionAppsController.java new file mode 100644 index 00000000000..f2a94ba7440 --- /dev/null +++ b/src/com/android/settings/bluetooth/BluetoothDetailsCompanionAppsController.java @@ -0,0 +1,224 @@ +/* + * Copyright (C) 2021 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.settings.bluetooth; + +import static com.android.internal.util.CollectionUtils.filter; + +import android.companion.Association; +import android.companion.CompanionDeviceManager; +import android.companion.ICompanionDeviceManager; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.graphics.drawable.Drawable; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.text.TextUtils; +import android.util.Log; + +import androidx.annotation.VisibleForTesting; +import androidx.appcompat.app.AlertDialog; +import androidx.preference.Preference; +import androidx.preference.PreferenceCategory; +import androidx.preference.PreferenceFragmentCompat; +import androidx.preference.PreferenceScreen; + +import com.android.settings.R; +import com.android.settingslib.bluetooth.CachedBluetoothDevice; +import com.android.settingslib.core.lifecycle.Lifecycle; + +import com.google.common.base.Objects; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + + +/** + * This class adds Companion Device app rows to launch the app or remove the associations + */ +public class BluetoothDetailsCompanionAppsController extends BluetoothDetailsController { + public static final String KEY_DEVICE_COMPANION_APPS = "device_companion_apps"; + private static final String LOG_TAG = "BTCompanionController"; + + private CachedBluetoothDevice mCachedDevice; + + @VisibleForTesting + PreferenceCategory mProfilesContainer; + + @VisibleForTesting + CompanionDeviceManager mCompanionDeviceManager; + + @VisibleForTesting + PackageManager mPackageManager; + + public BluetoothDetailsCompanionAppsController(Context context, + PreferenceFragmentCompat fragment, CachedBluetoothDevice device, Lifecycle lifecycle) { + super(context, fragment, device, lifecycle); + mCachedDevice = device; + mCompanionDeviceManager = context.getSystemService(CompanionDeviceManager.class); + mPackageManager = context.getPackageManager(); + lifecycle.addObserver(this); + } + + @Override + protected void init(PreferenceScreen screen) { + mProfilesContainer = screen.findPreference(getPreferenceKey()); + mProfilesContainer.setLayoutResource(R.layout.preference_companion_app); + } + + private List getAssociations(String address) { + return filter( + mCompanionDeviceManager.getAllAssociations(), + a -> Objects.equal(address, a.getDeviceMacAddress())); + } + + private static void removePreference(PreferenceCategory container, String packageName) { + Preference preference = container.findPreference(packageName); + if (preference != null) { + container.removePreference(preference); + } + } + + private void removeAssociationDialog(String packageName, String address, + PreferenceCategory container, CharSequence appName, Context context) { + DialogInterface.OnClickListener dialogClickListener = (dialog, which) -> { + if (which == DialogInterface.BUTTON_POSITIVE) { + removeAssociation(packageName, address, container); + } + }; + + AlertDialog.Builder builder = new AlertDialog.Builder(context); + + builder.setPositiveButton( + R.string.bluetooth_companion_app_remove_association_confirm_button, + dialogClickListener) + .setNegativeButton(android.R.string.cancel, dialogClickListener) + .setTitle(R.string.bluetooth_companion_app_remove_association_dialog_title) + .setMessage(mContext.getString( + R.string.bluetooth_companion_app_body, appName, mCachedDevice.getName())) + .show(); + } + + private static void removeAssociation(String packageName, String address, + PreferenceCategory container) { + try { + java.util.Objects.requireNonNull(ICompanionDeviceManager.Stub.asInterface( + ServiceManager.getService( + Context.COMPANION_DEVICE_SERVICE))).disassociate( + address, packageName); + } catch (RemoteException e) { + throw new RuntimeException(e); + } + + removePreference(container, packageName); + } + + private CharSequence getAppName(String packageName) { + CharSequence appName = null; + try { + appName = mPackageManager.getApplicationLabel( + mPackageManager.getApplicationInfo(packageName, 0)); + } catch (PackageManager.NameNotFoundException e) { + Log.e(LOG_TAG, "Package Not Found", e); + } + + return appName; + } + + private List getPreferencesNeedToShow(String address, PreferenceCategory container) { + List preferencesToRemove = new ArrayList<>(); + Set packages = getAssociations(address) + .stream().map(Association::getPackageName) + .collect(Collectors.toSet()); + + for (int i = 0; i < container.getPreferenceCount(); i++) { + String preferenceKey = container.getPreference(i).getKey(); + if (packages.isEmpty() || !packages.contains(preferenceKey)) { + preferencesToRemove.add(preferenceKey); + } + } + + for (String preferenceName : preferencesToRemove) { + removePreference(container, preferenceName); + } + + return packages.stream() + .filter(p -> container.findPreference(p) == null) + .collect(Collectors.toList()); + } + + /** + * Refreshes the state of the preferences for all the associations, possibly adding or + * removing preferences as needed. + */ + @Override + protected void refresh() { + updatePreferences(mContext, mCachedDevice.getAddress(), mProfilesContainer); + } + + /** + * Add preferences for each association for the bluetooth device + */ + public void updatePreferences(Context context, + String address, PreferenceCategory container) { + Set addedPackages = new HashSet<>(); + + for (String packageName : getPreferencesNeedToShow(address, container)) { + CharSequence appName = getAppName(packageName); + + if (TextUtils.isEmpty(appName) || !addedPackages.add(packageName)) { + continue; + } + + Drawable removeIcon = context.getResources().getDrawable(R.drawable.ic_clear); + CompanionAppWidgetPreference preference = new CompanionAppWidgetPreference( + removeIcon, + v -> removeAssociationDialog(packageName, address, container, appName, context), + context + ); + + Drawable appIcon; + + try { + appIcon = mPackageManager.getApplicationIcon(packageName); + } catch (PackageManager.NameNotFoundException e) { + Log.e(LOG_TAG, "Icon Not Found", e); + continue; + } + Intent intent = context.getPackageManager().getLaunchIntentForPackage(packageName); + preference.setIcon(appIcon); + preference.setTitle(appName.toString()); + preference.setOnPreferenceClickListener(v -> { + context.startActivity(intent); + return true; + }); + + preference.setKey(packageName); + preference.setVisible(true); + container.addPreference(preference); + } + } + + @Override + public String getPreferenceKey() { + return KEY_DEVICE_COMPANION_APPS; + } +} diff --git a/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragment.java b/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragment.java index a8812475ace..4980ba313fb 100644 --- a/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragment.java +++ b/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragment.java @@ -185,6 +185,8 @@ public class BluetoothDeviceDetailsFragment extends RestrictedDashboardFragment lifecycle, mManager)); controllers.add(new BluetoothDetailsButtonsController(context, this, mCachedDevice, lifecycle)); + controllers.add(new BluetoothDetailsCompanionAppsController(context, this, + mCachedDevice, lifecycle)); controllers.add(new BluetoothDetailsProfilesController(context, this, mManager, mCachedDevice, lifecycle)); controllers.add(new BluetoothDetailsMacAddressController(context, this, mCachedDevice, diff --git a/src/com/android/settings/bluetooth/CompanionAppWidgetPreference.java b/src/com/android/settings/bluetooth/CompanionAppWidgetPreference.java new file mode 100644 index 00000000000..cd0643373d6 --- /dev/null +++ b/src/com/android/settings/bluetooth/CompanionAppWidgetPreference.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2021 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.settings.bluetooth; + +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.view.View; +import android.widget.ImageButton; + +import androidx.preference.Preference; +import androidx.preference.PreferenceViewHolder; + +import com.android.settings.R; + +/** + * A custom preference for companion device apps. Added a button for association removal + */ +public class CompanionAppWidgetPreference extends Preference { + private Drawable mWidgetIcon; + private View.OnClickListener mWidgetListener; + private int mImageButtonPadding; + + public CompanionAppWidgetPreference(Drawable widgetIcon, View.OnClickListener widgetListener, + Context context) { + super(context); + mWidgetIcon = widgetIcon; + mWidgetListener = widgetListener; + mImageButtonPadding = context.getResources().getDimensionPixelSize( + R.dimen.bluetooth_companion_app_widget); + setWidgetLayoutResource(R.layout.companion_apps_remove_button_widget); + } + + @Override + public void onBindViewHolder(PreferenceViewHolder holder) { + super.onBindViewHolder(holder); + ImageButton imageButton = (ImageButton) holder.findViewById(R.id.remove_button); + imageButton.setPadding( + mImageButtonPadding, mImageButtonPadding, mImageButtonPadding, mImageButtonPadding); + imageButton.setColorFilter(getContext().getColor(android.R.color.darker_gray)); + imageButton.setImageDrawable(mWidgetIcon); + imageButton.setOnClickListener(mWidgetListener); + } + +} diff --git a/src/com/android/settings/dream/CurrentDreamPreferenceController.java b/src/com/android/settings/dream/CurrentDreamPreferenceController.java index a73898fdc0a..c267921fd2a 100644 --- a/src/com/android/settings/dream/CurrentDreamPreferenceController.java +++ b/src/com/android/settings/dream/CurrentDreamPreferenceController.java @@ -88,6 +88,6 @@ public class CurrentDreamPreferenceController extends BasePreferenceController { } final GearPreference gearPref = (GearPreference) preference; gearPref.setIconSize(RestrictedPreference.ICON_SIZE_SMALL); - Utils.setSafeIcon(gearPref, mBackend.getActiveIcon()); + gearPref.setIcon(Utils.getSafeIcon(mBackend.getActiveIcon())); } } diff --git a/src/com/android/settings/fuelgauge/AdvancedPowerUsageDetail.java b/src/com/android/settings/fuelgauge/AdvancedPowerUsageDetail.java index 3ef7c455302..4dbde251276 100644 --- a/src/com/android/settings/fuelgauge/AdvancedPowerUsageDetail.java +++ b/src/com/android/settings/fuelgauge/AdvancedPowerUsageDetail.java @@ -42,6 +42,8 @@ import com.android.settings.applications.appinfo.ButtonActionDialogFragment; import com.android.settings.core.InstrumentedPreferenceFragment; import com.android.settings.core.SubSettingLauncher; import com.android.settings.dashboard.DashboardFragment; +import com.android.settings.fuelgauge.batterytip.BatteryTipPreferenceController; +import com.android.settings.fuelgauge.batterytip.tips.BatteryTip; import com.android.settings.widget.EntityHeaderController; import com.android.settingslib.applications.AppUtils; import com.android.settingslib.applications.ApplicationsState; @@ -61,7 +63,7 @@ import java.util.List; */ public class AdvancedPowerUsageDetail extends DashboardFragment implements ButtonActionDialogFragment.AppButtonsDialogListener, - RadioButtonPreference.OnClickListener { + BatteryTipPreferenceController.BatteryTipListener, RadioButtonPreference.OnClickListener { public static final String TAG = "AdvancedPowerDetail"; public static final String EXTRA_UID = "extra_uid"; @@ -73,6 +75,8 @@ public class AdvancedPowerUsageDetail extends DashboardFragment implements public static final String EXTRA_POWER_USAGE_PERCENT = "extra_power_usage_percent"; public static final String EXTRA_POWER_USAGE_AMOUNT = "extra_power_usage_amount"; + private static final String KEY_PREF_FOREGROUND = "app_usage_foreground"; + private static final String KEY_PREF_BACKGROUND = "app_usage_background"; private static final String KEY_PREF_HEADER = "header_view"; private static final String KEY_PREF_UNRESTRICTED = "unrestricted_pref"; private static final String KEY_PREF_OPTIMIZED = "optimized_pref"; @@ -92,7 +96,10 @@ public class AdvancedPowerUsageDetail extends DashboardFragment implements BatteryUtils mBatteryUtils; @VisibleForTesting BatteryOptimizeUtils mBatteryOptimizeUtils; - + @VisibleForTesting + Preference mForegroundPreference; + @VisibleForTesting + Preference mBackgroundPreference; @VisibleForTesting Preference mFooterPreference; @VisibleForTesting @@ -101,18 +108,18 @@ public class AdvancedPowerUsageDetail extends DashboardFragment implements RadioButtonPreference mOptimizePreference; @VisibleForTesting RadioButtonPreference mUnrestrictedPreference; - private AppButtonsPreferenceController mAppButtonsPreferenceController; - private UnrestrictedPreferenceController mUnrestrictedPreferenceController; - private OptimizedPreferenceController mOptimizedPreferenceController; - private RestrictedPreferenceController mRestrictedPreferenceController; + @VisibleForTesting + boolean enableTriState = true; - private String mPackageName; + private AppButtonsPreferenceController mAppButtonsPreferenceController; + private BackgroundActivityPreferenceController mBackgroundActivityPreferenceController; // A wrapper class to carry LaunchBatteryDetailPage required arguments. private static final class LaunchBatteryDetailPageArgs { private String mUsagePercent; private String mPackageName; private String mAppLabel; + private String mSlotInformation; private int mUid; private int mIconId; private int mConsumedPower; @@ -124,18 +131,22 @@ public class AdvancedPowerUsageDetail extends DashboardFragment implements /** Launches battery details page for an individual battery consumer. */ public static void startBatteryDetailPage( Activity caller, InstrumentedPreferenceFragment fragment, - BatteryDiffEntry diffEntry, String usagePercent) { + BatteryDiffEntry diffEntry, String usagePercent, + boolean isValidToShowSummary, String slotInformation) { final BatteryHistEntry histEntry = diffEntry.mBatteryHistEntry; final LaunchBatteryDetailPageArgs launchArgs = new LaunchBatteryDetailPageArgs(); // configure the launch argument. launchArgs.mUsagePercent = usagePercent; launchArgs.mPackageName = diffEntry.getPackageName(); launchArgs.mAppLabel = diffEntry.getAppLabel(); + launchArgs.mSlotInformation = slotInformation; launchArgs.mUid = (int) histEntry.mUid; launchArgs.mIconId = diffEntry.getAppIconId(); launchArgs.mConsumedPower = (int) diffEntry.mConsumePower; - launchArgs.mForegroundTimeMs = diffEntry.mForegroundUsageTimeInMs; - launchArgs.mBackgroundTimeMs = diffEntry.mBackgroundUsageTimeInMs; + launchArgs.mForegroundTimeMs = + isValidToShowSummary ? diffEntry.mForegroundUsageTimeInMs : 0; + launchArgs.mBackgroundTimeMs = + isValidToShowSummary ? diffEntry.mBackgroundUsageTimeInMs : 0; launchArgs.mIsUserEntry = histEntry.isUserEntry(); startBatteryDetailPage(caller, fragment, launchArgs); } @@ -227,22 +238,17 @@ public class AdvancedPowerUsageDetail extends DashboardFragment implements public void onCreate(Bundle icicle) { super.onCreate(icicle); - mPackageName = getArguments().getString(EXTRA_PACKAGE_NAME); - mFooterPreference = findPreference(KEY_FOOTER_PREFERENCE); - mHeaderPreference = (LayoutPreference) findPreference(KEY_PREF_HEADER); + final String packageName = getArguments().getString(EXTRA_PACKAGE_NAME); + if (enableTriState) { + onCreateForTriState(packageName); + } else { + mForegroundPreference = findPreference(KEY_PREF_FOREGROUND); + mBackgroundPreference = findPreference(KEY_PREF_BACKGROUND); + } + mHeaderPreference = findPreference(KEY_PREF_HEADER); - mUnrestrictedPreference = findPreference(KEY_PREF_UNRESTRICTED); - mOptimizePreference = findPreference(KEY_PREF_OPTIMIZED); - mRestrictedPreference = findPreference(KEY_PREF_RESTRICTED); - mUnrestrictedPreference.setOnClickListener(this); - mOptimizePreference.setOnClickListener(this); - mRestrictedPreference.setOnClickListener(this); - - mBatteryOptimizeUtils = new BatteryOptimizeUtils( - getContext(), getArguments().getInt(EXTRA_UID), mPackageName); - - if (mPackageName != null) { - mAppEntry = mState.getEntry(mPackageName, UserHandle.myUserId()); + if (packageName != null) { + mAppEntry = mState.getEntry(packageName, UserHandle.myUserId()); } } @@ -251,7 +257,11 @@ public class AdvancedPowerUsageDetail extends DashboardFragment implements super.onResume(); initHeader(); - initPreference(); + if (enableTriState) { + initPreferenceForTriState(getContext()); + } else { + initPreference(getContext()); + } } @VisibleForTesting @@ -281,17 +291,39 @@ public class AdvancedPowerUsageDetail extends DashboardFragment implements controller.setIsInstantApp(AppUtils.isInstant(mAppEntry.info)); } - final long foregroundTimeMs = bundle.getLong(EXTRA_FOREGROUND_TIME); - final long backgroundTimeMs = bundle.getLong(EXTRA_BACKGROUND_TIME); - //TODO(b/178197718) Update layout to support multiple lines - controller.setSummary(getAppActiveTime(foregroundTimeMs, backgroundTimeMs)); + if (enableTriState) { + final long foregroundTimeMs = bundle.getLong(EXTRA_FOREGROUND_TIME); + final long backgroundTimeMs = bundle.getLong(EXTRA_BACKGROUND_TIME); + //TODO(b/178197718) Update layout to support multiple lines + controller.setSummary(getAppActiveTime(foregroundTimeMs, backgroundTimeMs)); + } controller.done(context, true /* rebindActions */); } @VisibleForTesting - void initPreference() { - final Context context = getContext(); + void initPreference(Context context) { + final Bundle bundle = getArguments(); + final long foregroundTimeMs = bundle.getLong(EXTRA_FOREGROUND_TIME); + final long backgroundTimeMs = bundle.getLong(EXTRA_BACKGROUND_TIME); + mForegroundPreference.setSummary( + TextUtils.expandTemplate(getText(R.string.battery_used_for), + StringUtil.formatElapsedTime( + context, + foregroundTimeMs, + /* withSeconds */ false, + /* collapseTimeUnit */ false))); + mBackgroundPreference.setSummary( + TextUtils.expandTemplate(getText(R.string.battery_active_for), + StringUtil.formatElapsedTime( + context, + backgroundTimeMs, + /* withSeconds */ false, + /* collapseTimeUnit */ false))); + } + + @VisibleForTesting + void initPreferenceForTriState(Context context) { final String stateString; final String footerString; //TODO(b/178197718) Update strings @@ -308,7 +340,6 @@ public class AdvancedPowerUsageDetail extends DashboardFragment implements } else { //Present default string to normal app. footerString = context.getString(R.string.manager_battery_usage_footer); - } mFooterPreference.setTitle(Html.fromHtml(footerString, Html.FROM_HTML_MODE_COMPACT)); } @@ -325,7 +356,7 @@ public class AdvancedPowerUsageDetail extends DashboardFragment implements @Override protected int getPreferenceScreenResId() { - return R.xml.power_usage_detail; + return enableTriState ? R.xml.power_usage_detail : R.xml.power_usage_detail_legacy; } @Override @@ -336,18 +367,20 @@ public class AdvancedPowerUsageDetail extends DashboardFragment implements final String packageName = bundle.getString(EXTRA_PACKAGE_NAME); mAppButtonsPreferenceController = new AppButtonsPreferenceController( - (SettingsActivity) getActivity(), this, getSettingsLifecycle(), packageName, mState, - REQUEST_UNINSTALL, REQUEST_REMOVE_DEVICE_ADMIN); + (SettingsActivity) getActivity(), this, getSettingsLifecycle(), packageName, + mState, REQUEST_UNINSTALL, REQUEST_REMOVE_DEVICE_ADMIN); controllers.add(mAppButtonsPreferenceController); - mUnrestrictedPreferenceController = - new UnrestrictedPreferenceController(context, uid, packageName); - mOptimizedPreferenceController = - new OptimizedPreferenceController(context, uid, packageName); - mRestrictedPreferenceController = - new RestrictedPreferenceController(context, uid, packageName); - controllers.add(mUnrestrictedPreferenceController); - controllers.add(mOptimizedPreferenceController); - controllers.add(mRestrictedPreferenceController); + if (enableTriState) { + controllers.add(new UnrestrictedPreferenceController(context, uid, packageName)); + controllers.add(new OptimizedPreferenceController(context, uid, packageName)); + controllers.add(new RestrictedPreferenceController(context, uid, packageName)); + } else { + mBackgroundActivityPreferenceController = new BackgroundActivityPreferenceController( + context, this, uid, packageName); + controllers.add(mBackgroundActivityPreferenceController); + controllers.add(new BatteryOptimizationPreferenceController( + (SettingsActivity) getActivity(), this, packageName)); + } return controllers; } @@ -367,6 +400,12 @@ public class AdvancedPowerUsageDetail extends DashboardFragment implements } } + @Override + public void onBatteryTipHandled(BatteryTip batteryTip) { + mBackgroundActivityPreferenceController.updateSummary( + findPreference(mBackgroundActivityPreferenceController.getPreferenceKey())); + } + @Override public void onRadioButtonClicked(RadioButtonPreference selected) { updatePreferenceState(mUnrestrictedPreference, selected.getKey()); @@ -378,6 +417,19 @@ public class AdvancedPowerUsageDetail extends DashboardFragment implements preference.setChecked(selectedKey.equals(preference.getKey())); } + private void onCreateForTriState(String packageName) { + mUnrestrictedPreference = findPreference(KEY_PREF_UNRESTRICTED); + mOptimizePreference = findPreference(KEY_PREF_OPTIMIZED); + mRestrictedPreference = findPreference(KEY_PREF_RESTRICTED); + mFooterPreference = findPreference(KEY_FOOTER_PREFERENCE); + mUnrestrictedPreference.setOnClickListener(this); + mOptimizePreference.setOnClickListener(this); + mRestrictedPreference.setOnClickListener(this); + + mBatteryOptimizeUtils = new BatteryOptimizeUtils( + getContext(), getArguments().getInt(EXTRA_UID), packageName); + } + //TODO(b/178197718) Update method to support time period private CharSequence getAppActiveTime(long foregroundTimeMs, long backgroundTimeMs) { final long totalTimeMs = foregroundTimeMs + backgroundTimeMs; diff --git a/src/com/android/settings/fuelgauge/BatteryChartPreferenceController.java b/src/com/android/settings/fuelgauge/BatteryChartPreferenceController.java index ff7447725fc..99e6b0db666 100644 --- a/src/com/android/settings/fuelgauge/BatteryChartPreferenceController.java +++ b/src/com/android/settings/fuelgauge/BatteryChartPreferenceController.java @@ -151,21 +151,22 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll final PowerGaugePreference powerPref = (PowerGaugePreference) preference; final BatteryDiffEntry diffEntry = powerPref.getBatteryDiffEntry(); final BatteryHistEntry histEntry = diffEntry.mBatteryHistEntry; + final String packageName = histEntry.mPackageName; // Checks whether the package is installed or not. boolean isValidPackage = true; if (histEntry.isAppEntry()) { if (mBatteryUtils == null) { mBatteryUtils = BatteryUtils.getInstance(mPrefContext); } - isValidPackage = mBatteryUtils.getPackageUid(histEntry.mPackageName) + isValidPackage = mBatteryUtils.getPackageUid(packageName) != BatteryUtils.UID_NULL; } Log.d(TAG, String.format("handleClick() label=%s key=%s isValid:%b %s", - diffEntry.getAppLabel(), histEntry.getKey(), isValidPackage, - histEntry.mPackageName)); + diffEntry.getAppLabel(), histEntry.getKey(), isValidPackage, packageName)); if (isValidPackage) { AdvancedPowerUsageDetail.startBatteryDetailPage( - mActivity, mFragment, diffEntry, powerPref.getPercent()); + mActivity, mFragment, diffEntry, powerPref.getPercent(), + isValidToShowSummary(packageName), getSlotInformation()); return true; } return false; @@ -267,8 +268,8 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll || (mTrapezoidIndex == trapezoidIndex && !isForce)) { return false; } - Log.d(TAG, String.format("refreshUi: index=%d batteryIndexedMap.size=%d", - mTrapezoidIndex, mBatteryIndexedMap.size())); + Log.d(TAG, String.format("refreshUi: index=%d size=%d isForce:%b", + trapezoidIndex, mBatteryIndexedMap.size(), isForce)); mTrapezoidIndex = trapezoidIndex; mHandler.post(() -> { @@ -471,11 +472,9 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll final long backgroundUsageTimeInMs = entry.mBackgroundUsageTimeInMs; final long totalUsageTimeInMs = foregroundUsageTimeInMs + backgroundUsageTimeInMs; // Checks whether the package is allowed to show summary or not. - for (CharSequence notAllowPackageName : mNotAllowShowSummaryPackages) { - if (TextUtils.equals(entry.getPackageName(), notAllowPackageName)) { - preference.setSummary(null); - return; - } + if (!isValidToShowSummary(entry.getPackageName())) { + preference.setSummary(null); + return; } String usageTimeSummary = null; // Not shows summary for some system components without usage time. @@ -515,6 +514,17 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll return mPrefContext.getString(resourceId, timeSequence); } + private boolean isValidToShowSummary(String packageName) { + if (mNotAllowShowSummaryPackages != null) { + for (CharSequence notAllowPackageName : mNotAllowShowSummaryPackages) { + if (TextUtils.equals(packageName, notAllowPackageName)) { + return false; + } + } + } + return true; + } + private static String utcToLocalTime(long[] timestamps) { final StringBuilder builder = new StringBuilder(); for (int index = 0; index < timestamps.length; index++) { diff --git a/src/com/android/settings/fuelgauge/BatteryChartView.java b/src/com/android/settings/fuelgauge/BatteryChartView.java index 366946e12e2..d56cbd45bec 100644 --- a/src/com/android/settings/fuelgauge/BatteryChartView.java +++ b/src/com/android/settings/fuelgauge/BatteryChartView.java @@ -22,11 +22,13 @@ import android.graphics.Color; import android.graphics.CornerPathEffect; import android.graphics.Paint; import android.graphics.Path; +import android.graphics.Rect; import android.util.AttributeSet; import android.util.Log; import android.view.HapticFeedbackConstants; import android.view.MotionEvent; import android.view.View; +import android.widget.TextView; import androidx.appcompat.widget.AppCompatImageView; @@ -38,6 +40,8 @@ import java.util.Locale; /** A widget component to draw chart graph. */ public class BatteryChartView extends AppCompatImageView implements View.OnClickListener { private static final String TAG = "BatteryChartView"; + // For drawing the percentage information. + private static final String[] PERCENTAGES = new String[] {"100%", "50%", "0%"}; private static final int DEFAULT_TRAPEZOID_COUNT = 12; /** Selects all trapezoid shapes. */ public static final int SELECTED_INDEX_ALL = -1; @@ -58,8 +62,14 @@ public class BatteryChartView extends AppCompatImageView implements View.OnClick private int mTrapezoidColor; private int mTrapezoidSolidColor; private final int mDividerColor = Color.parseColor("#CDCCC5"); + // For drawing the percentage information. + private int mTextPadding; + private final Rect mIndent = new Rect(); + private final Rect[] mPercentageBound = + new Rect[] {new Rect(), new Rect(), new Rect()}; private int[] mLevels; + private Paint mTextPaint; private Paint mDividerPaint; private Paint mTrapezoidPaint; private TrapezoidSlot[] mTrapezoidSlot; @@ -128,6 +138,37 @@ public class BatteryChartView extends AppCompatImageView implements View.OnClick mOnSelectListener = listener; } + /** Sets the companion {@link TextView} for percentage information. */ + public void setCompanionTextView(TextView textView) { + requestLayout(); + if (textView != null) { + // Pre-draws the view first to load style atttributions into paint. + textView.draw(new Canvas()); + mTextPaint = textView.getPaint(); + } else { + mTextPaint = null; + } + } + + @Override + public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + // Measures text bounds and updates indent configuration. + if (mTextPaint != null) { + for (int index = 0; index < PERCENTAGES.length; index++) { + mTextPaint.getTextBounds( + PERCENTAGES[index], 0, PERCENTAGES[index].length(), + mPercentageBound[index]); + } + // Updates the indent configurations. + mIndent.top = mPercentageBound[0].height(); + mIndent.right = mPercentageBound[0].width() + mTextPadding * 2; + Log.d(TAG, "setIndent:" + mPercentageBound[0]); + } else { + mIndent.set(0, 0, 0, 0); + } + } + @Override public void draw(Canvas canvas) { super.draw(canvas); @@ -194,32 +235,56 @@ public class BatteryChartView extends AppCompatImageView implements View.OnClick mTrapezoidPaint.setPathEffect( new CornerPathEffect( resources.getDimensionPixelSize(R.dimen.chartview_trapezoid_radius))); + // Initializes for drawing text information. + mTextPadding = resources.getDimensionPixelSize(R.dimen.chartview_text_padding); } private void drawHorizontalDividers(Canvas canvas) { + final int width = getWidth() - mIndent.right; + final int height = getHeight() - mIndent.top - mIndent.bottom; // Draws the top divider line for 100% curve. - float offsetY = mDividerWidth * 0.5f; - canvas.drawLine(0, offsetY, getWidth(), offsetY, mDividerPaint); + float offsetY = mIndent.top + mDividerWidth * .5f; + canvas.drawLine(0, offsetY, width, offsetY, mDividerPaint); + if (mTextPaint != null) { + canvas.drawText( + PERCENTAGES[0], + getWidth() - mPercentageBound[0].width(), + offsetY + mPercentageBound[0].height() *.5f , mTextPaint); + } // Draws the center divider line for 50% curve. final float availableSpace = - getHeight() - mDividerWidth * 2 - mTrapezoidVOffset - mDividerHeight; - offsetY = mDividerWidth + availableSpace * 0.5f; - canvas.drawLine(0, offsetY, getWidth(), offsetY, mDividerPaint); - // Draws the center divider line for 0% curve. - offsetY = getHeight() - mDividerHeight - mDividerWidth * 0.5f; - canvas.drawLine(0, offsetY, getWidth(), offsetY, mDividerPaint); + height - mDividerWidth * 2 - mTrapezoidVOffset - mDividerHeight; + offsetY = mIndent.top + mDividerWidth + availableSpace * .5f; + canvas.drawLine(0, offsetY, width, offsetY, mDividerPaint); + if (mTextPaint != null) { + canvas.drawText( + PERCENTAGES[1], + getWidth() - mPercentageBound[1].width(), + offsetY + mPercentageBound[1].height() *.5f , mTextPaint); + } + // Draws the bottom divider line for 0% curve. + offsetY = mIndent.top + (height - mDividerHeight - mDividerWidth * .5f); + canvas.drawLine(0, offsetY, width, offsetY, mDividerPaint); + if (mTextPaint != null) { + canvas.drawText( + PERCENTAGES[2], + getWidth() - mPercentageBound[2].width(), + offsetY + mPercentageBound[2].height() *.5f , mTextPaint); + } } private void drawVerticalDividers(Canvas canvas) { + final int width = getWidth() - mIndent.right; final int dividerCount = mTrapezoidCount + 1; final float dividerSpace = dividerCount * mDividerWidth; - final float unitWidth = (getWidth() - dividerSpace) / (float) mTrapezoidCount; - final float startY = getHeight() - mDividerHeight; - final float trapezoidSlotOffset = mTrapezoidHOffset + mDividerWidth * 0.5f; + final float unitWidth = (width - dividerSpace) / (float) mTrapezoidCount; + final float bottomY = getHeight() - mIndent.bottom; + final float startY = bottomY - mDividerHeight; + final float trapezoidSlotOffset = mTrapezoidHOffset + mDividerWidth * .5f; // Draws each vertical dividers. - float startX = mDividerWidth * 0.5f; + float startX = mDividerWidth * .5f; for (int index = 0; index < dividerCount; index++) { - canvas.drawLine(startX, startY, startX, getHeight(), mDividerPaint); + canvas.drawLine(startX, startY, startX, bottomY, mDividerPaint); final float nextX = startX + mDividerWidth + unitWidth; // Updates the trapezoid slots for drawing. if (index < mTrapezoidSlot.length) { @@ -236,8 +301,9 @@ public class BatteryChartView extends AppCompatImageView implements View.OnClick return; } final float trapezoidBottom = - getHeight() - mDividerHeight - mDividerWidth - mTrapezoidVOffset; - final float availableSpace = trapezoidBottom - mDividerWidth; + getHeight() - mIndent.bottom - mDividerHeight - mDividerWidth + - mTrapezoidVOffset; + final float availableSpace = trapezoidBottom - mDividerWidth * .5f - mIndent.top; final float unitHeight = availableSpace / 100f; // Draws all trapezoid shapes into the canvas. final Path trapezoidPath = new Path(); @@ -249,8 +315,8 @@ public class BatteryChartView extends AppCompatImageView implements View.OnClick // Configures the trapezoid paint color. mTrapezoidPaint.setColor( mSelectedIndex == index || mSelectedIndex == SELECTED_INDEX_ALL - ? mTrapezoidSolidColor - : mTrapezoidColor); + ? mTrapezoidSolidColor + : mTrapezoidColor); final float leftTop = round(trapezoidBottom - mLevels[index] * unitHeight); final float rightTop = round(trapezoidBottom - mLevels[index + 1] * unitHeight); trapezoidPath.reset(); diff --git a/src/com/android/settings/fuelgauge/BatteryHistoryPreference.java b/src/com/android/settings/fuelgauge/BatteryHistoryPreference.java index 94e35cb3966..b2818bf601d 100644 --- a/src/com/android/settings/fuelgauge/BatteryHistoryPreference.java +++ b/src/com/android/settings/fuelgauge/BatteryHistoryPreference.java @@ -100,6 +100,8 @@ public class BatteryHistoryPreference extends Preference { } if (mIsChartGraphEnabled) { mBatteryChartView = (BatteryChartView) view.findViewById(R.id.battery_chart); + mBatteryChartView.setCompanionTextView( + (TextView) view.findViewById(R.id.companion_text)); if (mChartPreferenceController != null) { mChartPreferenceController.setBatteryChartView(mBatteryChartView); } diff --git a/src/com/android/settings/fuelgauge/ExpandDividerPreference.java b/src/com/android/settings/fuelgauge/ExpandDividerPreference.java index 40536eb37c7..b89f2d42acb 100644 --- a/src/com/android/settings/fuelgauge/ExpandDividerPreference.java +++ b/src/com/android/settings/fuelgauge/ExpandDividerPreference.java @@ -38,6 +38,7 @@ public class ExpandDividerPreference extends Preference { private OnExpandListener mOnExpandListener; private boolean mIsExpanded = false; + private String mTitleContent = null; /** A callback listener for expand state is changed by users. */ public interface OnExpandListener { @@ -72,6 +73,7 @@ public class ExpandDividerPreference extends Preference { } void setTitle(final String titleContent) { + mTitleContent = titleContent; if (mTextView != null) { mTextView.postDelayed( () -> mTextView.setText(titleContent), 50); @@ -95,5 +97,6 @@ public class ExpandDividerPreference extends Preference { if (mImageView != null) { mImageView.setImageResource(iconId); } + setTitle(mTitleContent); } } diff --git a/src/com/android/settings/localepicker/LocaleRecyclerView.java b/src/com/android/settings/localepicker/LocaleRecyclerView.java index e82874dab70..d32a735d48c 100644 --- a/src/com/android/settings/localepicker/LocaleRecyclerView.java +++ b/src/com/android/settings/localepicker/LocaleRecyclerView.java @@ -37,7 +37,7 @@ class LocaleRecyclerView extends RecyclerView { @Override public boolean onTouchEvent(MotionEvent e) { - if (e.getAction() == MotionEvent.ACTION_UP) { + if (e.getAction() == MotionEvent.ACTION_UP || e.getAction() == MotionEvent.ACTION_CANCEL) { LocaleDragAndDropAdapter adapter = (LocaleDragAndDropAdapter) this.getAdapter(); if (adapter != null) { adapter.doTheUpdate(); diff --git a/src/com/android/settings/widget/RadioButtonPickerFragment.java b/src/com/android/settings/widget/RadioButtonPickerFragment.java index 0f4fbc3433b..7d765df60fc 100644 --- a/src/com/android/settings/widget/RadioButtonPickerFragment.java +++ b/src/com/android/settings/widget/RadioButtonPickerFragment.java @@ -205,7 +205,7 @@ public abstract class RadioButtonPickerFragment extends InstrumentedPreferenceFr public RadioButtonPreference bindPreference(RadioButtonPreference pref, String key, CandidateInfo info, String defaultKey) { pref.setTitle(info.loadLabel()); - Utils.setSafeIcon(pref, info.loadIcon()); + pref.setIcon(Utils.getSafeIcon(info.loadIcon())); pref.setKey(key); if (TextUtils.equals(defaultKey, key)) { pref.setChecked(true); diff --git a/src/com/android/settings/wifi/WifiConfigController2.java b/src/com/android/settings/wifi/WifiConfigController2.java index 9f3b1a711d9..1e0670788ad 100644 --- a/src/com/android/settings/wifi/WifiConfigController2.java +++ b/src/com/android/settings/wifi/WifiConfigController2.java @@ -1339,12 +1339,13 @@ public class WifiConfigController2 implements TextWatcher, mIpAddressView = (TextView) mView.findViewById(R.id.ipaddress); mIpAddressView.addTextChangedListener(this); mGatewayView = (TextView) mView.findViewById(R.id.gateway); - mGatewayView.addTextChangedListener(this); + mGatewayView.addTextChangedListener(getIpConfigFieldsTextWatcher(mGatewayView)); mNetworkPrefixLengthView = (TextView) mView.findViewById( R.id.network_prefix_length); - mNetworkPrefixLengthView.addTextChangedListener(this); + mNetworkPrefixLengthView.addTextChangedListener( + getIpConfigFieldsTextWatcher(mNetworkPrefixLengthView)); mDns1View = (TextView) mView.findViewById(R.id.dns1); - mDns1View.addTextChangedListener(this); + mDns1View.addTextChangedListener(getIpConfigFieldsTextWatcher(mDns1View)); mDns2View = (TextView) mView.findViewById(R.id.dns2); mDns2View.addTextChangedListener(this); } @@ -1562,6 +1563,47 @@ public class WifiConfigController2 implements TextWatcher, // work done in afterTextChanged } + /* TODO: Add more test cases for this TextWatcher b/186368002 + * This TextWatcher is for IP config fields (Gateway/Network Prefix Length/DNS1) to prevent + * to rewrite the value in these columns that the user wanted to change after they saved. + * When afterTextChanged we will check the text is empty or not then set the Hint for user. + */ + private TextWatcher getIpConfigFieldsTextWatcher(final TextView view) { + return new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + // work done in afterTextChanged + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + // work done in afterTextChanged + } + + @Override + public void afterTextChanged(Editable s) { + if (s.length() == 0) { + if (view.getId() == R.id.gateway) { + mGatewayView.setHint(R.string.wifi_gateway_hint); + } else if (view.getId() == R.id.network_prefix_length) { + mNetworkPrefixLengthView.setHint(R.string.wifi_network_prefix_length_hint); + } else if (view.getId() == R.id.dns1) { + mDns1View.setHint(R.string.wifi_dns1_hint); + } + Button submit = mConfigUi.getSubmitButton(); + if (submit == null) return; + + submit.setEnabled(false); + } else { + ThreadUtils.postOnMainThread(() -> { + showWarningMessagesIfAppropriate(); + enableSubmitIfAppropriate(); + }); + } + } + }; + } + @Override public boolean onEditorAction(TextView textView, int id, KeyEvent keyEvent) { if (textView == mPasswordView) { diff --git a/src/com/android/settings/wifi/dpp/WifiDppConfiguratorActivity.java b/src/com/android/settings/wifi/dpp/WifiDppConfiguratorActivity.java index 6659c7ace91..ecaf9ee8fc4 100644 --- a/src/com/android/settings/wifi/dpp/WifiDppConfiguratorActivity.java +++ b/src/com/android/settings/wifi/dpp/WifiDppConfiguratorActivity.java @@ -144,31 +144,8 @@ public class WifiDppConfiguratorActivity extends WifiDppBaseActivity implements } break; case Settings.ACTION_PROCESS_WIFI_EASY_CONNECT_URI: - final Uri uri = intent.getData(); - final String uriString = (uri == null) ? null : uri.toString(); - mWifiDppQrCode = WifiQrCode.getValidWifiDppQrCodeOrNull(uriString); - mWifiDppRemoteBandSupport = intent.getIntArrayExtra( - Settings.EXTRA_EASY_CONNECT_BAND_LIST); // returns null if none - final boolean isDppSupported = WifiDppUtils.isWifiDppEnabled(this); - if (!isDppSupported) { - Log.e(TAG, - "ACTION_PROCESS_WIFI_EASY_CONNECT_URI for a device that doesn't " - + "support Wifi DPP - use WifiManager#isEasyConnectSupported"); - } - if (mWifiDppQrCode == null) { - Log.e(TAG, "ACTION_PROCESS_WIFI_EASY_CONNECT_URI with null URI!"); - } - if (mWifiDppQrCode == null || !isDppSupported) { - cancelActivity = true; - } else { - final WifiNetworkConfig connectedConfig = getConnectedWifiNetworkConfigOrNull(); - if (connectedConfig == null || !connectedConfig.isSupportWifiDpp(this)) { - showChooseSavedWifiNetworkFragment(/* addToBackStack */ false); - } else { - mWifiNetworkConfig = connectedConfig; - showAddDeviceFragment(/* addToBackStack */ false); - } - } + WifiDppUtils.showLockScreen(this, + () -> handleActionProcessWifiEasyConnectUriIntent(intent)); break; default: cancelActivity = true; @@ -180,6 +157,34 @@ public class WifiDppConfiguratorActivity extends WifiDppBaseActivity implements } } + private void handleActionProcessWifiEasyConnectUriIntent(Intent intent) { + final Uri uri = intent.getData(); + final String uriString = (uri == null) ? null : uri.toString(); + mWifiDppQrCode = WifiQrCode.getValidWifiDppQrCodeOrNull(uriString); + mWifiDppRemoteBandSupport = intent.getIntArrayExtra( + Settings.EXTRA_EASY_CONNECT_BAND_LIST); // returns null if none + final boolean isDppSupported = WifiDppUtils.isWifiDppEnabled(this); + if (!isDppSupported) { + Log.e(TAG, + "ACTION_PROCESS_WIFI_EASY_CONNECT_URI for a device that doesn't " + + "support Wifi DPP - use WifiManager#isEasyConnectSupported"); + } + if (mWifiDppQrCode == null) { + Log.e(TAG, "ACTION_PROCESS_WIFI_EASY_CONNECT_URI with null URI!"); + } + if (mWifiDppQrCode == null || !isDppSupported) { + finish(); + } else { + final WifiNetworkConfig connectedConfig = getConnectedWifiNetworkConfigOrNull(); + if (connectedConfig == null || !connectedConfig.isSupportWifiDpp(this)) { + showChooseSavedWifiNetworkFragment(/* addToBackStack */ false); + } else { + mWifiNetworkConfig = connectedConfig; + showAddDeviceFragment(/* addToBackStack */ false); + } + } + } + private void showQrCodeScannerFragment() { WifiDppQrCodeScannerFragment fragment = (WifiDppQrCodeScannerFragment) mFragmentManager.findFragmentByTag( diff --git a/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsCompanionAppsControllerTest.java b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsCompanionAppsControllerTest.java new file mode 100644 index 00000000000..3f49938412b --- /dev/null +++ b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsCompanionAppsControllerTest.java @@ -0,0 +1,178 @@ +/* + * Copyright (C) 2021 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.settings.bluetooth; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +import android.companion.Association; +import android.companion.CompanionDeviceManager; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; + +import androidx.preference.Preference; +import androidx.preference.PreferenceCategory; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.robolectric.RobolectricTestRunner; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +@RunWith(RobolectricTestRunner.class) +public class BluetoothDetailsCompanionAppsControllerTest extends + BluetoothDetailsControllerTestBase { + private static final String PACKAGE_NAME_ONE = "com.google.android.deskclock"; + private static final String PACKAGE_NAME_TWO = "com.google.android.calculator"; + private static final String PACKAGE_NAME_THREE = "com.google.android.GoogleCamera"; + private static final CharSequence APP_NAME_ONE = "deskclock"; + private static final CharSequence APP_NAME_TWO = "calculator"; + private static final CharSequence APP_NAME_THREE = "GoogleCamera"; + + @Mock + private CompanionDeviceManager mCompanionDeviceManager; + @Mock + private PackageManager mPackageManager; + + private BluetoothDetailsCompanionAppsController mController; + private PreferenceCategory mProfiles; + private List mPackages; + private List mAppNames; + private List mAssociations; + + + @Override + public void setUp() { + super.setUp(); + mPackages = Arrays.asList(PACKAGE_NAME_ONE, PACKAGE_NAME_TWO, PACKAGE_NAME_THREE); + mAppNames = Arrays.asList(APP_NAME_ONE, APP_NAME_TWO, APP_NAME_THREE); + mProfiles = spy(new PreferenceCategory(mContext)); + mAssociations = new ArrayList<>(); + when(mCompanionDeviceManager.getAllAssociations()).thenReturn(mAssociations); + when(mProfiles.getPreferenceManager()).thenReturn(mPreferenceManager); + setupDevice(mDeviceConfig); + mController = + new BluetoothDetailsCompanionAppsController(mContext, mFragment, mCachedDevice, + mLifecycle); + mController.mCompanionDeviceManager = mCompanionDeviceManager; + mController.mPackageManager = mPackageManager; + mController.mProfilesContainer = mProfiles; + mProfiles.setKey(mController.getPreferenceKey()); + mScreen.addPreference(mProfiles); + } + + private void setupFakeLabelAndInfo(String packageName, CharSequence appName) { + ApplicationInfo appInfo = mock(ApplicationInfo.class); + try { + when(mPackageManager.getApplicationInfo(packageName, 0)).thenReturn(appInfo); + when(mPackageManager.getApplicationLabel(appInfo)).thenReturn(appName); + } catch (PackageManager.NameNotFoundException e) { + throw new RuntimeException(e); + } + } + + private void addFakeAssociation(String packageName, CharSequence appName) { + setupFakeLabelAndInfo(packageName, appName); + Association association = new Association( + 0, mCachedDevice.getAddress(), packageName, "", true, System.currentTimeMillis()); + mAssociations.add(association); + showScreen(mController); + } + + private Preference getPreference(int index) { + PreferenceCategory preferenceCategory = mProfiles.findPreference( + mController.getPreferenceKey()); + return Objects.requireNonNull(preferenceCategory).getPreference(index); + } + + private void removeAssociation(String packageName) { + mAssociations = mAssociations.stream() + .filter(a -> !a.getPackageName().equals(packageName)) + .collect(Collectors.toList()); + + when(mCompanionDeviceManager.getAllAssociations()).thenReturn(mAssociations); + + showScreen(mController); + } + + @Test + public void addOneAssociation_preferenceShouldBeAdded() { + addFakeAssociation(PACKAGE_NAME_ONE, APP_NAME_ONE); + + Preference preferenceOne = getPreference(0); + + assertThat(preferenceOne.getClass()).isEqualTo(CompanionAppWidgetPreference.class); + assertThat(preferenceOne.getKey()).isEqualTo(PACKAGE_NAME_ONE); + assertThat(preferenceOne.getTitle()).isEqualTo(APP_NAME_ONE.toString()); + assertThat(mProfiles.getPreferenceCount()).isEqualTo(1); + + removeAssociation(PACKAGE_NAME_ONE); + + assertThat(mProfiles.getPreferenceCount()).isEqualTo(0); + } + + @Test + public void removeOneAssociation_preferenceShouldBeRemoved() { + addFakeAssociation(PACKAGE_NAME_ONE, APP_NAME_ONE); + + removeAssociation(PACKAGE_NAME_ONE); + + assertThat(mProfiles.getPreferenceCount()).isEqualTo(0); + } + + @Test + public void addMultipleAssociations_preferencesShouldBeAdded() { + for (int i = 0; i < mPackages.size(); i++) { + addFakeAssociation(mPackages.get(i), mAppNames.get(i)); + + Preference preference = getPreference(i); + + assertThat(preference.getClass()).isEqualTo(CompanionAppWidgetPreference.class); + assertThat(preference.getKey()).isEqualTo(mPackages.get(i)); + assertThat(preference.getTitle()).isEqualTo(mAppNames.get(i).toString()); + assertThat(mProfiles.getPreferenceCount()).isEqualTo(i + 1); + } + } + + @Test + public void removeMultipleAssociations_preferencesShouldBeRemoved() { + for (int i = 0; i < mPackages.size(); i++) { + addFakeAssociation(mPackages.get(i), mAppNames.get(i).toString()); + } + + for (int i = 0; i < mPackages.size(); i++) { + removeAssociation(mPackages.get(i)); + + assertThat(mProfiles.getPreferenceCount()).isEqualTo(mPackages.size() - i - 1); + + if (i == mPackages.size() - 1) { + break; + } + + assertThat(getPreference(0).getKey()).isEqualTo(mPackages.get(i + 1)); + assertThat(getPreference(0).getTitle()).isEqualTo(mAppNames.get(i + 1).toString()); + } + } +} diff --git a/tests/robotests/src/com/android/settings/bluetooth/CompanionAppWidgetPreferenceTest.java b/tests/robotests/src/com/android/settings/bluetooth/CompanionAppWidgetPreferenceTest.java new file mode 100644 index 00000000000..8ee3ef78d81 --- /dev/null +++ b/tests/robotests/src/com/android/settings/bluetooth/CompanionAppWidgetPreferenceTest.java @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2021 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.settings.bluetooth; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; + +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.view.ContextThemeWrapper; +import android.view.View; + +import com.android.settings.R; +import com.android.settings.testutils.shadow.ShadowAlertDialogCompat; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; + +import java.util.ArrayList; +import java.util.List; + +@RunWith(RobolectricTestRunner.class) +@Config(shadows = {ShadowAlertDialogCompat.class}) +public class CompanionAppWidgetPreferenceTest { + private static final String TITLE_ONE = "Test Title 1"; + private static final String TITLE_TWO = "Test Title 1"; + private static final String KEY_ONE = "Test Key 1"; + private static final String KEY_TWO = "Test Key 1"; + + private Context mContext; + private Drawable mWidgetIconOne; + private Drawable mWidgetIconTwo; + private Drawable mAppIconOne; + private Drawable mAppIconTwo; + + @Mock + private View.OnClickListener mWidgetListenerOne; + @Mock + private View.OnClickListener mWidgetListenerTwo; + + private List mPreferenceContainer; + + @Before + public void setUp() { + mPreferenceContainer = new ArrayList<>(); + Context context = spy(RuntimeEnvironment.application.getApplicationContext()); + mContext = new ContextThemeWrapper(context, R.style.Theme_Settings); + mWidgetIconOne = mock(Drawable.class); + mAppIconOne = mock(Drawable.class); + mWidgetListenerOne = mock(View.OnClickListener.class); + mWidgetIconTwo = mock(Drawable.class); + mAppIconTwo = mock(Drawable.class); + mWidgetListenerTwo = mock(View.OnClickListener.class); + } + + private void setUpPreferenceContainer(Drawable widgetIcon, Drawable appIcon, + View.OnClickListener listener, String title, String key) { + CompanionAppWidgetPreference preference = new CompanionAppWidgetPreference( + widgetIcon, listener, mContext); + preference.setIcon(appIcon); + preference.setTitle(title); + preference.setKey(key); + mPreferenceContainer.add(preference); + } + + @Test + public void setUpPreferenceContainer_preferenceShouldBeAdded() { + setUpPreferenceContainer( + mWidgetIconOne, mAppIconOne, mWidgetListenerOne, TITLE_ONE, KEY_ONE); + + assertThat(mPreferenceContainer.get(0).getIcon()).isEqualTo(mAppIconOne); + assertThat(mPreferenceContainer.get(0).getKey()).isEqualTo(KEY_ONE); + assertThat(mPreferenceContainer.get(0).getTitle()).isEqualTo(TITLE_ONE); + + setUpPreferenceContainer( + mWidgetIconTwo, mAppIconTwo, mWidgetListenerTwo, TITLE_TWO, KEY_TWO); + + assertThat(mPreferenceContainer.get(1).getIcon()).isEqualTo(mAppIconTwo); + assertThat(mPreferenceContainer.get(1).getKey()).isEqualTo(KEY_TWO); + assertThat(mPreferenceContainer.get(1).getTitle()).isEqualTo(TITLE_TWO); + } +} diff --git a/tests/robotests/src/com/android/settings/fuelgauge/AdvancedPowerUsageDetailTest.java b/tests/robotests/src/com/android/settings/fuelgauge/AdvancedPowerUsageDetailTest.java index dd10a9e1556..5185593e8b7 100644 --- a/tests/robotests/src/com/android/settings/fuelgauge/AdvancedPowerUsageDetailTest.java +++ b/tests/robotests/src/com/android/settings/fuelgauge/AdvancedPowerUsageDetailTest.java @@ -47,6 +47,7 @@ import androidx.loader.app.LoaderManager; import androidx.preference.Preference; import androidx.recyclerview.widget.RecyclerView; +import com.android.settings.R; import com.android.settings.SettingsActivity; import com.android.settings.testutils.FakeFeatureFactory; import com.android.settings.testutils.shadow.ShadowActivityManager; @@ -82,6 +83,7 @@ public class AdvancedPowerUsageDetailTest { private static final String USAGE_PERCENT = "16%"; private static final int ICON_ID = 123; private static final int UID = 1; + private static final int POWER_MAH = 150; private static final long BACKGROUND_TIME_MS = 100; private static final long FOREGROUND_ACTIVITY_TIME_MS = 123; private static final long FOREGROUND_SERVICE_TIME_MS = 444; @@ -117,6 +119,8 @@ public class AdvancedPowerUsageDetailTest { @Mock private BatteryOptimizeUtils mBatteryOptimizeUtils; private Context mContext; + private Preference mForegroundPreference; + private Preference mBackgroundPreference; private Preference mFooterPreference; private RadioButtonPreference mRestrictedPreference; private RadioButtonPreference mOptimizePreference; @@ -166,6 +170,7 @@ public class AdvancedPowerUsageDetailTest { mFragment.mHeaderPreference = mHeaderPreference; mFragment.mState = mState; + mFragment.enableTriState = true; mFragment.mBatteryUtils = new BatteryUtils(RuntimeEnvironment.application); mFragment.mBatteryOptimizeUtils = mBatteryOptimizeUtils; mAppEntry.info = mock(ApplicationInfo.class); @@ -190,10 +195,14 @@ public class AdvancedPowerUsageDetailTest { nullable(UserHandle.class)); doAnswer(callable).when(mActivity).startActivity(captor.capture()); + mForegroundPreference = new Preference(mContext); + mBackgroundPreference = new Preference(mContext); mFooterPreference = new Preference(mContext); mRestrictedPreference = new RadioButtonPreference(mContext); mOptimizePreference = new RadioButtonPreference(mContext); mUnrestrictedPreference = new RadioButtonPreference(mContext); + mFragment.mForegroundPreference = mForegroundPreference; + mFragment.mBackgroundPreference = mBackgroundPreference; mFragment.mFooterPreference = mFooterPreference; mFragment.mRestrictedPreference = mRestrictedPreference; mFragment.mOptimizePreference = mOptimizePreference; @@ -205,6 +214,17 @@ public class AdvancedPowerUsageDetailTest { ShadowEntityHeaderController.reset(); } + @Test + public void testGetPreferenceScreenResId_returnNewLayout() { + assertThat(mFragment.getPreferenceScreenResId()).isEqualTo(R.xml.power_usage_detail); + } + + @Test + public void testGetPreferenceScreenResId_disableTriState_returnLegacyLayout() { + mFragment.enableTriState = false; + assertThat(mFragment.getPreferenceScreenResId()).isEqualTo(R.xml.power_usage_detail_legacy); + } + @Test public void testInitHeader_NoAppEntry_BuildByBundle() { mFragment.mAppEntry = null; @@ -455,32 +475,52 @@ public class AdvancedPowerUsageDetailTest { } @Test - public void testInitPreference_isValidPackageName_hasCorrectString() { + public void testInitPreference_hasCorrectSummary() { + Bundle bundle = new Bundle(4); + bundle.putLong(AdvancedPowerUsageDetail.EXTRA_BACKGROUND_TIME, BACKGROUND_TIME_MS); + bundle.putLong(AdvancedPowerUsageDetail.EXTRA_FOREGROUND_TIME, FOREGROUND_TIME_MS); + bundle.putString(AdvancedPowerUsageDetail.EXTRA_POWER_USAGE_PERCENT, USAGE_PERCENT); + bundle.putInt(AdvancedPowerUsageDetail.EXTRA_POWER_USAGE_AMOUNT, POWER_MAH); + when(mFragment.getArguments()).thenReturn(bundle); + + doReturn(mContext.getText(R.string.battery_used_for)).when(mFragment).getText( + R.string.battery_used_for); + doReturn(mContext.getText(R.string.battery_active_for)).when(mFragment).getText( + R.string.battery_active_for); + + mFragment.initPreference(mContext); + + assertThat(mForegroundPreference.getSummary().toString()).isEqualTo("Used for 0 min"); + assertThat(mBackgroundPreference.getSummary().toString()).isEqualTo("Active for 0 min"); + } + + @Test + public void testInitPreferenceForTriState_isValidPackageName_hasCorrectString() { when(mBatteryOptimizeUtils.isValidPackageName()).thenReturn(false); - mFragment.initPreference(); + mFragment.initPreferenceForTriState(mContext); assertThat(mFooterPreference.getTitle().toString()) .isEqualTo("This app requires Optimized battery usage."); } @Test - public void testInitPreference_isSystemOrDefaultApp_hasCorrectString() { + public void testInitPreferenceForTriState_isSystemOrDefaultApp_hasCorrectString() { when(mBatteryOptimizeUtils.isValidPackageName()).thenReturn(true); when(mBatteryOptimizeUtils.isSystemOrDefaultApp()).thenReturn(true); - mFragment.initPreference(); + mFragment.initPreferenceForTriState(mContext); assertThat(mFooterPreference.getTitle() .toString()).isEqualTo("This app requires Unrestricted battery usage."); } @Test - public void testInitPreference_hasCorrectString() { + public void testInitPreferenceForTriState_hasCorrectString() { when(mBatteryOptimizeUtils.isValidPackageName()).thenReturn(true); when(mBatteryOptimizeUtils.isSystemOrDefaultApp()).thenReturn(false); - mFragment.initPreference(); + mFragment.initPreferenceForTriState(mContext); assertThat(mFooterPreference.getTitle().toString()) .isEqualTo("Changing how an app uses your battery can affect its performance.");