Make flags UI available on release build of launcher

The UI will only be shown on eng/userdebug platform builds.

Bug: 117223984
Change-Id: I27843f2d856a4a19f3fe53c4d306606eaa5714a2
This commit is contained in:
Ryan Lothian 2018-10-12 14:14:16 -04:00
parent cf7715511e
commit fa530cd23f
15 changed files with 338 additions and 43 deletions

View File

@ -22,14 +22,10 @@ import android.content.Context;
* Defines a set of flags used to control various launcher behaviors
*/
public final class FeatureFlags extends BaseFlags {
private static FeatureFlags instance = new FeatureFlags();
public static FeatureFlags getInstance(Context context) {
return instance;
private FeatureFlags() {
// Prevent instantiation
}
private FeatureFlags() {}
// Features to control Launcher3Go behavior
public static final boolean GO_DISABLE_WIDGETS = true;
public static final boolean LAUNCHER3_SPRING_ICONS = false;

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
/*
* 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.
*/
-->
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
android:key="feature_flags"
android:persistent="false">
</PreferenceScreen>

View File

@ -52,4 +52,10 @@
android:defaultValue=""
android:persistent="false" />
<PreferenceScreen
android:fragment="com.android.launcher3.config.FlagTogglerPreferenceFragment"
android:key="flag_toggler"
android:persistent="false"
android:title="Feature flags"/>
</PreferenceScreen>

View File

@ -1753,12 +1753,12 @@ public class Launcher extends BaseDraggingActivity implements LauncherExterns,
@Override
public void bindScreens(IntArray orderedScreenIds) {
// Make sure the first screen is always at the start.
if (FeatureFlags.getInstance(this).isQsbOnFirstScreenEnabled() &&
if (FeatureFlags.QSB_ON_FIRST_SCREEN.get() &&
orderedScreenIds.indexOf(Workspace.FIRST_SCREEN_ID) != 0) {
orderedScreenIds.removeValue(Workspace.FIRST_SCREEN_ID);
orderedScreenIds.add(0, Workspace.FIRST_SCREEN_ID);
LauncherModel.updateWorkspaceScreenOrder(this, orderedScreenIds);
} else if (!FeatureFlags.getInstance(this).isQsbOnFirstScreenEnabled()
} else if (!FeatureFlags.QSB_ON_FIRST_SCREEN.get()
&& orderedScreenIds.isEmpty()) {
// If there are no screens, we need to have an empty screen
mWorkspace.addExtraEmptyScreen();
@ -1775,8 +1775,7 @@ public class Launcher extends BaseDraggingActivity implements LauncherExterns,
int count = orderedScreenIds.size();
for (int i = 0; i < count; i++) {
int screenId = orderedScreenIds.get(i);
if (!FeatureFlags.getInstance(this).isQsbOnFirstScreenEnabled()
|| screenId != Workspace.FIRST_SCREEN_ID) {
if (!FeatureFlags.QSB_ON_FIRST_SCREEN.get() || screenId != Workspace.FIRST_SCREEN_ID) {
// No need to bind the first screen, as its always bound.
mWorkspace.insertNewWorkspaceScreenBeforeEmptyScreen(screenId);
}

View File

@ -70,9 +70,6 @@ import java.io.PrintWriter;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashSet;
public class LauncherProvider extends ContentProvider {
private static final String TAG = "LauncherProvider";
@ -793,7 +790,7 @@ public class LauncherProvider extends ContentProvider {
convertShortcutsToLauncherActivities(db);
case 26:
// QSB was moved to the grid. Clear the first row on screen 0.
if (FeatureFlags.getInstance(mContext).isQsbOnFirstScreenEnabled() &&
if (FeatureFlags.QSB_ON_FIRST_SCREEN.get() &&
!LauncherDbUtils.prepareScreenZeroToHostQsb(mContext, db)) {
break;
}

View File

@ -18,6 +18,7 @@ package com.android.launcher3;
import android.content.Context;
import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.graphics.IconShapeOverride;
import com.android.launcher3.logging.FileLog;
import com.android.launcher3.util.ResourceBasedOverride;
@ -35,6 +36,7 @@ public class MainProcessInitializer implements ResourceBasedOverride {
protected void init(Context context) {
FileLog.setDir(context.getApplicationContext().getFilesDir());
FeatureFlags.initialize(context);
IconShapeOverride.apply(context);
SessionCommitReceiver.applyDefaultUserPrefs(context);
}

View File

@ -43,6 +43,7 @@ import android.view.View;
import android.widget.Adapter;
import android.widget.ListView;
import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.graphics.IconShapeOverride;
import com.android.launcher3.notification.NotificationListener;
import com.android.launcher3.util.ListViewHighlighter;
@ -57,6 +58,8 @@ import java.util.Objects;
public class SettingsActivity extends Activity
implements PreferenceFragment.OnPreferenceStartFragmentCallback {
private static final String FLAGS_PREFERENCE_KEY = "flag_toggler";
private static final String ICON_BADGING_PREFERENCE_KEY = "pref_icon_badging";
/** Hidden field Settings.Secure.NOTIFICATION_BADGING */
public static final String NOTIFICATION_BADGING = "notification_badging";
@ -126,6 +129,12 @@ public class SettingsActivity extends Activity
getPreferenceManager().setSharedPreferencesName(LauncherFiles.SHARED_PREFERENCES_KEY);
addPreferencesFromResource(R.xml.launcher_preferences);
// Only show flag toggler UI if this build variant implements that.
Preference flagToggler = findPreference(FLAGS_PREFERENCE_KEY);
if (flagToggler != null && !FeatureFlags.showFlagTogglerUi()) {
getPreferenceScreen().removePreference(flagToggler);
}
ContentResolver resolver = getActivity().getContentResolver();
ButtonPreference iconBadgingPref =

View File

@ -480,7 +480,7 @@ public class Workspace extends PagedView<WorkspacePageIndicator>
* @param qsb an existing qsb to recycle or null.
*/
public void bindAndInitFirstWorkspaceScreen(View qsb) {
if (!FeatureFlags.getInstance(getContext()).isQsbOnFirstScreenEnabled()) {
if (!FeatureFlags.QSB_ON_FIRST_SCREEN.get()) {
return;
}
// Add the first page
@ -779,9 +779,7 @@ public class Workspace extends PagedView<WorkspacePageIndicator>
int id = mWorkspaceScreens.keyAt(i);
CellLayout cl = mWorkspaceScreens.valueAt(i);
// FIRST_SCREEN_ID can never be removed.
boolean qsbFirstScreenEnabled =
FeatureFlags.getInstance(getContext()).isQsbOnFirstScreenEnabled();
if ((!qsbFirstScreenEnabled || id > FIRST_SCREEN_ID)
if ((!FeatureFlags.QSB_ON_FIRST_SCREEN.get() || id > FIRST_SCREEN_ID)
&& cl.getShortcutsAndWidgets().getChildCount() == 0) {
removeScreens.add(id);
}

View File

@ -16,6 +16,21 @@
package com.android.launcher3.config;
import static androidx.core.util.Preconditions.checkNotNull;
import android.content.Context;
import android.content.SharedPreferences;
import androidx.annotation.GuardedBy;
import androidx.annotation.Keep;
import com.android.launcher3.Utilities;
import java.util.ArrayList;
import java.util.List;
import java.util.SortedMap;
import java.util.TreeMap;
/**
* Defines a set of flags used to control various launcher behaviors.
*
@ -23,11 +38,21 @@ package com.android.launcher3.config;
*
* <p>This class is kept package-private to prevent direct access.
*/
@Keep
abstract class BaseFlags {
private static final String TAG = "FeatureFlags";
private static final Object sLock = new Object();
@GuardedBy("sLock")
private static final List<TogglableFlag> sFlags = new ArrayList<>();
static final String FLAGS_PREF_NAME = "featureFlags";
BaseFlags() {
throw new UnsupportedOperationException("Don't instantiate BaseFlags");
}
public static boolean showFlagTogglerUi() {
return Utilities.IS_DEBUG_DEVICE;
}
public static final boolean IS_DOGFOOD_BUILD = false;
@ -36,10 +61,12 @@ abstract class BaseFlags {
// When enabled the promise icon is visible in all apps while installation an app.
public static final boolean LAUNCHER3_PROMISE_APPS_IN_ALL_APPS = false;
/** Feature flag to enable moving the QSB on the 0th screen of the workspace. */
public boolean isQsbOnFirstScreenEnabled() {
return true;
}
public static final TogglableFlag QSB_ON_FIRST_SCREEN = new TogglableFlag("QSB_ON_FIRST_SCREEN",
true,
"Enable moving the QSB on the 0th screen of the workspace");
public static final TogglableFlag EXAMPLE_FLAG = new TogglableFlag("EXAMPLE_FLAG", true,
"An example flag that doesn't do anything. Useful for testing");
//Feature flag to enable pulling down navigation shade from workspace.
public static final boolean PULL_DOWN_STATUS_BAR = true;
@ -56,4 +83,110 @@ abstract class BaseFlags {
// When true, overview shows screenshots in the orientation they were taken rather than
// trying to make them fit the orientation the device is in.
public static final boolean OVERVIEW_USE_SCREENSHOT_ORIENTATION = true;
public static void initialize(Context context) {
// Avoid the disk read for builds without the flags UI.
if (showFlagTogglerUi()) {
SharedPreferences sharedPreferences =
context.getSharedPreferences(FLAGS_PREF_NAME, Context.MODE_PRIVATE);
synchronized (sLock) {
for (TogglableFlag flag : sFlags) {
flag.currentValue = sharedPreferences.getBoolean(flag.key, flag.defaultValue);
}
}
} else {
synchronized (sLock) {
for (TogglableFlag flag : sFlags) {
flag.currentValue = flag.defaultValue;
}
}
}
}
static List<TogglableFlag> getTogglableFlags() {
// By Java Language Spec 12.4.2
// https://docs.oracle.com/javase/specs/jls/se7/html/jls-12.html#jls-12.4.2, the
// TogglableFlag instances on BaseFlags will be created before those on the FeatureFlags
// subclass. This code handles flags that are redeclared in FeatureFlags, ensuring the
// FeatureFlags one takes priority.
SortedMap<String, TogglableFlag> flagsByKey = new TreeMap<>();
synchronized (sLock) {
for (TogglableFlag flag : sFlags) {
flagsByKey.put(flag.key, flag);
}
}
return new ArrayList<>(flagsByKey.values());
}
public static final class TogglableFlag {
private final String key;
private final boolean defaultValue;
private final String description;
private boolean currentValue;
TogglableFlag(
String key,
boolean defaultValue,
String description) {
this.key = checkNotNull(key);
this.defaultValue = defaultValue;
this.description = checkNotNull(description);
synchronized (sLock) {
sFlags.add(this);
}
}
String getKey() {
return key;
}
boolean getDefaultValue() {
return defaultValue;
}
/** Returns the value of the flag at process start, including any overrides present. */
public boolean get() {
return currentValue;
}
String getDescription() {
return description;
}
@Override
public String toString() {
return "TogglableFlag{"
+ "key=" + key + ", "
+ "defaultValue=" + defaultValue + ", "
+ "description=" + description
+ "}";
}
@Override
public boolean equals(Object o) {
if (o == this) {
return true;
}
if (o instanceof TogglableFlag) {
TogglableFlag that = (TogglableFlag) o;
return (this.key.equals(that.getKey()))
&& (this.defaultValue == that.getDefaultValue())
&& (this.description.equals(that.getDescription()));
}
return false;
}
@Override
public int hashCode() {
int h$ = 1;
h$ *= 1000003;
h$ ^= key.hashCode();
h$ *= 1000003;
h$ ^= defaultValue ? 1231 : 1237;
h$ *= 1000003;
h$ ^= description.hashCode();
return h$;
}
}
}

View File

@ -0,0 +1,120 @@
/*
* 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.launcher3.config;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.os.Process;
import android.preference.PreferenceDataStore;
import android.preference.PreferenceFragment;
import android.preference.SwitchPreference;
import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.widget.Toast;
import com.android.launcher3.R;
import com.android.launcher3.config.BaseFlags.TogglableFlag;
/**
* Dev-build only UI allowing developers to toggle flag settings. See {@link FeatureFlags}.
*/
public final class FlagTogglerPreferenceFragment extends PreferenceFragment {
private static final String TAG = "FlagTogglerPrefFrag";
private SharedPreferences mSharedPreferences;
private MenuItem saveButton;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
addPreferencesFromResource(R.xml.flag_preferences);
mSharedPreferences = getContext().getSharedPreferences(
FeatureFlags.FLAGS_PREF_NAME, Context.MODE_PRIVATE);
// For flag overrides we only want to store when the engineer chose to override the
// flag with a different value than the default. That way, when we flip flags in
// future, engineers will pick up the new value immediately. To accomplish this, we use a
// custom preference data store.
getPreferenceManager().setPreferenceDataStore(new PreferenceDataStore() {
@Override
public void putBoolean(String key, boolean value) {
for (TogglableFlag flag : FeatureFlags.getTogglableFlags()) {
if (flag.getKey().equals(key)) {
if (value == flag.getDefaultValue()) {
mSharedPreferences.edit().remove(key).apply();
} else {
mSharedPreferences.edit().putBoolean(key, value).apply();
}
}
}
}
});
for (TogglableFlag flag : FeatureFlags.getTogglableFlags()) {
SwitchPreference switchPreference = new SwitchPreference(getContext());
switchPreference.setKey(flag.getKey());
switchPreference.setDefaultValue(flag.getDefaultValue());
switchPreference.setChecked(getFlagStateFromSharedPrefs(flag));
switchPreference.setTitle(flag.getKey());
switchPreference.setSummaryOn(flag.getDefaultValue() ? "" : "overridden");
switchPreference.setSummaryOff(flag.getDefaultValue() ? "overridden" : "");
getPreferenceScreen().addPreference(switchPreference);
}
setHasOptionsMenu(true);
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
saveButton = menu.add("Apply");
saveButton.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item == saveButton) {
mSharedPreferences.edit().commit();
Log.e(TAG,
"Killing launcher process " + Process.myPid() + " to apply new flag values");
System.exit(0);
}
return super.onOptionsItemSelected(item);
}
@Override
public void onStop() {
boolean anyChanged = false;
for (TogglableFlag flag : FeatureFlags.getTogglableFlags()) {
anyChanged = anyChanged ||
getFlagStateFromSharedPrefs(flag) != flag.get();
}
if (anyChanged) {
Toast.makeText(
getContext(),
"Flag won't be applied until you restart launcher",
Toast.LENGTH_LONG).show();
}
super.onStop();
}
private boolean getFlagStateFromSharedPrefs(TogglableFlag flag) {
return mSharedPreferences.getBoolean(flag.getKey(), flag.getDefaultValue());
}
}

View File

@ -12,6 +12,7 @@ import android.database.Cursor;
import android.graphics.Point;
import android.net.Uri;
import android.util.Log;
import com.android.launcher3.InvariantDeviceProfile;
import com.android.launcher3.ItemInfo;
import com.android.launcher3.LauncherAppState;
@ -109,6 +110,7 @@ public class GridSizeMigrationTask {
/**
* Applied all the pending DB operations
*
* @return true if any DB operation was commited.
*/
private boolean applyOperations() throws Exception {
@ -135,6 +137,7 @@ public class GridSizeMigrationTask {
* entries is more than what can fit in the new hotseat, we drop the entries with least weight.
* For weight calculation {@see #WT_SHORTCUT}, {@see #WT_APPLICATION}
* & {@see #WT_FOLDER_FACTOR}.
*
* @return true if any DB change was made
*/
protected boolean migrateHotseat() throws Exception {
@ -235,7 +238,8 @@ public class GridSizeMigrationTask {
int screenId = allScreens.get(i);
v.put(LauncherSettings.WorkspaceScreens._ID, screenId);
v.put(LauncherSettings.WorkspaceScreens.SCREEN_RANK, i);
mUpdateOperations.add(ContentProviderOperation.newInsert(uri).withValues(v).build());
mUpdateOperations.add(ContentProviderOperation.newInsert(uri).withValues(
v).build());
}
}
return applyOperations();
@ -254,8 +258,7 @@ public class GridSizeMigrationTask {
protected void migrateScreen(int screenId) {
// If we are migrating the first screen, do not touch the first row.
int startY =
(FeatureFlags.getInstance(mContext).isQsbOnFirstScreenEnabled()
&& screenId == Workspace.FIRST_SCREEN_ID)
(FeatureFlags.QSB_ON_FIRST_SCREEN.get() && screenId == Workspace.FIRST_SCREEN_ID)
? 1 : 0;
ArrayList<DbEntry> items = loadWorkspaceEntries(screenId);
@ -280,9 +283,11 @@ public class GridSizeMigrationTask {
for (int y = mSrcY - 1; y >= startY; y--) {
// Use a deep copy when trying out a particular combination as it can change
// the underlying object.
ArrayList<DbEntry> itemsOnScreen = tryRemove(x, y, startY, deepCopy(items), outLoss);
ArrayList<DbEntry> itemsOnScreen = tryRemove(x, y, startY, deepCopy(items),
outLoss);
if ((outLoss[0] < removeWt) || ((outLoss[0] == removeWt) && (outLoss[1] < moveWt))) {
if ((outLoss[0] < removeWt) || ((outLoss[0] == removeWt) && (outLoss[1]
< moveWt))) {
removeWt = outLoss[0];
moveWt = outLoss[1];
removedCol = mShouldRemoveX ? x : removedCol;
@ -363,6 +368,7 @@ public class GridSizeMigrationTask {
/**
* Tries the remove the provided row and column.
*
* @param items all the items on the screen under operation
* @param outLoss array of size 2. The first entry is filled with weight loss, and the second
* with the overall item movement.
@ -438,6 +444,7 @@ public class GridSizeMigrationTask {
/**
* Recursively finds a placement for the provided items.
*
* @param index the position in {@link #itemsToPlace} to start looking at.
* @param weightLoss total weight loss upto this point
* @param moveCost total move cost upto this point
@ -550,7 +557,8 @@ public class GridSizeMigrationTask {
for (int x = 0; x < mTrgX; x++) {
if (!occupied.cells[x][y]) {
int dist = ignoreMove ? 0 :
((me.cellX - x) * (me.cellX - x) + (me.cellY - y) * (me.cellY - y));
((me.cellX - x) * (me.cellX - x) + (me.cellY - y) * (me.cellY
- y));
if (dist < newDistance) {
newX = x;
newY = y;
@ -815,7 +823,8 @@ public class GridSizeMigrationTask {
public float weight;
public DbEntry() { }
public DbEntry() {
}
public DbEntry copy() {
DbEntry entry = new DbEntry();
@ -887,6 +896,7 @@ public class GridSizeMigrationTask {
/**
* Migrates the workspace and hotseat in case their sizes changed.
*
* @return false if the migration failed.
*/
public static boolean migrateGridIfNeeded(Context context) {
@ -896,7 +906,8 @@ public class GridSizeMigrationTask {
String gridSizeString = getPointString(idp.numColumns, idp.numRows);
if (gridSizeString.equals(prefs.getString(KEY_MIGRATION_SRC_WORKSPACE_SIZE, "")) &&
idp.numHotseatIcons == prefs.getInt(KEY_MIGRATION_SRC_HOTSEAT_COUNT, idp.numHotseatIcons)) {
idp.numHotseatIcons == prefs.getInt(KEY_MIGRATION_SRC_HOTSEAT_COUNT,
idp.numHotseatIcons)) {
// Skip if workspace and hotseat sizes have not changed.
return true;
}
@ -907,7 +918,8 @@ public class GridSizeMigrationTask {
HashSet<String> validPackages = getValidPackages(context);
// Hotseat
int srcHotseatCount = prefs.getInt(KEY_MIGRATION_SRC_HOTSEAT_COUNT, idp.numHotseatIcons);
int srcHotseatCount = prefs.getInt(KEY_MIGRATION_SRC_HOTSEAT_COUNT,
idp.numHotseatIcons);
if (srcHotseatCount != idp.numHotseatIcons) {
// Migrate hotseat.
@ -920,7 +932,8 @@ public class GridSizeMigrationTask {
Point sourceSize = parsePoint(prefs.getString(
KEY_MIGRATION_SRC_WORKSPACE_SIZE, gridSizeString));
if (new MultiStepMigrationTask(validPackages, context).migrate(sourceSize, targetSize)) {
if (new MultiStepMigrationTask(validPackages, context).migrate(sourceSize,
targetSize)) {
dbChanged = true;
}
@ -970,9 +983,11 @@ public class GridSizeMigrationTask {
/**
* Removes any broken item from the hotseat.
*
* @return a map with occupied hotseat position set to non-null value.
*/
public static IntSparseArrayMap<Object> removeBrokenHotseatItems(Context context) throws Exception {
public static IntSparseArrayMap<Object> removeBrokenHotseatItems(Context context)
throws Exception {
GridSizeMigrationTask task = new GridSizeMigrationTask(
context, LauncherAppState.getIDP(context), getValidPackages(context),
Integer.MAX_VALUE, Integer.MAX_VALUE);

View File

@ -441,7 +441,7 @@ public class LoaderCursor extends CursorWrapper {
// Mark the first row as occupied (if the feature is enabled)
// in order to account for the QSB.
screen.markCells(0, 0, countX + 1, 1,
FeatureFlags.getInstance(mContext).isQsbOnFirstScreenEnabled());
FeatureFlags.QSB_ON_FIRST_SCREEN.get());
}
occupied.put(item.screenId, screen);
}

View File

@ -136,7 +136,7 @@ public class ImportDataTask {
.getSerialNumberForUser(Process.myUserHandle()));
boolean createEmptyRowOnFirstScreen;
if (FeatureFlags.getInstance(mContext).isQsbOnFirstScreenEnabled()) {
if (FeatureFlags.QSB_ON_FIRST_SCREEN.get()) {
try (Cursor c = mContext.getContentResolver().query(mOtherFavoritesUri, null,
// get items on the first row of the first screen
"profileId = ? AND container = -100 AND screen = ? AND cellY = 0",

View File

@ -213,7 +213,7 @@ public class QsbContainerView extends FrameLayout {
}
public boolean isQsbEnabled() {
return FeatureFlags.getInstance(getContext()).isQsbOnFirstScreenEnabled();
return FeatureFlags.QSB_ON_FIRST_SCREEN.get();
}
protected Bundle createBindOptions() {

View File

@ -22,11 +22,7 @@ import android.content.Context;
* Defines a set of flags used to control various launcher behaviors
*/
public final class FeatureFlags extends BaseFlags {
private static FeatureFlags instance = new FeatureFlags();
public static FeatureFlags getInstance(Context context) {
return instance;
private FeatureFlags() {
// Prevent instantiation
}
private FeatureFlags() {}
}