diff --git a/Android.bp b/Android.bp index aefc1f0b3c..2608280c92 100644 --- a/Android.bp +++ b/Android.bp @@ -23,6 +23,7 @@ java_library_static { srcs: [ "tests/tapl/**/*.java", "quickstep/src/com/android/quickstep/SwipeUpSetting.java", + "src/com/android/launcher3/util/SecureSettingsObserver.java", "src/com/android/launcher3/TestProtocol.java", ], platform_apis: true, diff --git a/Android.mk b/Android.mk index cddd53d90f..15daf1fd2f 100644 --- a/Android.mk +++ b/Android.mk @@ -28,6 +28,34 @@ LOCAL_UNINSTALLABLE_MODULE := true LOCAL_SDK_VERSION := current include $(BUILD_PREBUILT) +include $(CLEAR_VARS) +LOCAL_MODULE := libPluginCore +LOCAL_MODULE_TAGS := optional +LOCAL_MODULE_CLASS := JAVA_LIBRARIES +LOCAL_SRC_FILES := libs/plugin_core.jar +LOCAL_UNINSTALLABLE_MODULE := true +LOCAL_SDK_VERSION := current +include $(BUILD_PREBUILT) + +# +# Build rule for plugin lib (needed to write a plugin). +# +include $(CLEAR_VARS) +LOCAL_USE_AAPT2 := true +LOCAL_AAPT2_ONLY := true +LOCAL_MODULE_TAGS := optional + +LOCAL_STATIC_JAVA_LIBRARIES := libPluginCore + +LOCAL_SRC_FILES := \ + $(call all-java-files-under, src_plugins) + +LOCAL_SDK_VERSION := current +LOCAL_MIN_SDK_VERSION := 28 +LOCAL_MODULE := LauncherPluginLib + +include $(BUILD_STATIC_JAVA_LIBRARY) + # # Build rule for Launcher3 dependencies lib. # @@ -40,6 +68,8 @@ LOCAL_STATIC_ANDROID_LIBRARIES := \ androidx.recyclerview_recyclerview \ androidx.dynamicanimation_dynamicanimation +LOCAL_STATIC_JAVA_LIBRARIES := LauncherPluginLib + LOCAL_SRC_FILES := \ $(call all-proto-files-under, protos) \ $(call all-proto-files-under, proto_overrides) @@ -74,6 +104,8 @@ LOCAL_SRC_FILES := \ $(call all-java-files-under, src_flags) LOCAL_PROGUARD_FLAG_FILES := proguard.flags +# Proguard is disable for testing. Derivarive prjects to keep proguard enabled +LOCAL_PROGUARD_ENABLED := disabled LOCAL_SDK_VERSION := current LOCAL_MIN_SDK_VERSION := 21 diff --git a/go/src_flags/com/android/launcher3/config/FeatureFlags.java b/go/src_flags/com/android/launcher3/config/FeatureFlags.java index b11bb7c6aa..a90808c1e3 100644 --- a/go/src_flags/com/android/launcher3/config/FeatureFlags.java +++ b/go/src_flags/com/android/launcher3/config/FeatureFlags.java @@ -16,12 +16,15 @@ package com.android.launcher3.config; +import android.content.Context; + /** * Defines a set of flags used to control various launcher behaviors */ public final class FeatureFlags extends BaseFlags { - - private FeatureFlags() {} + private FeatureFlags() { + // Prevent instantiation + } // Features to control Launcher3Go behavior public static final boolean GO_DISABLE_WIDGETS = true; diff --git a/libs/plugin_core.jar b/libs/plugin_core.jar new file mode 100644 index 0000000000..dd27f86fa3 Binary files /dev/null and b/libs/plugin_core.jar differ diff --git a/quickstep/libs/sysui_shared.jar b/quickstep/libs/sysui_shared.jar index 7ff664d41c..c5a7c051e9 100644 Binary files a/quickstep/libs/sysui_shared.jar and b/quickstep/libs/sysui_shared.jar differ diff --git a/quickstep/src/com/android/launcher3/uioverrides/BackgroundAppState.java b/quickstep/src/com/android/launcher3/uioverrides/BackgroundAppState.java index 53dcc74808..fdb13b1d16 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/BackgroundAppState.java +++ b/quickstep/src/com/android/launcher3/uioverrides/BackgroundAppState.java @@ -15,10 +15,14 @@ */ package com.android.launcher3.uioverrides; +import android.os.RemoteException; import com.android.launcher3.Launcher; import com.android.launcher3.allapps.AllAppsTransitionController; import com.android.quickstep.QuickScrubController; +import com.android.quickstep.RecentsModel; import com.android.quickstep.util.LayoutUtils; +import com.android.quickstep.views.RecentsView; +import com.android.systemui.shared.recents.ISystemUiProxy; /** * State indicating that the Launcher is behind an app @@ -43,4 +47,27 @@ public class BackgroundAppState extends OverviewState { float progressDelta = (transitionLength / scrollRange); return super.getVerticalProgress(launcher) + progressDelta; } + + @Override + public float[] getOverviewScaleAndTranslationYFactor(Launcher launcher) { + // Initialize the recents view scale to what it would be when starting swipe up/quickscrub + RecentsView recentsView = launcher.getOverviewPanel(); + recentsView.getTaskSize(sTempRect); + int appWidth = launcher.getDragLayer().getWidth(); + if (recentsView.shouldUseMultiWindowTaskSizeStrategy()) { + ISystemUiProxy sysUiProxy = RecentsModel.INSTANCE.get(launcher).getSystemUiProxy(); + if (sysUiProxy != null) { + try { + // Try to use the actual non-minimized app width (launcher will be resized to + // the non-minimized bounds, which differs from the app width in landscape + // multi-window mode + appWidth = sysUiProxy.getNonMinimizedSplitScreenSecondaryBounds().width(); + } catch (RemoteException e) { + // Ignore, fall back to just using the drag layer width + } + } + } + float scale = (float) appWidth / sTempRect.width(); + return new float[] { scale, 0f }; + } } diff --git a/quickstep/src/com/android/launcher3/uioverrides/UiFactory.java b/quickstep/src/com/android/launcher3/uioverrides/UiFactory.java index 5980d85c0b..b406b30781 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/UiFactory.java +++ b/quickstep/src/com/android/launcher3/uioverrides/UiFactory.java @@ -117,6 +117,7 @@ public class UiFactory { launcher.getStateManager().addStateListener(new LauncherStateManager.StateListener() { @Override public void onStateSetImmediately(LauncherState state) { + onStateTransitionComplete(state); } @Override @@ -142,6 +143,7 @@ public class UiFactory { launcher.getStateManager().addStateListener(new LauncherStateManager.StateListener() { @Override public void onStateSetImmediately(LauncherState state) { + onStateTransitionComplete(state); } @Override diff --git a/quickstep/src/com/android/launcher3/uioverrides/plugins/PluginEnablerImpl.java b/quickstep/src/com/android/launcher3/uioverrides/plugins/PluginEnablerImpl.java new file mode 100644 index 0000000000..e9fac26bdb --- /dev/null +++ b/quickstep/src/com/android/launcher3/uioverrides/plugins/PluginEnablerImpl.java @@ -0,0 +1,45 @@ +/* + * 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.uioverrides.plugins; + +import android.content.ComponentName; +import android.content.Context; +import android.content.SharedPreferences; + +import com.android.launcher3.Utilities; +import com.android.systemui.shared.plugins.PluginEnabler; + +public class PluginEnablerImpl implements PluginEnabler { + + final private SharedPreferences mSharedPrefs; + + public PluginEnablerImpl(Context context) { + mSharedPrefs = Utilities.getDevicePrefs(context); + } + + @Override + public void setEnabled(ComponentName component, boolean enabled) { + mSharedPrefs.edit().putBoolean(toPrefString(component), enabled).apply(); + } + + @Override + public boolean isEnabled(ComponentName component) { + return mSharedPrefs.getBoolean(toPrefString(component), true); + } + + private String toPrefString(ComponentName component) { + return "PLUGIN_ENABLED_" + component.flattenToString(); + } +} diff --git a/quickstep/src/com/android/launcher3/uioverrides/plugins/PluginInitializerImpl.java b/quickstep/src/com/android/launcher3/uioverrides/plugins/PluginInitializerImpl.java new file mode 100644 index 0000000000..8a6aa050cd --- /dev/null +++ b/quickstep/src/com/android/launcher3/uioverrides/plugins/PluginInitializerImpl.java @@ -0,0 +1,47 @@ +/* + * 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.uioverrides.plugins; + +import android.content.Context; +import android.os.Looper; + +import com.android.launcher3.LauncherModel; +import com.android.systemui.shared.plugins.PluginEnabler; +import com.android.systemui.shared.plugins.PluginInitializer; + +public class PluginInitializerImpl implements PluginInitializer { + @Override + public Looper getBgLooper() { + return LauncherModel.getWorkerLooper(); + } + + @Override + public void onPluginManagerInit() { + } + + @Override + public String[] getWhitelistedPlugins(Context context) { + return new String[0]; + } + + @Override + public PluginEnabler getPluginEnabler(Context context) { + return new PluginEnablerImpl(context); + } + + @Override + public void handleWtfs() { + } +} diff --git a/quickstep/src/com/android/launcher3/uioverrides/plugins/PluginManagerWrapper.java b/quickstep/src/com/android/launcher3/uioverrides/plugins/PluginManagerWrapper.java new file mode 100644 index 0000000000..88c362d32d --- /dev/null +++ b/quickstep/src/com/android/launcher3/uioverrides/plugins/PluginManagerWrapper.java @@ -0,0 +1,57 @@ +/* + * 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.uioverrides.plugins; + +import android.content.Context; + +import com.android.launcher3.util.MainThreadInitializedObject; +import com.android.systemui.plugins.Plugin; +import com.android.systemui.plugins.PluginListener; +import com.android.systemui.shared.plugins.PluginEnabler; +import com.android.systemui.shared.plugins.PluginInitializer; +import com.android.systemui.shared.plugins.PluginManager; +import com.android.systemui.shared.plugins.PluginManagerImpl; + +public class PluginManagerWrapper { + + public static final MainThreadInitializedObject INSTANCE = + new MainThreadInitializedObject<>(PluginManagerWrapper::new); + + private final PluginManager mPluginManager; + private final PluginEnabler mPluginEnabler; + + private PluginManagerWrapper(Context c) { + PluginInitializer pluginInitializer = new PluginInitializerImpl(); + mPluginManager = new PluginManagerImpl(c, pluginInitializer); + mPluginEnabler = pluginInitializer.getPluginEnabler(c); + } + + PluginEnabler getPluginEnabler() { + return mPluginEnabler; + } + + public void addPluginListener(PluginListener listener, Class pluginClass) { + addPluginListener(listener, pluginClass, false); + } + + public void addPluginListener(PluginListener listener, Class pluginClass, + boolean allowMultiple) { + mPluginManager.addPluginListener(listener, pluginClass, allowMultiple); + } + + public void removePluginListener(PluginListener listener) { + mPluginManager.removePluginListener(listener); + } +} diff --git a/quickstep/src/com/android/launcher3/uioverrides/plugins/PluginPreferencesFragment.java b/quickstep/src/com/android/launcher3/uioverrides/plugins/PluginPreferencesFragment.java new file mode 100644 index 0000000000..3da4f84274 --- /dev/null +++ b/quickstep/src/com/android/launcher3/uioverrides/plugins/PluginPreferencesFragment.java @@ -0,0 +1,217 @@ +/* + * 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.uioverrides.plugins; + +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.net.Uri; +import android.os.Bundle; +import android.preference.PreferenceFragment; +import android.preference.PreferenceScreen; +import android.preference.SwitchPreference; +import android.provider.Settings; +import android.util.ArrayMap; +import android.util.ArraySet; +import android.view.View; + +import com.android.launcher3.R; +import com.android.systemui.shared.plugins.PluginEnabler; +import com.android.systemui.shared.plugins.PluginManager; +import com.android.systemui.shared.plugins.PluginPrefs; + +import java.util.List; +import java.util.Set; + +/** + * This class is copied from System UI Tuner, except using our PluginEnablerImpl. The reason we + * can't share a common base class in the shared lib is because the androidx preference dependency + * interferes with our recyclerview and fragment dependencies. + */ +public class PluginPreferencesFragment extends PreferenceFragment { + public static final String ACTION_PLUGIN_SETTINGS + = "com.android.systemui.action.PLUGIN_SETTINGS"; + + private static final String PLUGIN_PERMISSION = "com.android.systemui.permission.PLUGIN"; + + private PluginPrefs mPluginPrefs; + private PluginEnabler mPluginEnabler; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED); + filter.addAction(Intent.ACTION_PACKAGE_CHANGED); + filter.addAction(Intent.ACTION_PACKAGE_REMOVED); + filter.addDataScheme("package"); + getContext().registerReceiver(mReceiver, filter); + filter = new IntentFilter(Intent.ACTION_USER_UNLOCKED); + getContext().registerReceiver(mReceiver, filter); + + mPluginEnabler = PluginManagerWrapper.INSTANCE.get(getContext()).getPluginEnabler(); + loadPrefs(); + } + + @Override + public void onDestroy() { + super.onDestroy(); + getContext().unregisterReceiver(mReceiver); + } + + private void loadPrefs() { + PreferenceScreen screen = getPreferenceManager().createPreferenceScreen(getContext()); + screen.setOrderingAsAdded(false); + Context prefContext = getContext(); + mPluginPrefs = new PluginPrefs(getContext()); + PackageManager pm = getContext().getPackageManager(); + + Set pluginActions = mPluginPrefs.getPluginList(); + ArrayMap> plugins = new ArrayMap<>(); + for (String action : pluginActions) { + String name = toName(action); + List result = pm.queryIntentServices( + new Intent(action), PackageManager.MATCH_DISABLED_COMPONENTS); + for (ResolveInfo info : result) { + String packageName = info.serviceInfo.packageName; + if (!plugins.containsKey(packageName)) { + plugins.put(packageName, new ArraySet<>()); + } + plugins.get(packageName).add(name); + } + } + + List apps = pm.getPackagesHoldingPermissions(new String[]{PLUGIN_PERMISSION}, + PackageManager.MATCH_DISABLED_COMPONENTS | PackageManager.GET_SERVICES); + apps.forEach(app -> { + if (!plugins.containsKey(app.packageName)) return; + SwitchPreference pref = new PluginPreference(prefContext, app, mPluginEnabler); + pref.setSummary("Plugins: " + toString(plugins.get(app.packageName))); + screen.addPreference(pref); + }); + setPreferenceScreen(screen); + } + + private String toString(ArraySet plugins) { + StringBuilder b = new StringBuilder(); + for (String string : plugins) { + if (b.length() != 0) { + b.append(", "); + } + b.append(string); + } + return b.toString(); + } + + private String toName(String action) { + String str = action.replace("com.android.systemui.action.PLUGIN_", ""); + StringBuilder b = new StringBuilder(); + for (String s : str.split("_")) { + if (b.length() != 0) { + b.append(' '); + } + b.append(s.substring(0, 1)); + b.append(s.substring(1).toLowerCase()); + } + return b.toString(); + } + + private final BroadcastReceiver mReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + loadPrefs(); + } + }; + + private static class PluginPreference extends SwitchPreference { + private final boolean mHasSettings; + private final PackageInfo mInfo; + private final PluginEnabler mPluginEnabler; + + public PluginPreference(Context prefContext, PackageInfo info, PluginEnabler pluginEnabler) { + super(prefContext); + PackageManager pm = prefContext.getPackageManager(); + mHasSettings = pm.resolveActivity(new Intent(ACTION_PLUGIN_SETTINGS) + .setPackage(info.packageName), 0) != null; + mInfo = info; + mPluginEnabler = pluginEnabler; + setTitle(info.applicationInfo.loadLabel(pm)); + setChecked(isPluginEnabled()); + setWidgetLayoutResource(R.layout.switch_preference_with_settings); + } + + private boolean isPluginEnabled() { + for (int i = 0; i < mInfo.services.length; i++) { + ComponentName componentName = new ComponentName(mInfo.packageName, + mInfo.services[i].name); + if (!mPluginEnabler.isEnabled(componentName)) { + return false; + } + } + return true; + } + + @Override + protected boolean persistBoolean(boolean isEnabled) { + boolean shouldSendBroadcast = false; + for (int i = 0; i < mInfo.services.length; i++) { + ComponentName componentName = new ComponentName(mInfo.packageName, + mInfo.services[i].name); + + if (mPluginEnabler.isEnabled(componentName) != isEnabled) { + mPluginEnabler.setEnabled(componentName, isEnabled); + shouldSendBroadcast = true; + } + } + if (shouldSendBroadcast) { + final String pkg = mInfo.packageName; + final Intent intent = new Intent(PluginManager.PLUGIN_CHANGED, + pkg != null ? Uri.fromParts("package", pkg, null) : null); + getContext().sendBroadcast(intent); + } + setChecked(isEnabled); + return true; + } + + @Override + protected void onBindView(View view) { + super.onBindView(view); + view.findViewById(R.id.settings).setVisibility(mHasSettings ? View.VISIBLE + : View.GONE); + view.findViewById(R.id.divider).setVisibility(mHasSettings ? View.VISIBLE + : View.GONE); + view.findViewById(R.id.settings).setOnClickListener(v -> { + ResolveInfo result = v.getContext().getPackageManager().resolveActivity( + new Intent(ACTION_PLUGIN_SETTINGS).setPackage( + mInfo.packageName), 0); + if (result != null) { + v.getContext().startActivity(new Intent().setComponent( + new ComponentName(result.activityInfo.packageName, + result.activityInfo.name))); + } + }); + view.setOnLongClickListener(v -> { + Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); + intent.setData(Uri.fromParts("package", mInfo.packageName, null)); + getContext().startActivity(intent); + return true; + }); + } + } +} diff --git a/quickstep/src/com/android/quickstep/ActivityControlHelper.java b/quickstep/src/com/android/quickstep/ActivityControlHelper.java index 206c8be4b4..2331a4e302 100644 --- a/quickstep/src/com/android/quickstep/ActivityControlHelper.java +++ b/quickstep/src/com/android/quickstep/ActivityControlHelper.java @@ -286,7 +286,7 @@ public interface ActivityControlHelper { } if (interactionType == INTERACTION_NORMAL) { - playScaleDownAnim(anim, activity); + playScaleDownAnim(anim, activity, endState); } anim.setDuration(transitionLength * 2); @@ -304,14 +304,24 @@ public interface ActivityControlHelper { /** * Scale down recents from the center task being full screen to being in overview. */ - private void playScaleDownAnim(AnimatorSet anim, Launcher launcher) { + private void playScaleDownAnim(AnimatorSet anim, Launcher launcher, + LauncherState endState) { RecentsView recentsView = launcher.getOverviewPanel(); TaskView v = recentsView.getTaskViewAt(recentsView.getCurrentPage()); if (v == null) { return; } + + // Setup the clip animation helper source/target rects in the final transformed state + // of the recents view (a scale may be applied prior to this animation starting to + // line up the side pages during swipe up) + float prevRvScale = recentsView.getScaleX(); + float targetRvScale = endState.getOverviewScaleAndTranslationYFactor(launcher)[0]; + SCALE_PROPERTY.set(recentsView, targetRvScale); ClipAnimationHelper clipHelper = new ClipAnimationHelper(); clipHelper.fromTaskThumbnailView(v.getThumbnail(), (RecentsView) v.getParent(), null); + SCALE_PROPERTY.set(recentsView, prevRvScale); + if (!clipHelper.getSourceRect().isEmpty() && !clipHelper.getTargetRect().isEmpty()) { float fromScale = clipHelper.getSourceRect().width() / clipHelper.getTargetRect().width(); diff --git a/quickstep/src/com/android/quickstep/NormalizedIconLoader.java b/quickstep/src/com/android/quickstep/NormalizedIconLoader.java index 655776194f..bd6204aab1 100644 --- a/quickstep/src/com/android/quickstep/NormalizedIconLoader.java +++ b/quickstep/src/com/android/quickstep/NormalizedIconLoader.java @@ -43,7 +43,6 @@ public class NormalizedIconLoader extends IconLoader { private final SparseArray mDefaultIcons = new SparseArray<>(); private final DrawableFactory mDrawableFactory; private final boolean mDisableColorExtraction; - private LauncherIcons mLauncherIcons; public NormalizedIconLoader(Context context, TaskKeyLruCache iconCache, LruCache activityInfoCache, @@ -73,19 +72,18 @@ public class NormalizedIconLoader extends IconLoader { false)); } - private synchronized BitmapInfo getBitmapInfo(Drawable drawable, int userId, + private BitmapInfo getBitmapInfo(Drawable drawable, int userId, int primaryColor, boolean isInstantApp) { - if (mLauncherIcons == null) { - mLauncherIcons = LauncherIcons.obtain(mContext); + try (LauncherIcons la = LauncherIcons.obtain(mContext)) { if (mDisableColorExtraction) { - mLauncherIcons.disableColorExtraction(); + la.disableColorExtraction(); } - } + la.setWrapperBackgroundColor(primaryColor); - mLauncherIcons.setWrapperBackgroundColor(primaryColor); - // User version code O, so that the icon is always wrapped in an adaptive icon container. - return mLauncherIcons.createBadgedIconBitmap(drawable, UserHandle.of(userId), - Build.VERSION_CODES.O, isInstantApp); + // User version code O, so that the icon is always wrapped in an adaptive icon container + return la.createBadgedIconBitmap(drawable, UserHandle.of(userId), + Build.VERSION_CODES.O, isInstantApp); + } } @Override diff --git a/quickstep/src/com/android/quickstep/OtherActivityTouchConsumer.java b/quickstep/src/com/android/quickstep/OtherActivityTouchConsumer.java index 4417a3da77..94ec69a0c2 100644 --- a/quickstep/src/com/android/quickstep/OtherActivityTouchConsumer.java +++ b/quickstep/src/com/android/quickstep/OtherActivityTouchConsumer.java @@ -52,6 +52,7 @@ import com.android.quickstep.util.RemoteAnimationTargetSet; import com.android.systemui.shared.system.ActivityManagerWrapper; import com.android.systemui.shared.system.AssistDataReceiver; import com.android.systemui.shared.system.BackgroundExecutor; +import com.android.systemui.shared.system.InputConsumerController; import com.android.systemui.shared.system.NavigationBarCompat; import com.android.systemui.shared.system.NavigationBarCompat.HitTarget; import com.android.systemui.shared.system.RecentsAnimationControllerCompat; @@ -80,6 +81,7 @@ public class OtherActivityTouchConsumer extends ContextWrapper implements TouchC private final OverviewCallbacks mOverviewCallbacks; private final TaskOverlayFactory mTaskOverlayFactory; private final TouchInteractionLog mTouchInteractionLog; + private final InputConsumerController mInputConsumer; private final boolean mIsDeferredDownTarget; private final PointF mDownPos = new PointF(); @@ -101,8 +103,8 @@ public class OtherActivityTouchConsumer extends ContextWrapper implements TouchC RecentsModel recentsModel, Intent homeIntent, ActivityControlHelper activityControl, MainThreadExecutor mainThreadExecutor, Choreographer backgroundThreadChoreographer, @HitTarget int downHitTarget, OverviewCallbacks overviewCallbacks, - TaskOverlayFactory taskOverlayFactory, VelocityTracker velocityTracker, - TouchInteractionLog touchInteractionLog) { + TaskOverlayFactory taskOverlayFactory, InputConsumerController inputConsumer, + VelocityTracker velocityTracker, TouchInteractionLog touchInteractionLog) { super(base); mRunningTask = runningTaskInfo; @@ -117,6 +119,7 @@ public class OtherActivityTouchConsumer extends ContextWrapper implements TouchC mTaskOverlayFactory = taskOverlayFactory; mTouchInteractionLog = touchInteractionLog; mTouchInteractionLog.setTouchConsumer(this); + mInputConsumer = inputConsumer; } @Override @@ -226,7 +229,7 @@ public class OtherActivityTouchConsumer extends ContextWrapper implements TouchC RecentsAnimationState animationState = new RecentsAnimationState(); final WindowTransformSwipeHandler handler = new WindowTransformSwipeHandler( animationState.id, mRunningTask, this, touchTimeMs, mActivityControlHelper, - mTouchInteractionLog); + mInputConsumer, mTouchInteractionLog); // Preload the plan mRecentsModel.loadTasks(mRunningTask.id, null); diff --git a/quickstep/src/com/android/quickstep/OverviewInteractionState.java b/quickstep/src/com/android/quickstep/OverviewInteractionState.java index d71c08ad52..27f1399ffc 100644 --- a/quickstep/src/com/android/quickstep/OverviewInteractionState.java +++ b/quickstep/src/com/android/quickstep/OverviewInteractionState.java @@ -15,23 +15,21 @@ */ package com.android.quickstep; +import static com.android.quickstep.SwipeUpSetting.newSwipeUpSettingsObserver; import static com.android.systemui.shared.system.NavigationBarCompat.FLAG_DISABLE_QUICK_SCRUB; import static com.android.systemui.shared.system.NavigationBarCompat.FLAG_DISABLE_SWIPE_UP; import static com.android.systemui.shared.system.NavigationBarCompat.FLAG_SHOW_OVERVIEW_BUTTON; -import static com.android.systemui.shared.system.SettingsCompat.SWIPE_UP_SETTING_NAME; -import android.content.ContentResolver; import android.content.Context; -import android.database.ContentObserver; import android.os.Handler; import android.os.Message; import android.os.RemoteException; -import android.provider.Settings; import android.util.Log; import com.android.launcher3.Utilities; import com.android.launcher3.allapps.DiscoveryBounce; import com.android.launcher3.util.MainThreadInitializedObject; +import com.android.launcher3.util.SecureSettingsObserver; import com.android.launcher3.util.UiThreadHelper; import com.android.systemui.shared.recents.ISystemUiProxy; @@ -60,7 +58,7 @@ public class OverviewInteractionState { private static final int MSG_SET_BACK_BUTTON_ALPHA = 201; private static final int MSG_SET_SWIPE_UP_ENABLED = 202; - private final SwipeUpGestureEnabledSettingObserver mSwipeUpSettingObserver; + private final SecureSettingsObserver mSwipeUpSettingObserver; private final Context mContext; private final Handler mUiHandler; @@ -85,9 +83,11 @@ public class OverviewInteractionState { mBgHandler = new Handler(UiThreadHelper.getBackgroundLooper(), this::handleBgMessage); if (SwipeUpSetting.isSwipeUpSettingAvailable()) { - mSwipeUpSettingObserver = new SwipeUpGestureEnabledSettingObserver(mUiHandler, - context.getContentResolver()); + mSwipeUpSettingObserver = + newSwipeUpSettingsObserver(context, this::notifySwipeUpSettingChanged); mSwipeUpSettingObserver.register(); + mSwipeUpEnabled = mSwipeUpSettingObserver.getValue(); + resetHomeBounceSeenOnQuickstepEnabledFirstTime(); } else { mSwipeUpSettingObserver = null; mSwipeUpEnabled = SwipeUpSetting.isSwipeUpEnabledDefaultValue(); @@ -192,34 +192,6 @@ public class OverviewInteractionState { sendToTarget(); } - private class SwipeUpGestureEnabledSettingObserver extends ContentObserver { - private ContentResolver mResolver; - private final int defaultValue; - - SwipeUpGestureEnabledSettingObserver(Handler handler, ContentResolver resolver) { - super(handler); - mResolver = resolver; - defaultValue = SwipeUpSetting.isSwipeUpEnabledDefaultValue() ? 1 : 0; - } - - public void register() { - mResolver.registerContentObserver(Settings.Secure.getUriFor(SWIPE_UP_SETTING_NAME), - false, this); - mSwipeUpEnabled = getValue(); - resetHomeBounceSeenOnQuickstepEnabledFirstTime(); - } - - @Override - public void onChange(boolean selfChange) { - super.onChange(selfChange); - notifySwipeUpSettingChanged(getValue()); - } - - private boolean getValue() { - return Settings.Secure.getInt(mResolver, SWIPE_UP_SETTING_NAME, defaultValue) == 1; - } - } - private void resetHomeBounceSeenOnQuickstepEnabledFirstTime() { if (mSwipeUpEnabled && !Utilities.getPrefs(mContext).getBoolean( HAS_ENABLED_QUICKSTEP_ONCE, true)) { diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationWrapper.java b/quickstep/src/com/android/quickstep/RecentsAnimationWrapper.java index eea3971f40..2f3cb5f373 100644 --- a/quickstep/src/com/android/quickstep/RecentsAnimationWrapper.java +++ b/quickstep/src/com/android/quickstep/RecentsAnimationWrapper.java @@ -53,24 +53,18 @@ public class RecentsAnimationWrapper { new LooperExecutor(UiThreadHelper.getBackgroundLooper()); private final MainThreadExecutor mMainThreadExecutor = new MainThreadExecutor(); - private InputConsumerController mInputConsumer = - InputConsumerController.getRecentsAnimationInputConsumer(); + private final InputConsumerController mInputConsumer; private final Supplier mTouchProxySupplier; - private boolean mInputConsumerUnregistered; - private boolean mTouchProxyEnabled; - private TouchConsumer mTouchConsumer; private boolean mTouchInProgress; - private boolean mInputConsumerUnregisterPending; private boolean mFinishPending; - public RecentsAnimationWrapper(Supplier touchProxySupplier) { - // Register the input consumer on the UI thread, to ensure that it runs after any pending - // unregister calls + public RecentsAnimationWrapper(InputConsumerController inputConsumer, + Supplier touchProxySupplier) { + mInputConsumer = inputConsumer; mTouchProxySupplier = touchProxySupplier; - mMainThreadExecutor.execute(mInputConsumer::registerInputConsumer); } public synchronized void setController( @@ -109,6 +103,7 @@ public class RecentsAnimationWrapper { public void finish(boolean toHome, Runnable onFinishComplete) { if (!toHome) { mExecutorService.submit(() -> finishBg(false, onFinishComplete)); + return; } mMainThreadExecutor.execute(() -> { @@ -152,28 +147,12 @@ public class RecentsAnimationWrapper { } } - public void unregisterInputConsumer() { - mMainThreadExecutor.execute(this::unregisterInputConsumerUi); - } - - private void unregisterInputConsumerUi() { - if (mTouchProxyEnabled && mTouchInProgress) { - mInputConsumerUnregisterPending = true; - } else { - mInputConsumerUnregistered = true; - mInputConsumer.unregisterInputConsumer(); - } - } - public void enableTouchProxy() { mMainThreadExecutor.execute(this::enableTouchProxyUi); } private void enableTouchProxyUi() { - if (!mInputConsumerUnregistered) { - mTouchProxyEnabled = true; - mInputConsumer.setTouchListener(this::onInputConsumerTouch); - } + mInputConsumer.setTouchListener(this::onInputConsumerTouch); } private boolean onInputConsumerTouch(MotionEvent ev) { @@ -184,10 +163,6 @@ public class RecentsAnimationWrapper { } else if (action == ACTION_CANCEL || action == ACTION_UP) { // Finish any pending actions mTouchInProgress = false; - if (mInputConsumerUnregisterPending) { - mInputConsumerUnregisterPending = false; - mInputConsumer.unregisterInputConsumer(); - } if (mFinishPending) { mFinishPending = false; mExecutorService.submit(() -> finishBg(true, null)); diff --git a/quickstep/src/com/android/quickstep/SwipeUpSetting.java b/quickstep/src/com/android/quickstep/SwipeUpSetting.java index 0f91f97755..381ab9ffd8 100644 --- a/quickstep/src/com/android/quickstep/SwipeUpSetting.java +++ b/quickstep/src/com/android/quickstep/SwipeUpSetting.java @@ -16,9 +16,15 @@ package com.android.quickstep; +import static com.android.systemui.shared.system.SettingsCompat.SWIPE_UP_SETTING_NAME; + +import android.content.Context; import android.content.res.Resources; import android.util.Log; +import com.android.launcher3.util.SecureSettingsObserver; +import com.android.launcher3.util.SecureSettingsObserver.OnChangeListener; + public final class SwipeUpSetting { private static final String TAG = "SwipeUpSetting"; @@ -47,4 +53,10 @@ public final class SwipeUpSetting { public static boolean isSwipeUpEnabledDefaultValue() { return getSystemBooleanRes(SWIPE_UP_ENABLED_DEFAULT_RES_NAME); } + + public static SecureSettingsObserver newSwipeUpSettingsObserver(Context context, + OnChangeListener listener) { + return new SecureSettingsObserver(context.getContentResolver(), listener, + SWIPE_UP_SETTING_NAME, isSwipeUpEnabledDefaultValue() ? 1 : 0); + } } diff --git a/quickstep/src/com/android/quickstep/TaskSystemShortcut.java b/quickstep/src/com/android/quickstep/TaskSystemShortcut.java index e64d04afd8..66ce4c3ac7 100644 --- a/quickstep/src/com/android/quickstep/TaskSystemShortcut.java +++ b/quickstep/src/com/android/quickstep/TaskSystemShortcut.java @@ -64,7 +64,7 @@ public class TaskSystemShortcut extends SystemShortcut protected T mSystemShortcut; protected TaskSystemShortcut(T systemShortcut) { - super(systemShortcut.iconResId, systemShortcut.labelResId); + super(systemShortcut); mSystemShortcut = systemShortcut; } diff --git a/quickstep/src/com/android/quickstep/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java index b9f95ccfa4..9371a4c1cf 100644 --- a/quickstep/src/com/android/quickstep/TouchInteractionService.java +++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java @@ -19,7 +19,7 @@ import static android.view.MotionEvent.ACTION_CANCEL; import static android.view.MotionEvent.ACTION_DOWN; import static android.view.MotionEvent.ACTION_MOVE; import static android.view.MotionEvent.ACTION_POINTER_DOWN; -import static android.view.MotionEvent.ACTION_POINTER_UP; +import static android.view.MotionEvent.ACTION_POINTER_INDEX_SHIFT; import static android.view.MotionEvent.ACTION_UP; import static com.android.systemui.shared.system.ActivityManagerWrapper @@ -51,7 +51,9 @@ import com.android.systemui.shared.recents.IOverviewProxy; import com.android.systemui.shared.recents.ISystemUiProxy; import com.android.systemui.shared.system.ActivityManagerWrapper; import com.android.systemui.shared.system.ChoreographerCompat; +import com.android.systemui.shared.system.InputConsumerController; import com.android.systemui.shared.system.NavigationBarCompat.HitTarget; + import java.io.FileDescriptor; import java.io.PrintWriter; @@ -184,6 +186,7 @@ public class TouchInteractionService extends Service { private OverviewCallbacks mOverviewCallbacks; private TaskOverlayFactory mTaskOverlayFactory; private TouchInteractionLog mTouchInteractionLog; + private InputConsumerController mInputConsumer; private Choreographer mMainThreadChoreographer; private Choreographer mBackgroundThreadChoreographer; @@ -202,6 +205,8 @@ public class TouchInteractionService extends Service { mOverviewCallbacks = OverviewCallbacks.get(this); mTaskOverlayFactory = TaskOverlayFactory.get(this); mTouchInteractionLog = new TouchInteractionLog(); + mInputConsumer = InputConsumerController.getRecentsAnimationInputConsumer(); + mInputConsumer.registerInputConsumer(); sConnected = true; @@ -212,6 +217,7 @@ public class TouchInteractionService extends Service { @Override public void onDestroy() { + mInputConsumer.unregisterInputConsumer(); mOverviewCommandHelper.onDestroy(); sConnected = false; super.onDestroy(); @@ -255,7 +261,7 @@ public class TouchInteractionService extends Service { mOverviewCommandHelper.overviewIntent, mOverviewCommandHelper.getActivityControlHelper(), mMainThreadExecutor, mBackgroundThreadChoreographer, downHitTarget, mOverviewCallbacks, - mTaskOverlayFactory, tracker, mTouchInteractionLog); + mTaskOverlayFactory, mInputConsumer, tracker, mTouchInteractionLog); } } @@ -299,7 +305,7 @@ public class TouchInteractionService extends Service { mActivityHelper = activityHelper; mActivity = activity; mTarget = activity.getDragLayer(); - mTouchSlop = ViewConfiguration.get(mTarget.getContext()).getScaledTouchSlop(); + mTouchSlop = ViewConfiguration.get(mActivity).getScaledTouchSlop(); mStartingInActivityBounds = startingInActivityBounds; mQuickScrubController = mActivity.getOverviewPanel() @@ -324,12 +330,6 @@ public class TouchInteractionService extends Service { mDownPos.set(ev.getX(), ev.getY()); } else if (!mTrackingStarted) { switch (action) { - case ACTION_POINTER_UP: - case ACTION_POINTER_DOWN: - if (!mTrackingStarted) { - mInvalidated = true; - } - break; case ACTION_CANCEL: case ACTION_UP: startTouchTracking(ev, true /* updateLocationOffset */); @@ -359,15 +359,26 @@ public class TouchInteractionService extends Service { } // Send down touch event - MotionEvent down = MotionEvent.obtain(ev); + MotionEvent down = MotionEvent.obtainNoHistory(ev); down.setAction(ACTION_DOWN); sendEvent(down); - down.recycle(); mTrackingStarted = true; + // Send pointer down for remaining pointers. + int pointerCount = ev.getPointerCount(); + for (int i = 1; i < pointerCount; i++) { + down.setAction(ACTION_POINTER_DOWN | (i << ACTION_POINTER_INDEX_SHIFT)); + sendEvent(down); + } + + down.recycle(); } private void sendEvent(MotionEvent ev) { + if (!mTarget.verifyTouchDispatch(this, ev)) { + mInvalidated = true; + return; + } int flags = ev.getEdgeFlags(); ev.setEdgeFlags(flags | TouchInteractionService.EDGE_NAV_BAR); ev.offsetLocation(-mLocationOnScreen[0], -mLocationOnScreen[1]); diff --git a/quickstep/src/com/android/quickstep/WindowTransformSwipeHandler.java b/quickstep/src/com/android/quickstep/WindowTransformSwipeHandler.java index 9991552987..c4c660c647 100644 --- a/quickstep/src/com/android/quickstep/WindowTransformSwipeHandler.java +++ b/quickstep/src/com/android/quickstep/WindowTransformSwipeHandler.java @@ -78,6 +78,7 @@ import com.android.quickstep.views.RecentsView; import com.android.quickstep.views.TaskView; import com.android.systemui.shared.recents.model.ThumbnailData; import com.android.systemui.shared.system.ActivityManagerWrapper; +import com.android.systemui.shared.system.InputConsumerController; import com.android.systemui.shared.system.LatencyTrackerCompat; import com.android.systemui.shared.system.RecentsAnimationControllerCompat; import com.android.systemui.shared.system.RemoteAnimationTargetCompat; @@ -226,8 +227,7 @@ public class WindowTransformSwipeHandler { private @InteractionType int mInteractionType = INTERACTION_NORMAL; - private final RecentsAnimationWrapper mRecentsAnimationWrapper = - new RecentsAnimationWrapper(this::createNewTouchProxyHandler); + private final RecentsAnimationWrapper mRecentsAnimationWrapper; private final long mTouchTimeMs; private long mLauncherFrameDrawnTime; @@ -241,7 +241,7 @@ public class WindowTransformSwipeHandler { WindowTransformSwipeHandler(int id, RunningTaskInfo runningTaskInfo, Context context, long touchTimeMs, ActivityControlHelper controller, - TouchInteractionLog touchInteractionLog) { + InputConsumerController inputConsumer, TouchInteractionLog touchInteractionLog) { this.id = id; mContext = context; mRunningTaskInfo = runningTaskInfo; @@ -251,6 +251,8 @@ public class WindowTransformSwipeHandler { mActivityInitListener = mActivityControlHelper .createActivityInitListener(this::onActivityInit); mTouchInteractionLog = touchInteractionLog; + mRecentsAnimationWrapper = new RecentsAnimationWrapper(inputConsumer, + this::createNewTouchProxyHandler); initStateCallbacks(); } @@ -860,7 +862,6 @@ public class WindowTransformSwipeHandler { } mActivityInitListener.unregister(); - mRecentsAnimationWrapper.unregisterInputConsumer(); mTaskSnapshot = null; } diff --git a/quickstep/src/com/android/quickstep/util/ClipAnimationHelper.java b/quickstep/src/com/android/quickstep/util/ClipAnimationHelper.java index 711ef586ed..8c84f29b7d 100644 --- a/quickstep/src/com/android/quickstep/util/ClipAnimationHelper.java +++ b/quickstep/src/com/android/quickstep/util/ClipAnimationHelper.java @@ -90,8 +90,6 @@ public class ClipAnimationHelper { // Whether to boost the opening animation target layers, or the closing private int mBoostModeTargetLayers = -1; - // Wether or not applyTransform has been called yet since prepareAnimation() - private boolean mIsFirstFrame = true; private BiFunction mTaskAlphaCallback = (t, a1) -> a1; diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java index 8ad3cee6f2..1205bdc3b6 100644 --- a/quickstep/src/com/android/quickstep/views/RecentsView.java +++ b/quickstep/src/com/android/quickstep/views/RecentsView.java @@ -229,7 +229,9 @@ public abstract class RecentsView extends PagedView impl } }; - private int mLoadPlanId = -1; + // Used to keep track of the last requested load plan id, so that we do not request to load the + // tasks again if we have already requested it and the task list has not changed + private int mRequestedLoadPlanId = -1; // Only valid until the launcher state changes to NORMAL private int mRunningTaskId = -1; @@ -400,9 +402,6 @@ public abstract class RecentsView extends PagedView impl final int y = (int) ev.getY(); switch (ev.getAction()) { case MotionEvent.ACTION_UP: - if (mShowEmptyMessage) { - onAllTasksRemoved(); - } if (mTouchDownToStartHome) { startHome(); } @@ -413,7 +412,8 @@ public abstract class RecentsView extends PagedView impl break; case MotionEvent.ACTION_MOVE: // Passing the touch slop will not allow dismiss to home - if (mTouchDownToStartHome && Math.hypot(mDownX - x, mDownY - y) > mTouchSlop) { + if (mTouchDownToStartHome && + (isHandlingTouch() || Math.hypot(mDownX - x, mDownY - y) > mTouchSlop)) { mTouchDownToStartHome = false; } break; @@ -421,12 +421,17 @@ public abstract class RecentsView extends PagedView impl // Touch down anywhere but the deadzone around the visible clear all button and // between the task views will start home on touch up if (!isHandlingTouch()) { - updateDeadZoneRects(); - final boolean clearAllButtonDeadZoneConsumed = mClearAllButton.getAlpha() == 1 - && mClearAllButtonDeadZoneRect.contains(x, y); - if (!clearAllButtonDeadZoneConsumed - && !mTaskViewDeadZoneRect.contains(x + getScrollX(), y)) { + if (mShowEmptyMessage) { mTouchDownToStartHome = true; + } else { + updateDeadZoneRects(); + final boolean clearAllButtonDeadZoneConsumed = + mClearAllButton.getAlpha() == 1 + && mClearAllButtonDeadZoneRect.contains(x, y); + if (!clearAllButtonDeadZoneConsumed + && !mTaskViewDeadZoneRect.contains(x + getScrollX(), y)) { + mTouchDownToStartHome = true; + } } } mDownX = x; @@ -444,6 +449,7 @@ public abstract class RecentsView extends PagedView impl mPendingAnimation.addEndListener((onEndListener) -> applyLoadPlan(loadPlan)); return; } + TaskStack stack = loadPlan != null ? loadPlan.getTaskStack() : null; if (stack == null) { removeAllViews(); @@ -612,8 +618,9 @@ public abstract class RecentsView extends PagedView impl * and unloads the associated task data for tasks that are no longer visible. */ public void loadVisibleTaskData() { - if (!mOverviewStateEnabled) { - // Skip loading visible task data if we've already left the overview state + if (!mOverviewStateEnabled || mRequestedLoadPlanId == -1) { + // Skip loading visible task data if we've already left the overview state, or if the + // task list hasn't been loaded yet (the task views will not reflect the task list) return; } @@ -666,16 +673,13 @@ public abstract class RecentsView extends PagedView impl mHasVisibleTaskData.clear(); } - protected void onAllTasksRemoved() { - startHome(); - } - protected abstract void startHome(); public void reset() { mRunningTaskId = -1; mRunningTaskTileHidden = false; mIgnoreResetTaskId = -1; + mRequestedLoadPlanId = -1; unloadVisibleTaskData(); setCurrentPage(0); @@ -687,8 +691,8 @@ public abstract class RecentsView extends PagedView impl * Reloads the view if anything in recents changed. */ public void reloadIfNeeded() { - if (!mModel.isLoadPlanValid(mLoadPlanId)) { - mLoadPlanId = mModel.loadTasks(mRunningTaskId, this::applyLoadPlan); + if (!mModel.isLoadPlanValid(mRequestedLoadPlanId)) { + mRequestedLoadPlanId = mModel.loadTasks(mRunningTaskId, this::applyLoadPlan); } } @@ -749,8 +753,8 @@ public abstract class RecentsView extends PagedView impl setCurrentPage(0); - // Load the tasks (if the loading is already - mLoadPlanId = mModel.loadTasks(runningTaskId, this::applyLoadPlan); + // Load the tasks + reloadIfNeeded(); } public void showNextTask() { @@ -973,7 +977,7 @@ public abstract class RecentsView extends PagedView impl if (getTaskViewCount() == 0) { removeView(mClearAllButton); - onAllTasksRemoved(); + startHome(); } else { snapToPageImmediately(pageToSnapTo); } @@ -1002,7 +1006,7 @@ public abstract class RecentsView extends PagedView impl // Remove all the task views now ActivityManagerWrapper.getInstance().removeAllRecentTasks(); removeAllViews(); - onAllTasksRemoved(); + startHome(); } mPendingAnimation = null; }); diff --git a/quickstep/src/com/android/quickstep/views/TaskMenuView.java b/quickstep/src/com/android/quickstep/views/TaskMenuView.java index 28928a8e5a..c4afad7c99 100644 --- a/quickstep/src/com/android/quickstep/views/TaskMenuView.java +++ b/quickstep/src/com/android/quickstep/views/TaskMenuView.java @@ -208,8 +208,8 @@ public class TaskMenuView extends AbstractFloatingView { private void addMenuOption(TaskSystemShortcut menuOption, OnClickListener onClickListener) { ViewGroup menuOptionView = (ViewGroup) mActivity.getLayoutInflater().inflate( R.layout.task_view_menu_option, this, false); - menuOptionView.findViewById(R.id.icon).setBackgroundResource(menuOption.iconResId); - ((TextView) menuOptionView.findViewById(R.id.text)).setText(menuOption.labelResId); + menuOption.setIconAndLabelFor( + menuOptionView.findViewById(R.id.icon), menuOptionView.findViewById(R.id.text)); menuOptionView.setOnClickListener(onClickListener); mOptionLayout.addView(menuOptionView); } diff --git a/quickstep/src/com/android/quickstep/views/TaskView.java b/quickstep/src/com/android/quickstep/views/TaskView.java index a0615f502d..c1424c4751 100644 --- a/quickstep/src/com/android/quickstep/views/TaskView.java +++ b/quickstep/src/com/android/quickstep/views/TaskView.java @@ -387,8 +387,7 @@ public class TaskView extends FrameLayout implements TaskCallbacks, PageCallback for (TaskSystemShortcut menuOption : TaskMenuView.MENU_OPTIONS) { OnClickListener onClickListener = menuOption.getOnClickListener(activity, this); if (onClickListener != null) { - info.addAction(new AccessibilityNodeInfo.AccessibilityAction(menuOption.labelResId, - context.getText(menuOption.labelResId))); + info.addAction(menuOption.createAccessibilityAction(context)); } } @@ -409,7 +408,7 @@ public class TaskView extends FrameLayout implements TaskCallbacks, PageCallback } for (TaskSystemShortcut menuOption : TaskMenuView.MENU_OPTIONS) { - if (action == menuOption.labelResId) { + if (menuOption.hasHandlerForAction(action)) { OnClickListener onClickListener = menuOption.getOnClickListener( fromContext(getContext()), this); if (onClickListener != null) { diff --git a/quickstep/tests/src/com/android/quickstep/AbstractQuickStepTest.java b/quickstep/tests/src/com/android/quickstep/AbstractQuickStepTest.java new file mode 100644 index 0000000000..6854aa8dd5 --- /dev/null +++ b/quickstep/tests/src/com/android/quickstep/AbstractQuickStepTest.java @@ -0,0 +1,31 @@ +/* + * 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.quickstep; + +import com.android.launcher3.ui.AbstractLauncherUiTest; + +import org.junit.Rule; +import org.junit.rules.TestRule; + +/** + * Base class for all instrumentation tests that deal with Quickstep. + */ +public abstract class AbstractQuickStepTest extends AbstractLauncherUiTest { + @Rule + public TestRule mQuickstepOnOffExecutor = + new QuickStepOnOffRule(mMainThreadExecutor, mLauncher); +} diff --git a/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java b/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java new file mode 100644 index 0000000000..88b50d9dc5 --- /dev/null +++ b/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java @@ -0,0 +1,126 @@ +/* + * 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.quickstep; + +import static android.content.pm.PackageManager.MATCH_DISABLED_COMPONENTS; + +import static com.android.launcher3.tapl.LauncherInstrumentation.WAIT_TIME_MS; +import static com.android.launcher3.tapl.TestHelpers.getHomeIntentInPackage; +import static com.android.launcher3.tapl.TestHelpers.getLauncherInMyProcess; +import static com.android.launcher3.util.rule.ShellCommandRule.disableHeadsUpNotification; +import static com.android.launcher3.util.rule.ShellCommandRule.getLauncherCommand; +import static com.android.quickstep.QuickStepOnOffRule.Mode.OFF; + +import static org.junit.Assert.assertTrue; + +import static androidx.test.InstrumentationRegistry.getInstrumentation; + +import android.app.Instrumentation; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ActivityInfo; + +import com.android.launcher3.MainThreadExecutor; +import com.android.launcher3.tapl.LauncherInstrumentation; +import com.android.launcher3.testcomponent.TestCommandReceiver; +import com.android.quickstep.QuickStepOnOffRule.QuickstepOnOff; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestRule; +import org.junit.runner.RunWith; +import org.junit.runners.model.Statement; + +import androidx.test.filters.LargeTest; +import androidx.test.runner.AndroidJUnit4; +import androidx.test.uiautomator.By; +import androidx.test.uiautomator.UiDevice; +import androidx.test.uiautomator.Until; + +@LargeTest +@RunWith(AndroidJUnit4.class) +/** + * TODO: Fix fallback when quickstep is enabled + */ +public class FallbackRecentsTest { + + private final UiDevice mDevice; + private final LauncherInstrumentation mLauncher; + private final ActivityInfo mOtherLauncherActivity; + + @Rule public final TestRule mDisableHeadsUpNotification = disableHeadsUpNotification(); + @Rule public final TestRule mQuickstepOnOffExecutor; + + @Rule public final TestRule mSetLauncherCommand; + + public FallbackRecentsTest() { + Instrumentation instrumentation = getInstrumentation(); + Context context = instrumentation.getContext(); + mDevice = UiDevice.getInstance(instrumentation); + mLauncher = new LauncherInstrumentation(instrumentation); + + mQuickstepOnOffExecutor = new QuickStepOnOffRule(new MainThreadExecutor(), mLauncher); + mOtherLauncherActivity = context.getPackageManager().queryIntentActivities( + getHomeIntentInPackage(context), + MATCH_DISABLED_COMPONENTS).get(0).activityInfo; + + mSetLauncherCommand = (base, desc) -> new Statement() { + @Override + public void evaluate() throws Throwable { + TestCommandReceiver.callCommand(TestCommandReceiver.ENABLE_TEST_LAUNCHER); + UiDevice.getInstance(getInstrumentation()).executeShellCommand( + getLauncherCommand(mOtherLauncherActivity)); + try { + base.evaluate(); + } finally { + TestCommandReceiver.callCommand(TestCommandReceiver.DISABLE_TEST_LAUNCHER); + UiDevice.getInstance(getInstrumentation()).executeShellCommand( + getLauncherCommand(getLauncherInMyProcess())); + } + } + }; + } + + @QuickstepOnOff(mode = OFF) + @Test + public void goToOverviewFromHome() { + mDevice.pressHome(); + assertTrue("Fallback Launcher not visible", mDevice.wait(Until.hasObject(By.pkg( + mOtherLauncherActivity.packageName)), WAIT_TIME_MS)); + + mLauncher.getBackground().switchToOverview(); + } + + @QuickstepOnOff(mode = OFF) + @Test + public void goToOverviewFromApp() { + startAppFast("com.android.settings"); + + mLauncher.getBackground().switchToOverview(); + } + + private void startAppFast(String packageName) { + final Instrumentation instrumentation = getInstrumentation(); + final Intent intent = instrumentation.getContext().getPackageManager(). + getLaunchIntentForPackage(packageName); + intent.addCategory(Intent.CATEGORY_LAUNCHER); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + instrumentation.getTargetContext().startActivity(intent); + assertTrue(packageName + " didn't start", + mDevice.wait(Until.hasObject(By.pkg(packageName).depth(0)), WAIT_TIME_MS)); + } + +} diff --git a/quickstep/tests/src/com/android/quickstep/QuickStepOnOffRule.java b/quickstep/tests/src/com/android/quickstep/QuickStepOnOffRule.java new file mode 100644 index 0000000000..b801b4f01d --- /dev/null +++ b/quickstep/tests/src/com/android/quickstep/QuickStepOnOffRule.java @@ -0,0 +1,104 @@ +/* + * 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.quickstep; + +import static com.android.quickstep.QuickStepOnOffRule.Mode.BOTH; +import static com.android.quickstep.QuickStepOnOffRule.Mode.OFF; +import static com.android.quickstep.QuickStepOnOffRule.Mode.ON; + +import androidx.test.InstrumentationRegistry; + +import com.android.launcher3.tapl.LauncherInstrumentation; +import com.android.launcher3.tapl.TestHelpers; + +import org.junit.rules.TestRule; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.concurrent.Executor; + +/** + * Test rule that allows executing a test with Quickstep on and then Quickstep off. + * The test should be annotated with @QuickstepOnOff. + */ +public class QuickStepOnOffRule implements TestRule { + + public enum Mode { + ON, OFF, BOTH + } + + // Annotation for tests that need to be run with quickstep enabled and disabled. + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.METHOD) + public @interface QuickstepOnOff { + Mode mode() default BOTH; + } + + private final Executor mMainThreadExecutor; + private final LauncherInstrumentation mLauncher; + + public QuickStepOnOffRule(Executor mainThreadExecutor, LauncherInstrumentation launcher) { + mLauncher = launcher; + this.mMainThreadExecutor = mainThreadExecutor; + } + + @Override + public Statement apply(Statement base, Description description) { + if (TestHelpers.isInLauncherProcess() && + description.getAnnotation(QuickstepOnOff.class) != null) { + Mode mode = description.getAnnotation(QuickstepOnOff.class).mode(); + return new Statement() { + @Override + public void evaluate() throws Throwable { + try { + if (mode == ON || mode == BOTH) { + evaluateWithQuickstepOn(); + } + if (mode == OFF || mode == BOTH) { + evaluateWithQuickstepOff(); + } + } finally { + overrideSwipeUpEnabled(null); + } + } + + private void evaluateWithQuickstepOff() throws Throwable { + overrideSwipeUpEnabled(false); + base.evaluate(); + } + + private void evaluateWithQuickstepOn() throws Throwable { + overrideSwipeUpEnabled(true); + base.evaluate(); + } + + private void overrideSwipeUpEnabled(Boolean swipeUpEnabledOverride) { + mLauncher.overrideSwipeUpEnabled(swipeUpEnabledOverride); + mMainThreadExecutor.execute(() -> OverviewInteractionState.INSTANCE.get( + InstrumentationRegistry.getInstrumentation().getTargetContext()). + notifySwipeUpSettingChanged(mLauncher.isSwipeUpEnabled())); + } + }; + } else { + return base; + } + } +} diff --git a/res/drawable/qsb_host_view_focus_bg.xml b/res/drawable/qsb_host_view_focus_bg.xml new file mode 100644 index 0000000000..7300b6a6c3 --- /dev/null +++ b/res/drawable/qsb_host_view_focus_bg.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/layout/switch_preference_with_settings.xml b/res/layout/switch_preference_with_settings.xml new file mode 100644 index 0000000000..d3195610d7 --- /dev/null +++ b/res/layout/switch_preference_with_settings.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/res/values-hi/strings.xml b/res/values-hi/strings.xml index 0dbf75bfe8..ecb5fcb4a3 100644 --- a/res/values-hi/strings.xml +++ b/res/values-hi/strings.xml @@ -114,8 +114,7 @@ "आइटम यहां ले जाएं" "होम स्क्रीन में आइटम जोड़ा गया" "आइटम निकाला गया" - - + "पहले जैसा करें" "आइटम ले जाएं" "पंक्ति %1$s स्तंभ %2$s पर ले जाएं" "%1$s स्थिति पर ले जाएं" diff --git a/res/values-iw/strings.xml b/res/values-iw/strings.xml index 9aff1ffeaa..7de43887f5 100644 --- a/res/values-iw/strings.xml +++ b/res/values-iw/strings.xml @@ -65,10 +65,10 @@ "תיקיה ללא שם" "%1$s מושבתת" - לאפליקציה %1$s יש %2$d הודעות - לאפליקציה %1$s יש %2$d הודעות - לאפליקציה %1$s יש %2$d הודעות - לאפליקציה %1$s יש הודעה אחת (%2$d) + לאפליקציה %1$s יש %2$d התראות + לאפליקציה %1$s יש %2$d התראות + לאפליקציה %1$s יש %2$d התראות + לאפליקציה %1$s יש התראה אחת (%2$d) "‏דף %1$d מתוך %2$d" "‏מסך דף הבית %1$d מתוך %2$d" @@ -85,11 +85,11 @@ "הושבת על ידי מנהל המערכת שלך" "אפשרות סיבוב של מסך דף הבית" "כאשר הטלפון מסובב" - "סימני הודעות" + "סימני ההתראות" "מופעלת" "כבויה" - "נדרשת גישה להודעות" - "כדי להציג את סימני ההודעות, יש להפעיל הודעות מהאפליקציה %1$s" + "נדרשת גישה להתראות" + "כדי להציג את סימני ההתראות,יש להפעיל התראות מהאפליקציה %1$s" "שנה את ההגדרות" "הצגה של סימן ההודעות" "הוספת סמל במסך דף הבית" @@ -137,7 +137,7 @@ "קיצורי דרך" "קיצורי דרך והודעות" "סגור" - "ההודעה נסגרה" + "ההתראה נסגרה" "אישיות" "עבודה" "פרופיל עבודה" diff --git a/res/values-my/strings.xml b/res/values-my/strings.xml index f400832949..d5188c916d 100644 --- a/res/values-my/strings.xml +++ b/res/values-my/strings.xml @@ -73,7 +73,7 @@ "ပင်မမျက်နှာပြင် စာမျက်နှာသစ်" "ဖွင့်ထားသောအကန့်, %1$d နှင့် %2$d" "ဖိုင်တွဲကို ပိတ်ရန် တို့ပါ" - "အမည်ပြောင်းခြင်းကို သိမ်းဆည်းရန် တို့ပါ" + "အမည်ပြောင်းခြင်းကို သိမ်းရန် တို့ပါ" "ပိတ်ထားသောအကန့်" "ပြောင်းလဲလိုက်သော အကန့်အမည် %1$s" "အကန့်အမည်: %1$s" diff --git a/res/xml/flag_preferences.xml b/res/xml/flag_preferences.xml new file mode 100644 index 0000000000..aea1a6ab7b --- /dev/null +++ b/res/xml/flag_preferences.xml @@ -0,0 +1,24 @@ + + + + + + \ No newline at end of file diff --git a/res/xml/launcher_preferences.xml b/res/xml/launcher_preferences.xml index 3bba73a6ce..1df7c2fbaf 100644 --- a/res/xml/launcher_preferences.xml +++ b/res/xml/launcher_preferences.xml @@ -52,4 +52,10 @@ android:defaultValue="" android:persistent="false" /> + + diff --git a/src/com/android/launcher3/AutoInstallsLayout.java b/src/com/android/launcher3/AutoInstallsLayout.java index 4c34071a92..c7c1d6ac0e 100644 --- a/src/com/android/launcher3/AutoInstallsLayout.java +++ b/src/com/android/launcher3/AutoInstallsLayout.java @@ -40,6 +40,7 @@ import android.util.Patterns; import com.android.launcher3.LauncherProvider.SqlArguments; import com.android.launcher3.LauncherSettings.Favorites; import com.android.launcher3.icons.LauncherIcons; +import com.android.launcher3.util.IntArray; import com.android.launcher3.util.Thunk; import org.xmlpull.v1.XmlPullParser; @@ -163,7 +164,7 @@ public class AutoInstallsLayout { private final int mRowCount; private final int mColumnCount; - private final long[] mTemp = new long[2]; + private final int[] mTemp = new int[2]; @Thunk final ContentValues mValues; protected final String mRootTag; @@ -191,7 +192,7 @@ public class AutoInstallsLayout { /** * Loads the layout in the db and returns the number of entries added on the desktop. */ - public int loadLayout(SQLiteDatabase db, ArrayList screenIds) { + public int loadLayout(SQLiteDatabase db, IntArray screenIds) { mDb = db; try { return parseLayout(mLayoutId, screenIds); @@ -204,7 +205,7 @@ public class AutoInstallsLayout { /** * Parses the layout and returns the number of elements added on the homescreen. */ - protected int parseLayout(int layoutId, ArrayList screenIds) + protected int parseLayout(int layoutId, IntArray screenIds) throws XmlPullParserException, IOException { XmlResourceParser parser = mSourceRes.getXml(layoutId); beginDocument(parser, mRootTag); @@ -227,14 +228,14 @@ public class AutoInstallsLayout { * Parses container and screenId attribute from the current tag, and puts it in the out. * @param out array of size 2. */ - protected void parseContainerAndScreen(XmlResourceParser parser, long[] out) { + protected void parseContainerAndScreen(XmlResourceParser parser, int[] out) { if (HOTSEAT_CONTAINER_NAME.equals(getAttributeValue(parser, ATTR_CONTAINER))) { out[0] = Favorites.CONTAINER_HOTSEAT; // Hack: hotseat items are stored using screen ids - out[1] = Long.parseLong(getAttributeValue(parser, ATTR_RANK)); + out[1] = Integer.parseInt(getAttributeValue(parser, ATTR_RANK)); } else { out[0] = Favorites.CONTAINER_DESKTOP; - out[1] = Long.parseLong(getAttributeValue(parser, ATTR_SCREEN)); + out[1] = Integer.parseInt(getAttributeValue(parser, ATTR_SCREEN)); } } @@ -242,9 +243,7 @@ public class AutoInstallsLayout { * Parses the current node and returns the number of elements added. */ protected int parseAndAddNode( - XmlResourceParser parser, - ArrayMap tagParserMap, - ArrayList screenIds) + XmlResourceParser parser, ArrayMap tagParserMap, IntArray screenIds) throws XmlPullParserException, IOException { if (TAG_INCLUDE.equals(parser.getName())) { @@ -259,8 +258,8 @@ public class AutoInstallsLayout { mValues.clear(); parseContainerAndScreen(parser, mTemp); - final long container = mTemp[0]; - final long screenId = mTemp[1]; + final int container = mTemp[0]; + final int screenId = mTemp[1]; mValues.put(Favorites.CONTAINER, container); mValues.put(Favorites.SCREEN, screenId); @@ -275,7 +274,7 @@ public class AutoInstallsLayout { if (LOGD) Log.d(TAG, "Ignoring unknown element tag: " + parser.getName()); return 0; } - long newElementId = tagParser.parseAndAdd(parser); + int newElementId = tagParser.parseAndAdd(parser); if (newElementId >= 0) { // Keep track of the set of screens which need to be added to the db. if (!screenIds.contains(screenId) && @@ -287,8 +286,8 @@ public class AutoInstallsLayout { return 0; } - protected long addShortcut(String title, Intent intent, int type) { - long id = mCallback.generateNewItemId(); + protected int addShortcut(String title, Intent intent, int type) { + int id = mCallback.generateNewItemId(); mValues.put(Favorites.INTENT, intent.toUri(0)); mValues.put(Favorites.TITLE, title); mValues.put(Favorites.ITEM_TYPE, type); @@ -325,7 +324,7 @@ public class AutoInstallsLayout { * Parses the tag and adds to the db * @return the id of the row added or -1; */ - long parseAndAdd(XmlResourceParser parser) + int parseAndAdd(XmlResourceParser parser) throws XmlPullParserException, IOException; } @@ -335,7 +334,7 @@ public class AutoInstallsLayout { protected class AppShortcutParser implements TagParser { @Override - public long parseAndAdd(XmlResourceParser parser) { + public int parseAndAdd(XmlResourceParser parser) { final String packageName = getAttributeValue(parser, ATTR_PACKAGE_NAME); final String className = getAttributeValue(parser, ATTR_CLASS_NAME); @@ -372,7 +371,7 @@ public class AutoInstallsLayout { /** * Helper method to allow extending the parser capabilities */ - protected long invalidPackageOrClass(XmlResourceParser parser) { + protected int invalidPackageOrClass(XmlResourceParser parser) { Log.w(TAG, "Skipping invalid with no component"); return -1; } @@ -384,7 +383,7 @@ public class AutoInstallsLayout { protected class AutoInstallParser implements TagParser { @Override - public long parseAndAdd(XmlResourceParser parser) { + public int parseAndAdd(XmlResourceParser parser) { final String packageName = getAttributeValue(parser, ATTR_PACKAGE_NAME); final String className = getAttributeValue(parser, ATTR_CLASS_NAME); if (TextUtils.isEmpty(packageName) || TextUtils.isEmpty(className)) { @@ -415,7 +414,7 @@ public class AutoInstallsLayout { } @Override - public long parseAndAdd(XmlResourceParser parser) { + public int parseAndAdd(XmlResourceParser parser) { final int titleResId = getAttributeResourceValue(parser, ATTR_TITLE, 0); final int iconId = getAttributeResourceValue(parser, ATTR_ICON, 0); @@ -471,7 +470,7 @@ public class AutoInstallsLayout { protected class PendingWidgetParser implements TagParser { @Override - public long parseAndAdd(XmlResourceParser parser) + public int parseAndAdd(XmlResourceParser parser) throws XmlPullParserException, IOException { final String packageName = getAttributeValue(parser, ATTR_PACKAGE_NAME); final String className = getAttributeValue(parser, ATTR_CLASS_NAME); @@ -510,7 +509,7 @@ public class AutoInstallsLayout { return verifyAndInsert(new ComponentName(packageName, className), extras); } - protected long verifyAndInsert(ComponentName cn, Bundle extras) { + protected int verifyAndInsert(ComponentName cn, Bundle extras) { mValues.put(Favorites.APPWIDGET_PROVIDER, cn.flattenToString()); mValues.put(Favorites.RESTORED, LauncherAppWidgetInfo.FLAG_ID_NOT_VALID | @@ -521,7 +520,7 @@ public class AutoInstallsLayout { mValues.put(Favorites.INTENT, new Intent().putExtras(extras).toUri(0)); } - long insertedId = mCallback.insertAndCheck(mDb, mValues); + int insertedId = mCallback.insertAndCheck(mDb, mValues); if (insertedId < 0) { return -1; } else { @@ -542,7 +541,7 @@ public class AutoInstallsLayout { } @Override - public long parseAndAdd(XmlResourceParser parser) + public int parseAndAdd(XmlResourceParser parser) throws XmlPullParserException, IOException { final String title; final int titleResId = getAttributeResourceValue(parser, ATTR_TITLE, 0); @@ -557,14 +556,14 @@ public class AutoInstallsLayout { mValues.put(Favorites.SPANX, 1); mValues.put(Favorites.SPANY, 1); mValues.put(Favorites._ID, mCallback.generateNewItemId()); - long folderId = mCallback.insertAndCheck(mDb, mValues); + int folderId = mCallback.insertAndCheck(mDb, mValues); if (folderId < 0) { if (LOGD) Log.e(TAG, "Unable to add folder"); return -1; } final ContentValues myValues = new ContentValues(mValues); - ArrayList folderItems = new ArrayList<>(); + IntArray folderItems = new IntArray(); int type; int folderDepth = parser.getDepth(); @@ -580,7 +579,7 @@ public class AutoInstallsLayout { TagParser tagParser = mFolderElements.get(parser.getName()); if (tagParser != null) { - final long id = tagParser.parseAndAdd(parser); + final int id = tagParser.parseAndAdd(parser); if (id >= 0) { folderItems.add(id); rank++; @@ -590,7 +589,7 @@ public class AutoInstallsLayout { } } - long addedId = folderId; + int addedId = folderId; // We can only have folders with >= 2 items, so we need to remove the // folder and clean up if less than 2 items were included, or some @@ -675,9 +674,9 @@ public class AutoInstallsLayout { } public interface LayoutParserCallback { - long generateNewItemId(); + int generateNewItemId(); - long insertAndCheck(SQLiteDatabase db, ContentValues values); + int insertAndCheck(SQLiteDatabase db, ContentValues values); } @Thunk static void copyInteger(ContentValues from, ContentValues to, String key) { diff --git a/src/com/android/launcher3/CellLayout.java b/src/com/android/launcher3/CellLayout.java index a42238e5b2..92404d4cd3 100644 --- a/src/com/android/launcher3/CellLayout.java +++ b/src/com/android/launcher3/CellLayout.java @@ -2063,7 +2063,7 @@ public class CellLayout extends ViewGroup { private void commitTempPlacement() { mTmpOccupied.copyTo(mOccupied); - long screenId = mLauncher.getWorkspace().getIdForScreen(this); + int screenId = mLauncher.getWorkspace().getIdForScreen(this); int container = Favorites.CONTAINER_DESKTOP; if (mContainerType == HOTSEAT) { @@ -2688,8 +2688,8 @@ public class CellLayout extends ViewGroup { // the CellLayout that was long clicked public static final class CellInfo extends CellAndSpan { public final View cell; - final long screenId; - final long container; + final int screenId; + final int container; public CellInfo(View v, ItemInfo info) { cellX = info.cellX; diff --git a/src/com/android/launcher3/DefaultLayoutParser.java b/src/com/android/launcher3/DefaultLayoutParser.java index 1ec30ba68d..44830e8fee 100644 --- a/src/com/android/launcher3/DefaultLayoutParser.java +++ b/src/com/android/launcher3/DefaultLayoutParser.java @@ -76,13 +76,13 @@ public class DefaultLayoutParser extends AutoInstallsLayout { } @Override - protected void parseContainerAndScreen(XmlResourceParser parser, long[] out) { + protected void parseContainerAndScreen(XmlResourceParser parser, int[] out) { out[0] = LauncherSettings.Favorites.CONTAINER_DESKTOP; String strContainer = getAttributeValue(parser, ATTR_CONTAINER); if (strContainer != null) { - out[0] = Long.valueOf(strContainer); + out[0] = Integer.parseInt(strContainer); } - out[1] = Long.parseLong(getAttributeValue(parser, ATTR_SCREEN)); + out[1] = Integer.parseInt(getAttributeValue(parser, ATTR_SCREEN)); } /** @@ -91,7 +91,7 @@ public class DefaultLayoutParser extends AutoInstallsLayout { public class AppShortcutWithUriParser extends AppShortcutParser { @Override - protected long invalidPackageOrClass(XmlResourceParser parser) { + protected int invalidPackageOrClass(XmlResourceParser parser) { final String uri = getAttributeValue(parser, ATTR_URI); if (TextUtils.isEmpty(uri)) { Log.e(TAG, "Skipping invalid with no component or uri"); @@ -205,11 +205,11 @@ public class DefaultLayoutParser extends AutoInstallsLayout { private final AppShortcutWithUriParser mChildParser = new AppShortcutWithUriParser(); @Override - public long parseAndAdd(XmlResourceParser parser) throws XmlPullParserException, + public int parseAndAdd(XmlResourceParser parser) throws XmlPullParserException, IOException { final int groupDepth = parser.getDepth(); int type; - long addedId = -1; + int addedId = -1; while ((type = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > groupDepth) { if (type != XmlPullParser.START_TAG || addedId > -1) { @@ -233,7 +233,7 @@ public class DefaultLayoutParser extends AutoInstallsLayout { @Thunk class PartnerFolderParser implements TagParser { @Override - public long parseAndAdd(XmlResourceParser parser) throws XmlPullParserException, + public int parseAndAdd(XmlResourceParser parser) throws XmlPullParserException, IOException { // Folder contents come from an external XML resource final Partner partner = Partner.get(mPackageManager); @@ -259,7 +259,7 @@ public class DefaultLayoutParser extends AutoInstallsLayout { @Thunk class MyFolderParser extends FolderParser { @Override - public long parseAndAdd(XmlResourceParser parser) throws XmlPullParserException, + public int parseAndAdd(XmlResourceParser parser) throws XmlPullParserException, IOException { final int resId = getAttributeResourceValue(parser, ATTR_FOLDER_ITEMS, 0); if (resId != 0) { @@ -277,7 +277,7 @@ public class DefaultLayoutParser extends AutoInstallsLayout { protected class AppWidgetParser extends PendingWidgetParser { @Override - protected long verifyAndInsert(ComponentName cn, Bundle extras) { + protected int verifyAndInsert(ComponentName cn, Bundle extras) { try { mPackageManager.getReceiverInfo(cn, 0); } catch (Exception e) { @@ -293,7 +293,7 @@ public class DefaultLayoutParser extends AutoInstallsLayout { } final AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(mContext); - long insertedId = -1; + int insertedId = -1; try { int appWidgetId = mAppWidgetHost.allocateAppWidgetId(); diff --git a/src/com/android/launcher3/ItemInfo.java b/src/com/android/launcher3/ItemInfo.java index fa3253c67a..bffdd724b7 100644 --- a/src/com/android/launcher3/ItemInfo.java +++ b/src/com/android/launcher3/ItemInfo.java @@ -34,7 +34,7 @@ public class ItemInfo { /** * The id in the settings database for this item */ - public long id = NO_ID; + public int id = NO_ID; /** * One of {@link LauncherSettings.Favorites#ITEM_TYPE_APPLICATION}, @@ -52,14 +52,14 @@ public class ItemInfo { * will be {@link #NO_ID} (since it is not stored in the settings DB). For user folders * it will be the id of the folder. */ - public long container = NO_ID; + public int container = NO_ID; /** * Indicates the screen in which the shortcut appears if the container types is * {@link LauncherSettings.Favorites#CONTAINER_DESKTOP}. (i.e., ignore if the container type is * {@link LauncherSettings.Favorites#CONTAINER_HOTSEAT}) */ - public long screenId = -1; + public int screenId = -1; /** * Indicates the X position of the associated cell. @@ -158,8 +158,8 @@ public class ItemInfo { public void readFromValues(ContentValues values) { itemType = values.getAsInteger(LauncherSettings.Favorites.ITEM_TYPE); - container = values.getAsLong(LauncherSettings.Favorites.CONTAINER); - screenId = values.getAsLong(LauncherSettings.Favorites.SCREEN); + container = values.getAsInteger(LauncherSettings.Favorites.CONTAINER); + screenId = values.getAsInteger(LauncherSettings.Favorites.SCREEN); cellX = values.getAsInteger(LauncherSettings.Favorites.CELLX); cellY = values.getAsInteger(LauncherSettings.Favorites.CELLY); spanX = values.getAsInteger(LauncherSettings.Favorites.SPANX); diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java index bbe5b78f4c..0395fbbcc7 100644 --- a/src/com/android/launcher3/Launcher.java +++ b/src/com/android/launcher3/Launcher.java @@ -108,6 +108,7 @@ import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType; import com.android.launcher3.userevent.nano.LauncherLogProto.Target; import com.android.launcher3.util.ActivityResultInfo; import com.android.launcher3.util.ComponentKey; +import com.android.launcher3.util.IntArray; import com.android.launcher3.util.ItemInfoMatcher; import com.android.launcher3.util.MultiHashMap; import com.android.launcher3.util.MultiValueAlpha; @@ -490,9 +491,9 @@ public class Launcher extends BaseDraggingActivity implements LauncherExterns, * Returns whether we should delay spring loaded mode -- for shortcuts and widgets that have * a configuration step, this allows the proper animations to run after other transitions. */ - private long completeAdd( + private int completeAdd( int requestCode, Intent intent, int appWidgetId, PendingRequestArgs info) { - long screenId = info.screenId; + int screenId = info.screenId; if (info.container == LauncherSettings.Favorites.CONTAINER_DESKTOP) { // When the screen id represents an actual screen (as opposed to a rank) we make sure // that the drop page actually exists. @@ -694,7 +695,7 @@ public class Launcher extends BaseDraggingActivity implements LauncherExterns, * @param screenId the screen id to check * @return the new screen, or screenId if it exists */ - private long ensurePendingDropLayoutExists(long screenId) { + private int ensurePendingDropLayoutExists(int screenId) { CellLayout dropLayout = mWorkspace.getScreenWithId(screenId); if (dropLayout == null) { // it's possible that the add screen was removed because it was @@ -974,7 +975,7 @@ public class Launcher extends BaseDraggingActivity implements LauncherExterns, * * @param data The intent describing the shortcut. */ - private void completeAddShortcut(Intent data, long container, long screenId, int cellX, + private void completeAddShortcut(Intent data, int container, int screenId, int cellX, int cellY, PendingRequestArgs args) { if (args.getRequestCode() != REQUEST_CREATE_SHORTCUT || args.getPendingIntent().getComponent() == null) { @@ -1050,7 +1051,7 @@ public class Launcher extends BaseDraggingActivity implements LauncherExterns, } } - public FolderIcon findFolderIcon(final long folderIconId) { + public FolderIcon findFolderIcon(final int folderIconId) { return (FolderIcon) mWorkspace.getHomescreenIconByItemId(folderIconId); } @@ -1413,7 +1414,7 @@ public class Launcher extends BaseDraggingActivity implements LauncherExterns, } } - public void addPendingItem(PendingAddItemInfo info, long container, long screenId, + public void addPendingItem(PendingAddItemInfo info, int container, int screenId, int[] cell, int spanX, int spanY) { info.container = container; info.screenId = screenId; @@ -1489,7 +1490,7 @@ public class Launcher extends BaseDraggingActivity implements LauncherExterns, } } - FolderIcon addFolder(CellLayout layout, long container, final long screenId, int cellX, + FolderIcon addFolder(CellLayout layout, int container, final int screenId, int cellX, int cellY) { final FolderInfo folderInfo = new FolderInfo(); folderInfo.title = getText(R.string.folder_name); @@ -1650,7 +1651,7 @@ public class Launcher extends BaseDraggingActivity implements LauncherExterns, /** * Returns the CellLayout of the specified container at the specified screen. */ - public CellLayout getCellLayout(long container, long screenId) { + public CellLayout getCellLayout(int container, int screenId) { if (container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) { if (mHotseat != null) { return mHotseat.getLayout(); @@ -1750,14 +1751,15 @@ public class Launcher extends BaseDraggingActivity implements LauncherExterns, } @Override - public void bindScreens(ArrayList orderedScreenIds) { + public void bindScreens(IntArray orderedScreenIds) { // Make sure the first screen is always at the start. - if (FeatureFlags.QSB_ON_FIRST_SCREEN && + if (FeatureFlags.QSB_ON_FIRST_SCREEN.get() && orderedScreenIds.indexOf(Workspace.FIRST_SCREEN_ID) != 0) { - orderedScreenIds.remove(Workspace.FIRST_SCREEN_ID); + orderedScreenIds.removeValue(Workspace.FIRST_SCREEN_ID); orderedScreenIds.add(0, Workspace.FIRST_SCREEN_ID); LauncherModel.updateWorkspaceScreenOrder(this, orderedScreenIds); - } else if (!FeatureFlags.QSB_ON_FIRST_SCREEN && orderedScreenIds.isEmpty()) { + } else if (!FeatureFlags.QSB_ON_FIRST_SCREEN.get() + && orderedScreenIds.isEmpty()) { // If there are no screens, we need to have an empty screen mWorkspace.addExtraEmptyScreen(); } @@ -1769,11 +1771,11 @@ public class Launcher extends BaseDraggingActivity implements LauncherExterns, mWorkspace.unlockWallpaperFromDefaultPageOnNextLayout(); } - private void bindAddScreens(ArrayList orderedScreenIds) { + private void bindAddScreens(IntArray orderedScreenIds) { int count = orderedScreenIds.size(); for (int i = 0; i < count; i++) { - long screenId = orderedScreenIds.get(i); - if (!FeatureFlags.QSB_ON_FIRST_SCREEN || screenId != Workspace.FIRST_SCREEN_ID) { + int screenId = orderedScreenIds.get(i); + 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); } @@ -1792,7 +1794,7 @@ public class Launcher extends BaseDraggingActivity implements LauncherExterns, } @Override - public void bindAppsAdded(ArrayList newScreens, ArrayList addNotAnimated, + public void bindAppsAdded(IntArray newScreens, ArrayList addNotAnimated, ArrayList addAnimated) { // Add the new screens if (newScreens != null) { @@ -1823,7 +1825,7 @@ public class Launcher extends BaseDraggingActivity implements LauncherExterns, final Collection bounceAnims = new ArrayList<>(); final boolean animateIcons = forceAnimateIcons && canRunNewAppsAnimation(); Workspace workspace = mWorkspace; - long newItemsScreenId = -1; + int newItemsScreenId = -1; int end = items.size(); for (int i = 0; i < end; i++) { final ItemInfo item = items.get(i); @@ -1896,7 +1898,7 @@ public class Launcher extends BaseDraggingActivity implements LauncherExterns, AnimatorSet anim = new AnimatorSet(); anim.playTogether(bounceAnims); - long currentScreenId = mWorkspace.getScreenIdForPageIndex(mWorkspace.getNextPage()); + int currentScreenId = mWorkspace.getScreenIdForPageIndex(mWorkspace.getNextPage()); final int newScreenIndex = mWorkspace.getPageIndexForScreenId(newItemsScreenId); final Runnable startBounceAnimRunnable = anim::start; @@ -2283,6 +2285,8 @@ public class Launcher extends BaseDraggingActivity implements LauncherExterns, writer.print(" mPendingRequestArgs=" + mPendingRequestArgs); writer.println(" mPendingActivityResult=" + mPendingActivityResult); writer.println(" mRotationHelper: " + mRotationHelper); + // Extra logging for b/116853349 + mDragLayer.dumpAlpha(writer); dumpMisc(writer); try { diff --git a/src/com/android/launcher3/LauncherAppState.java b/src/com/android/launcher3/LauncherAppState.java index b02182c571..6bf581277e 100644 --- a/src/com/android/launcher3/LauncherAppState.java +++ b/src/com/android/launcher3/LauncherAppState.java @@ -16,7 +16,7 @@ package com.android.launcher3; -import static com.android.launcher3.SettingsActivity.NOTIFICATION_BADGING; +import static com.android.launcher3.util.SecureSettingsObserver.newNotificationSettingsObserver; import android.content.ComponentName; import android.content.ContentProviderClient; @@ -33,7 +33,7 @@ import com.android.launcher3.icons.IconCache; import com.android.launcher3.notification.NotificationListener; import com.android.launcher3.util.MainThreadInitializedObject; import com.android.launcher3.util.Preconditions; -import com.android.launcher3.util.SettingsObserver; +import com.android.launcher3.util.SecureSettingsObserver; public class LauncherAppState { @@ -48,7 +48,7 @@ public class LauncherAppState { private final IconCache mIconCache; private final WidgetPreviewLoader mWidgetCache; private final InvariantDeviceProfile mInvariantDeviceProfile; - private final SettingsObserver mNotificationBadgingObserver; + private final SecureSettingsObserver mNotificationBadgingObserver; public static LauncherAppState getInstance(final Context context) { return INSTANCE.get(context); @@ -99,17 +99,17 @@ public class LauncherAppState { mNotificationBadgingObserver = null; } else { // Register an observer to rebind the notification listener when badging is re-enabled. - mNotificationBadgingObserver = new SettingsObserver.Secure( - mContext.getContentResolver()) { - @Override - public void onSettingChanged(boolean isNotificationBadgingEnabled) { - if (isNotificationBadgingEnabled) { - NotificationListener.requestRebind(new ComponentName( - mContext, NotificationListener.class)); - } - } - }; - mNotificationBadgingObserver.register(NOTIFICATION_BADGING); + mNotificationBadgingObserver = + newNotificationSettingsObserver(mContext, this::onNotificationSettingsChanged); + mNotificationBadgingObserver.register(); + mNotificationBadgingObserver.dispatchOnChange(); + } + } + + protected void onNotificationSettingsChanged(boolean isNotificationBadgingEnabled) { + if (isNotificationBadgingEnabled) { + NotificationListener.requestRebind(new ComponentName( + mContext, NotificationListener.class)); } } diff --git a/src/com/android/launcher3/LauncherModel.java b/src/com/android/launcher3/LauncherModel.java index 5e09f8adc5..cbfde25596 100644 --- a/src/com/android/launcher3/LauncherModel.java +++ b/src/com/android/launcher3/LauncherModel.java @@ -55,6 +55,7 @@ import com.android.launcher3.provider.LauncherDbUtils; import com.android.launcher3.shortcuts.DeepShortcutManager; import com.android.launcher3.shortcuts.ShortcutInfoCompat; import com.android.launcher3.util.ComponentKey; +import com.android.launcher3.util.IntArray; import com.android.launcher3.util.ItemInfoMatcher; import com.android.launcher3.util.MultiHashMap; import com.android.launcher3.util.PackageUserKey; @@ -143,15 +144,14 @@ public class LauncherModel extends BroadcastReceiver public void clearPendingBinds(); public void startBinding(); public void bindItems(List shortcuts, boolean forceAnimateIcons); - public void bindScreens(ArrayList orderedScreenIds); + public void bindScreens(IntArray orderedScreenIds); public void finishFirstPageBind(ViewOnDrawExecutor executor); public void finishBindingItems(int currentScreen); public void bindAllApplications(ArrayList apps); public void bindAppsAddedOrUpdated(ArrayList apps); public void preAddApps(); - public void bindAppsAdded(ArrayList newScreens, - ArrayList addNotAnimated, - ArrayList addAnimated); + public void bindAppsAdded(IntArray newScreens, + ArrayList addNotAnimated, ArrayList addAnimated); public void bindPromiseAppProgressUpdated(PromiseAppInfo app); public void bindShortcutsChanged(ArrayList updated, UserHandle user); public void bindWidgetsRestored(ArrayList widgets); @@ -211,7 +211,12 @@ public class LauncherModel extends BroadcastReceiver } static void checkItemInfoLocked( - final long itemId, final ItemInfo item, StackTraceElement[] stackTrace) { + final int itemId, final ItemInfo item, StackTraceElement[] stackTrace) { + if (com.android.launcher3.Utilities.IS_RUNNING_IN_TEST_HARNESS + && com.android.launcher3.Utilities.IS_DEBUG_DEVICE) { + android.util.Log.d("b/117332845", + "Checking item: " + android.util.Log.getStackTraceString(new Throwable())); + } ItemInfo modelItem = sBgDataModel.itemsIdMap.get(itemId); if (modelItem != null && item != modelItem) { // check all the data is consistent @@ -250,7 +255,7 @@ public class LauncherModel extends BroadcastReceiver static void checkItemInfo(final ItemInfo item) { final StackTraceElement[] stackTrace = new Throwable().getStackTrace(); - final long itemId = item.id; + final int itemId = item.id; Runnable r = new Runnable() { public void run() { synchronized (sBgDataModel) { @@ -265,17 +270,16 @@ public class LauncherModel extends BroadcastReceiver * Update the order of the workspace screens in the database. The array list contains * a list of screen ids in the order that they should appear. */ - public static void updateWorkspaceScreenOrder(Context context, final ArrayList screens) { - final ArrayList screensCopy = new ArrayList(screens); + public static void updateWorkspaceScreenOrder(Context context, IntArray screens) { final ContentResolver cr = context.getContentResolver(); final Uri uri = LauncherSettings.WorkspaceScreens.CONTENT_URI; - // Remove any negative screen ids -- these aren't persisted - Iterator iter = screensCopy.iterator(); - while (iter.hasNext()) { - long id = iter.next(); - if (id < 0) { - iter.remove(); + // Create a copy with only non-negative values + final IntArray screensCopy = new IntArray(); + for (int i = 0; i < screens.size(); i++) { + int id = screens.get(i); + if (id >= 0) { + screensCopy.add(id); } } @@ -288,7 +292,7 @@ public class LauncherModel extends BroadcastReceiver int count = screensCopy.size(); for (int i = 0; i < count; i++) { ContentValues v = new ContentValues(); - long screenId = screensCopy.get(i); + int screenId = screensCopy.get(i); v.put(LauncherSettings.WorkspaceScreens._ID, screenId); v.put(LauncherSettings.WorkspaceScreens.SCREEN_RANK, i); ops.add(ContentProviderOperation.newInsert(uri).withValues(v).build()); @@ -447,6 +451,11 @@ public class LauncherModel extends BroadcastReceiver * @return true if the page could be bound synchronously. */ public boolean startLoader(int synchronousBindPage) { + if (com.android.launcher3.Utilities.IS_RUNNING_IN_TEST_HARNESS + && com.android.launcher3.Utilities.IS_DEBUG_DEVICE) { + android.util.Log.d("b/117332845", + android.util.Log.getStackTraceString(new Throwable())); + } // Enable queue before starting loader. It will get disabled in Launcher#finishBindingItems InstallShortcutReceiver.enableInstallQueue(InstallShortcutReceiver.FLAG_LOADER_RUNNING); synchronized (mLock) { @@ -511,7 +520,7 @@ public class LauncherModel extends BroadcastReceiver /** * Loads the workspace screen ids in an ordered list. */ - public static ArrayList loadWorkspaceScreensDb(Context context) { + public static IntArray loadWorkspaceScreensDb(Context context) { final ContentResolver contentResolver = context.getContentResolver(); final Uri screensUri = LauncherSettings.WorkspaceScreens.CONTENT_URI; @@ -558,6 +567,11 @@ public class LauncherModel extends BroadcastReceiver synchronized (mLock) { // Everything loaded bind the data. mModelLoaded = true; + if (com.android.launcher3.Utilities.IS_RUNNING_IN_TEST_HARNESS + && com.android.launcher3.Utilities.IS_DEBUG_DEVICE) { + android.util.Log.d("b/117332845", + android.util.Log.getStackTraceString(new Throwable())); + } } } diff --git a/src/com/android/launcher3/LauncherProvider.java b/src/com/android/launcher3/LauncherProvider.java index 7d208d48d3..7d62adaf46 100644 --- a/src/com/android/launcher3/LauncherProvider.java +++ b/src/com/android/launcher3/LauncherProvider.java @@ -58,6 +58,8 @@ import com.android.launcher3.model.DbDowngradeHelper; import com.android.launcher3.provider.LauncherDbUtils; import com.android.launcher3.provider.LauncherDbUtils.SQLiteTransaction; import com.android.launcher3.provider.RestoreDbTask; +import com.android.launcher3.util.IntArray; +import com.android.launcher3.util.IntSet; import com.android.launcher3.util.NoLocaleSQLiteHelper; import com.android.launcher3.util.Preconditions; import com.android.launcher3.util.Thunk; @@ -67,9 +69,7 @@ import java.io.FileDescriptor; import java.io.PrintWriter; import java.net.URISyntaxException; import java.util.ArrayList; -import java.util.Collections; -import java.util.HashSet; -import java.util.LinkedHashSet; +import java.util.Arrays; public class LauncherProvider extends ContentProvider { private static final String TAG = "LauncherProvider"; @@ -170,7 +170,7 @@ public class LauncherProvider extends ContentProvider { return result; } - @Thunk static long dbInsertAndCheck(DatabaseHelper helper, + @Thunk static int dbInsertAndCheck(DatabaseHelper helper, SQLiteDatabase db, String table, String nullColumnHack, ContentValues values) { if (values == null) { throw new RuntimeException("Error: attempting to insert null values"); @@ -179,7 +179,7 @@ public class LauncherProvider extends ContentProvider { throw new RuntimeException("Error: attempting to add item without specifying an id"); } helper.checkId(table, values); - return db.insert(table, nullColumnHack, values); + return (int) db.insert(table, nullColumnHack, values); } private void reloadLauncherIfExternal() { @@ -205,7 +205,7 @@ public class LauncherProvider extends ContentProvider { SQLiteDatabase db = mOpenHelper.getWritableDatabase(); addModifiedTime(initialValues); - final long rowId = dbInsertAndCheck(mOpenHelper, db, args.table, null, initialValues); + final int rowId = dbInsertAndCheck(mOpenHelper, db, args.table, null, initialValues); if (rowId < 0) return null; uri = ContentUris.withAppendedId(uri, rowId); @@ -230,7 +230,7 @@ public class LauncherProvider extends ContentProvider { private boolean initializeExternalAdd(ContentValues values) { // 1. Ensure that externally added items have a valid item id - long id = mOpenHelper.generateNewItemId(); + int id = mOpenHelper.generateNewItemId(); values.put(LauncherSettings.Favorites._ID, id); // 2. In the case of an app widget, and if no app widget id is specified, we @@ -263,7 +263,7 @@ public class LauncherProvider extends ContentProvider { } // Add screen id if not present - long screenId = values.getAsLong(LauncherSettings.Favorites.SCREEN); + int screenId = values.getAsInteger(LauncherSettings.Favorites.SCREEN); SQLiteStatement stmp = null; try { stmp = mOpenHelper.getWritableDatabase().compileStatement( @@ -369,17 +369,18 @@ public class LauncherProvider extends ContentProvider { } case LauncherSettings.Settings.METHOD_DELETE_EMPTY_FOLDERS: { Bundle result = new Bundle(); - result.putSerializable(LauncherSettings.Settings.EXTRA_VALUE, deleteEmptyFolders()); + result.putIntArray(LauncherSettings.Settings.EXTRA_VALUE, deleteEmptyFolders() + .toArray()); return result; } case LauncherSettings.Settings.METHOD_NEW_ITEM_ID: { Bundle result = new Bundle(); - result.putLong(LauncherSettings.Settings.EXTRA_VALUE, mOpenHelper.generateNewItemId()); + result.putInt(LauncherSettings.Settings.EXTRA_VALUE, mOpenHelper.generateNewItemId()); return result; } case LauncherSettings.Settings.METHOD_NEW_SCREEN_ID: { Bundle result = new Bundle(); - result.putLong(LauncherSettings.Settings.EXTRA_VALUE, mOpenHelper.generateNewScreenId()); + result.putInt(LauncherSettings.Settings.EXTRA_VALUE, mOpenHelper.generateNewScreenId()); return result; } case LauncherSettings.Settings.METHOD_CREATE_EMPTY_DB: { @@ -402,8 +403,8 @@ public class LauncherProvider extends ContentProvider { * Deletes any empty folder from the DB. * @return Ids of deleted folders. */ - private ArrayList deleteEmptyFolders() { - ArrayList folderIds = new ArrayList<>(); + private IntArray deleteEmptyFolders() { + IntArray folderIds = new IntArray(); SQLiteDatabase db = mOpenHelper.getWritableDatabase(); try (SQLiteTransaction t = new SQLiteTransaction(db)) { // Select folders whose id do not match any container value. @@ -542,8 +543,8 @@ public class LauncherProvider extends ContentProvider { public static class DatabaseHelper extends NoLocaleSQLiteHelper implements LayoutParserCallback { private final Handler mWidgetHostResetHandler; private final Context mContext; - private long mMaxItemId = -1; - private long mMaxScreenId = -1; + private int mMaxItemId = -1; + private int mMaxScreenId = -1; DatabaseHelper(Context context, Handler widgetHostResetHandler) { this(context, widgetHostResetHandler, LauncherFiles.LAUNCHER_DB); @@ -789,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.QSB_ON_FIRST_SCREEN && + if (FeatureFlags.QSB_ON_FIRST_SCREEN.get() && !LauncherDbUtils.prepareScreenZeroToHostQsb(mContext, db)) { break; } @@ -844,7 +845,7 @@ public class LauncherProvider extends ContentProvider { Log.e(TAG, "getAppWidgetIds not supported", e); return; } - final HashSet validWidgets = new HashSet<>(); + final IntSet validWidgets = new IntSet(); try (Cursor c = db.query(Favorites.TABLE_NAME, new String[] {Favorites.APPWIDGET_ID }, "itemType=" + Favorites.ITEM_TYPE_APPWIDGET, null, null, null, null)) { @@ -899,7 +900,7 @@ public class LauncherProvider extends ContentProvider { continue; } - long id = c.getLong(idIndex); + int id = c.getInt(idIndex); updateStmt.bindLong(1, id); updateStmt.executeUpdateDelete(); } @@ -914,15 +915,14 @@ public class LauncherProvider extends ContentProvider { */ public boolean recreateWorkspaceTable(SQLiteDatabase db) { try (SQLiteTransaction t = new SQLiteTransaction(db)) { - final ArrayList sortedIDs; + final IntArray sortedIDs; try (Cursor c = db.query(WorkspaceScreens.TABLE_NAME, new String[] {LauncherSettings.WorkspaceScreens._ID}, null, null, null, null, LauncherSettings.WorkspaceScreens.SCREEN_RANK)) { // Use LinkedHashSet so that ordering is preserved - sortedIDs = new ArrayList<>( - LauncherDbUtils.iterateCursor(c, 0, new LinkedHashSet())); + sortedIDs = LauncherDbUtils.getScreenIdsFromCursor(c); } db.execSQL("DROP TABLE IF EXISTS " + WorkspaceScreens.TABLE_NAME); addWorkspacesTable(db, false); @@ -937,7 +937,11 @@ public class LauncherProvider extends ContentProvider { db.insertOrThrow(WorkspaceScreens.TABLE_NAME, null, values); } t.commit(); - mMaxScreenId = sortedIDs.isEmpty() ? 0 : Collections.max(sortedIDs); + + mMaxScreenId = 0; + for (int i = 0; i < sortedIDs.size(); i++) { + mMaxScreenId = Math.max(mMaxScreenId, sortedIDs.get(i)); + } } catch (SQLException ex) { // Old version remains, which means we wipe old data Log.e(TAG, ex.getMessage(), ex); @@ -997,7 +1001,7 @@ public class LauncherProvider extends ContentProvider { // constructor is called, and we only pass a reference to LauncherProvider to LauncherApp // after that point @Override - public long generateNewItemId() { + public int generateNewItemId() { if (mMaxItemId < 0) { throw new RuntimeException("Error: max item id was not initialized"); } @@ -1010,12 +1014,12 @@ public class LauncherProvider extends ContentProvider { } @Override - public long insertAndCheck(SQLiteDatabase db, ContentValues values) { + public int insertAndCheck(SQLiteDatabase db, ContentValues values) { return dbInsertAndCheck(this, db, Favorites.TABLE_NAME, null, values); } public void checkId(String table, ContentValues values) { - long id = values.getAsLong(LauncherSettings.BaseLauncherColumns._ID); + int id = values.getAsInteger(LauncherSettings.BaseLauncherColumns._ID); if (WorkspaceScreens.TABLE_NAME.equals(table)) { mMaxScreenId = Math.max(id, mMaxScreenId); } else { @@ -1023,7 +1027,7 @@ public class LauncherProvider extends ContentProvider { } } - private long initializeMaxItemId(SQLiteDatabase db) { + private int initializeMaxItemId(SQLiteDatabase db) { return getMaxId(db, Favorites.TABLE_NAME); } @@ -1032,7 +1036,7 @@ public class LauncherProvider extends ContentProvider { // call the constructor from the worker thread; however, this doesn't extend until after the // constructor is called, and we only pass a reference to LauncherProvider to LauncherApp // after that point - public long generateNewScreenId() { + public int generateNewScreenId() { if (mMaxScreenId < 0) { throw new RuntimeException("Error: max screen id was not initialized"); } @@ -1040,20 +1044,21 @@ public class LauncherProvider extends ContentProvider { return mMaxScreenId; } - private long initializeMaxScreenId(SQLiteDatabase db) { + private int initializeMaxScreenId(SQLiteDatabase db) { return getMaxId(db, WorkspaceScreens.TABLE_NAME); } @Thunk int loadFavorites(SQLiteDatabase db, AutoInstallsLayout loader) { - ArrayList screenIds = new ArrayList(); + IntArray screenIds = new IntArray(); // TODO: Use multiple loaders with fall-back and transaction. int count = loader.loadLayout(db, screenIds); // Add the screens specified by the items above - Collections.sort(screenIds); + int[] sortedScreenIds = screenIds.toArray(); + Arrays.sort(sortedScreenIds); int rank = 0; ContentValues values = new ContentValues(); - for (Long id : screenIds) { + for (int id : sortedScreenIds) { values.clear(); values.put(LauncherSettings.WorkspaceScreens._ID, id); values.put(LauncherSettings.WorkspaceScreens.SCREEN_RANK, rank); @@ -1075,12 +1080,12 @@ public class LauncherProvider extends ContentProvider { /** * @return the max _id in the provided table. */ - @Thunk static long getMaxId(SQLiteDatabase db, String table) { + @Thunk static int getMaxId(SQLiteDatabase db, String table) { Cursor c = db.rawQuery("SELECT MAX(_id) FROM " + table, null); // get the result - long id = -1; + int id = -1; if (c != null && c.moveToNext()) { - id = c.getLong(0); + id = c.getInt(0); } if (c != null) { c.close(); diff --git a/src/com/android/launcher3/LauncherSettings.java b/src/com/android/launcher3/LauncherSettings.java index 3b337ef18e..0b12b15f9e 100644 --- a/src/com/android/launcher3/LauncherSettings.java +++ b/src/com/android/launcher3/LauncherSettings.java @@ -128,7 +128,7 @@ public class LauncherSettings { * * @return The unique content URL for the specified row. */ - public static Uri getContentUri(long id) { + public static Uri getContentUri(int id) { return Uri.parse("content://" + LauncherProvider.AUTHORITY + "/" + TABLE_NAME + "/" + id); } diff --git a/src/com/android/launcher3/MainProcessInitializer.java b/src/com/android/launcher3/MainProcessInitializer.java index 0028f97cd7..a18dfde310 100644 --- a/src/com/android/launcher3/MainProcessInitializer.java +++ b/src/com/android/launcher3/MainProcessInitializer.java @@ -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); } diff --git a/src/com/android/launcher3/SettingsActivity.java b/src/com/android/launcher3/SettingsActivity.java index 1f802267c9..a17f614192 100644 --- a/src/com/android/launcher3/SettingsActivity.java +++ b/src/com/android/launcher3/SettingsActivity.java @@ -18,6 +18,7 @@ package com.android.launcher3; import static com.android.launcher3.states.RotationHelper.ALLOW_ROTATION_PREFERENCE_KEY; import static com.android.launcher3.states.RotationHelper.getAllowRotationDefaultValue; +import static com.android.launcher3.util.SecureSettingsObserver.newNotificationSettingsObserver; import android.annotation.TargetApi; import android.app.Activity; @@ -25,7 +26,6 @@ import android.app.AlertDialog; import android.app.Dialog; import android.app.DialogFragment; import android.app.Fragment; -import android.app.FragmentManager; import android.content.ComponentName; import android.content.ContentResolver; import android.content.Context; @@ -43,10 +43,11 @@ 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; -import com.android.launcher3.util.SettingsObserver; +import com.android.launcher3.util.SecureSettingsObserver; import com.android.launcher3.views.ButtonPreference; import java.util.Objects; @@ -57,9 +58,9 @@ 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"; /** Hidden field Settings.Secure.ENABLED_NOTIFICATION_LISTENERS */ private static final String NOTIFICATION_ENABLED_LISTENERS = "enabled_notification_listeners"; @@ -111,7 +112,7 @@ public class SettingsActivity extends Activity */ public static class LauncherSettingsFragment extends PreferenceFragment { - private IconBadgingObserver mIconBadgingObserver; + private SecureSettingsObserver mIconBadgingObserver; private String mPreferenceKey; private boolean mPreferenceHighlighted = false; @@ -126,6 +127,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 = @@ -138,9 +145,14 @@ public class SettingsActivity extends Activity getPreferenceScreen().removePreference(iconBadgingPref); } else { // Listen to system notification badge settings while this UI is active. - mIconBadgingObserver = new IconBadgingObserver( - iconBadgingPref, resolver, getFragmentManager()); - mIconBadgingObserver.register(NOTIFICATION_BADGING, NOTIFICATION_ENABLED_LISTENERS); + mIconBadgingObserver = newNotificationSettingsObserver( + getActivity(), new IconBadgingObserver(iconBadgingPref, resolver)); + mIconBadgingObserver.register(); + // Also listen if notification permission changes + mIconBadgingObserver.getResolver().registerContentObserver( + Settings.Secure.getUriFor(NOTIFICATION_ENABLED_LISTENERS), false, + mIconBadgingObserver); + mIconBadgingObserver.dispatchOnChange(); } Preference iconShapeOverride = findPreference(IconShapeOverride.KEY_PREFERENCE); @@ -246,22 +258,18 @@ public class SettingsActivity extends Activity * Content observer which listens for system badging setting changes, * and updates the launcher badging setting subtext accordingly. */ - private static class IconBadgingObserver extends SettingsObserver.Secure { + private static class IconBadgingObserver implements SecureSettingsObserver.OnChangeListener { private final ButtonPreference mBadgingPref; private final ContentResolver mResolver; - private final FragmentManager mFragmentManager; - public IconBadgingObserver(ButtonPreference badgingPref, ContentResolver resolver, - FragmentManager fragmentManager) { - super(resolver); + public IconBadgingObserver(ButtonPreference badgingPref, ContentResolver resolver) { mBadgingPref = badgingPref; mResolver = resolver; - mFragmentManager = fragmentManager; } @Override - public void onSettingChanged(boolean enabled) { + public void onSettingsChanged(boolean enabled) { int summary = enabled ? R.string.icon_badging_desc_on : R.string.icon_badging_desc_off; boolean serviceEnabled = true; diff --git a/src/com/android/launcher3/Utilities.java b/src/com/android/launcher3/Utilities.java index 74fc8d1080..d11cfcb0cb 100644 --- a/src/com/android/launcher3/Utilities.java +++ b/src/com/android/launcher3/Utilities.java @@ -52,6 +52,7 @@ import android.view.View; import android.view.animation.Interpolator; import com.android.launcher3.config.FeatureFlags; +import com.android.launcher3.util.IntArray; import java.io.ByteArrayOutputStream; import java.io.Closeable; @@ -136,9 +137,13 @@ public final class Utilities { CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE, TimeUnit.SECONDS, new LinkedBlockingQueue()); - public static final boolean IS_RUNNING_IN_TEST_HARNESS = + public static boolean IS_RUNNING_IN_TEST_HARNESS = ActivityManager.isRunningInTestHarness(); + public static void enableRunningInTestHarnessForTests() { + IS_RUNNING_IN_TEST_HARNESS = true; + } + public static boolean isPropertyEnabled(String propertyName) { return Log.isLoggable(propertyName, Log.VERBOSE); } @@ -452,8 +457,8 @@ public final class Utilities { size, metrics)); } - public static String createDbSelectionQuery(String columnName, Iterable values) { - return String.format(Locale.ENGLISH, "%s IN (%s)", columnName, TextUtils.join(", ", values)); + public static String createDbSelectionQuery(String columnName, IntArray values) { + return String.format(Locale.ENGLISH, "%s IN (%s)", columnName, values.toConcatString()); } public static boolean isBootCompleted() { @@ -510,13 +515,6 @@ public final class Utilities { return spanned; } - /** - * Replacement for Long.compare() which was added in API level 19. - */ - public static int longCompare(long lhs, long rhs) { - return lhs < rhs ? -1 : (lhs == rhs ? 0 : 1); - } - public static SharedPreferences getPrefs(Context context) { return context.getSharedPreferences( LauncherFiles.SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE); diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java index b5a770f6c3..11e601c7f3 100644 --- a/src/com/android/launcher3/Workspace.java +++ b/src/com/android/launcher3/Workspace.java @@ -28,7 +28,6 @@ import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.LayoutTransition; import android.animation.ObjectAnimator; -import android.animation.PropertyValuesHolder; import android.animation.ValueAnimator; import android.animation.ValueAnimator.AnimatorUpdateListener; import android.annotation.SuppressLint; @@ -85,8 +84,10 @@ import com.android.launcher3.touch.WorkspaceTouchListener; import com.android.launcher3.userevent.nano.LauncherLogProto.Action; import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType; import com.android.launcher3.userevent.nano.LauncherLogProto.Target; +import com.android.launcher3.util.IntArray; +import com.android.launcher3.util.IntSparseArrayMap; +import com.android.launcher3.util.IntSet; import com.android.launcher3.util.ItemInfoMatcher; -import com.android.launcher3.util.LongArrayMap; import com.android.launcher3.util.PackageUserKey; import com.android.launcher3.util.Thunk; import com.android.launcher3.util.WallpaperOffsetInterpolator; @@ -130,17 +131,17 @@ public class Workspace extends PagedView private static final boolean MAP_RECURSE = true; // The screen id used for the empty screen always present to the right. - public static final long EXTRA_EMPTY_SCREEN_ID = -201; + public static final int EXTRA_EMPTY_SCREEN_ID = -201; // The is the first screen. It is always present, even if its empty. - public static final long FIRST_SCREEN_ID = 0; + public static final int FIRST_SCREEN_ID = 0; private LayoutTransition mLayoutTransition; @Thunk final WallpaperManager mWallpaperManager; private ShortcutAndWidgetContainer mDragSourceInternal; - @Thunk final LongArrayMap mWorkspaceScreens = new LongArrayMap<>(); - @Thunk final ArrayList mScreenOrder = new ArrayList<>(); + @Thunk final IntSparseArrayMap mWorkspaceScreens = new IntSparseArrayMap<>(); + @Thunk final IntArray mScreenOrder = new IntArray(); @Thunk Runnable mRemoveEmptyScreenRunnable; @Thunk boolean mDeferRemoveExtraEmptyScreen = false; @@ -227,7 +228,7 @@ public class Workspace extends PagedView @Thunk int mLastReorderY = -1; private SparseArray mSavedStates; - private final ArrayList mRestoredPages = new ArrayList<>(); + private final IntArray mRestoredPages = new IntArray(); private float mCurrentScale; private float mTransitionProgress; @@ -479,7 +480,7 @@ public class Workspace extends PagedView * @param qsb an existing qsb to recycle or null. */ public void bindAndInitFirstWorkspaceScreen(View qsb) { - if (!FeatureFlags.QSB_ON_FIRST_SCREEN) { + if (!FeatureFlags.QSB_ON_FIRST_SCREEN.get()) { return; } // Add the first page @@ -523,7 +524,7 @@ public class Workspace extends PagedView enableLayoutTransitions(); } - public void insertNewWorkspaceScreenBeforeEmptyScreen(long screenId) { + public void insertNewWorkspaceScreenBeforeEmptyScreen(int screenId) { // Find the index to insert this view into. If the empty screen exists, then // insert it before that. int insertIndex = mScreenOrder.indexOf(EXTRA_EMPTY_SCREEN_ID); @@ -533,11 +534,11 @@ public class Workspace extends PagedView insertNewWorkspaceScreen(screenId, insertIndex); } - public void insertNewWorkspaceScreen(long screenId) { + public void insertNewWorkspaceScreen(int screenId) { insertNewWorkspaceScreen(screenId, getChildCount()); } - public CellLayout insertNewWorkspaceScreen(long screenId, int insertIndex) { + public CellLayout insertNewWorkspaceScreen(int screenId, int insertIndex) { if (mWorkspaceScreens.containsKey(screenId)) { throw new RuntimeException("Screen id " + screenId + " already exists!"); } @@ -604,7 +605,7 @@ public class Workspace extends PagedView } if (hasExtraEmptyScreen() || mScreenOrder.size() == 0) return; - long finalScreenId = mScreenOrder.get(mScreenOrder.size() - 1); + int finalScreenId = mScreenOrder.get(mScreenOrder.size() - 1); CellLayout finalScreen = mWorkspaceScreens.get(finalScreenId); @@ -612,7 +613,7 @@ public class Workspace extends PagedView if (finalScreen.getShortcutsAndWidgets().getChildCount() == 0 && !finalScreen.isDropPending()) { mWorkspaceScreens.remove(finalScreenId); - mScreenOrder.remove(finalScreenId); + mScreenOrder.removeValue(finalScreenId); // if this is the last screen, convert it to the empty screen mWorkspaceScreens.put(EXTRA_EMPTY_SCREEN_ID, finalScreen); @@ -678,7 +679,7 @@ public class Workspace extends PagedView public void run() { if (hasExtraEmptyScreen()) { mWorkspaceScreens.remove(EXTRA_EMPTY_SCREEN_ID); - mScreenOrder.remove(EXTRA_EMPTY_SCREEN_ID); + mScreenOrder.removeValue(EXTRA_EMPTY_SCREEN_ID); removeView(cl); if (stripEmptyScreens) { stripEmptyScreens(); @@ -710,7 +711,7 @@ public class Workspace extends PagedView return mWorkspaceScreens.containsKey(EXTRA_EMPTY_SCREEN_ID) && getChildCount() > 1; } - public long commitExtraEmptyScreen() { + public int commitExtraEmptyScreen() { if (mLauncher.isWorkspaceLoading()) { // Invalid and dangerous operation if workspace is loading return -1; @@ -718,11 +719,11 @@ public class Workspace extends PagedView CellLayout cl = mWorkspaceScreens.get(EXTRA_EMPTY_SCREEN_ID); mWorkspaceScreens.remove(EXTRA_EMPTY_SCREEN_ID); - mScreenOrder.remove(EXTRA_EMPTY_SCREEN_ID); + mScreenOrder.removeValue(EXTRA_EMPTY_SCREEN_ID); - long newId = LauncherSettings.Settings.call(getContext().getContentResolver(), + int newId = LauncherSettings.Settings.call(getContext().getContentResolver(), LauncherSettings.Settings.METHOD_NEW_SCREEN_ID) - .getLong(LauncherSettings.Settings.EXTRA_VALUE); + .getInt(LauncherSettings.Settings.EXTRA_VALUE); mWorkspaceScreens.put(newId, cl); mScreenOrder.add(newId); @@ -732,11 +733,11 @@ public class Workspace extends PagedView return newId; } - public CellLayout getScreenWithId(long screenId) { + public CellLayout getScreenWithId(int screenId) { return mWorkspaceScreens.get(screenId); } - public long getIdForScreen(CellLayout layout) { + public int getIdForScreen(CellLayout layout) { int index = mWorkspaceScreens.indexOfValue(layout); if (index != -1) { return mWorkspaceScreens.keyAt(index); @@ -744,18 +745,18 @@ public class Workspace extends PagedView return -1; } - public int getPageIndexForScreenId(long screenId) { + public int getPageIndexForScreenId(int screenId) { return indexOfChild(mWorkspaceScreens.get(screenId)); } - public long getScreenIdForPageIndex(int index) { + public int getScreenIdForPageIndex(int index) { if (0 <= index && index < mScreenOrder.size()) { return mScreenOrder.get(index); } return -1; } - public ArrayList getScreenOrder() { + public IntArray getScreenOrder() { return mScreenOrder; } @@ -772,13 +773,13 @@ public class Workspace extends PagedView } int currentPage = getNextPage(); - ArrayList removeScreens = new ArrayList<>(); + IntArray removeScreens = new IntArray(); int total = mWorkspaceScreens.size(); for (int i = 0; i < total; i++) { - long id = mWorkspaceScreens.keyAt(i); + int id = mWorkspaceScreens.keyAt(i); CellLayout cl = mWorkspaceScreens.valueAt(i); // FIRST_SCREEN_ID can never be removed. - if ((!FeatureFlags.QSB_ON_FIRST_SCREEN || id > FIRST_SCREEN_ID) + if ((!FeatureFlags.QSB_ON_FIRST_SCREEN.get() || id > FIRST_SCREEN_ID) && cl.getShortcutsAndWidgets().getChildCount() == 0) { removeScreens.add(id); } @@ -791,10 +792,11 @@ public class Workspace extends PagedView int minScreens = 1; int pageShift = 0; - for (Long id: removeScreens) { + for (int i = 0; i < removeScreens.size(); i++) { + int id = removeScreens.get(i); CellLayout cl = mWorkspaceScreens.get(id); mWorkspaceScreens.remove(id); - mScreenOrder.remove(id); + mScreenOrder.removeValue(id); if (getChildCount() > minScreens) { if (indexOfChild(cl) < currentPage) { @@ -843,7 +845,7 @@ public class Workspace extends PagedView /** * Adds the specified child in the specified screen based on the {@param info} - * See {@link #addInScreen(View, long, long, int, int, int, int)}. + * See {@link #addInScreen(View, int, int, int, int, int, int)}. */ public void addInScreen(View child, ItemInfo info) { addInScreen(child, info.container, info.screenId, info.cellX, info.cellY, @@ -861,7 +863,7 @@ public class Workspace extends PagedView * @param spanX The number of cells spanned horizontally by the child. * @param spanY The number of cells spanned vertically by the child. */ - private void addInScreen(View child, long container, long screenId, int x, int y, + private void addInScreen(View child, int container, int screenId, int x, int y, int spanX, int spanY) { if (container == LauncherSettings.Favorites.CONTAINER_DESKTOP) { if (getScreenWithId(screenId) == null) { @@ -1675,7 +1677,7 @@ public class Workspace extends PagedView } } - long screenId = getIdForScreen(dropTargetLayout); + int screenId = getIdForScreen(dropTargetLayout); if (screenId == EXTRA_EMPTY_SCREEN_ID) { commitExtraEmptyScreen(); } @@ -1740,7 +1742,7 @@ public class Workspace extends PagedView return false; } - boolean createUserFolderIfNecessary(View newView, long container, CellLayout target, + boolean createUserFolderIfNecessary(View newView, int container, CellLayout target, int[] targetCell, float distance, boolean external, DragView dragView) { if (distance > mMaxDistanceForFolderCreation) return false; View v = target.getChildAt(targetCell[0], targetCell[1]); @@ -1754,7 +1756,7 @@ public class Workspace extends PagedView if (v == null || hasntMoved || !mCreateUserFolderOnDrop) return false; mCreateUserFolderOnDrop = false; - final long screenId = getIdForScreen(target); + final int screenId = getIdForScreen(target); boolean aboveShortcut = (v.getTag() instanceof ShortcutInfo); boolean willBecomeShortcut = (newView.getTag() instanceof ShortcutInfo); @@ -1853,10 +1855,10 @@ public class Workspace extends PagedView // Move internally boolean hasMovedLayouts = (getParentCellLayoutForView(cell) != dropTargetLayout); boolean hasMovedIntoHotseat = mLauncher.isHotseatLayout(dropTargetLayout); - long container = hasMovedIntoHotseat ? + int container = hasMovedIntoHotseat ? LauncherSettings.Favorites.CONTAINER_HOTSEAT : LauncherSettings.Favorites.CONTAINER_DESKTOP; - long screenId = (mTargetCell[0] < 0) ? + int screenId = (mTargetCell[0] < 0) ? mDragInfo.screenId : getIdForScreen(dropTargetLayout); int spanX = mDragInfo != null ? mDragInfo.spanX : 1; int spanY = mDragInfo != null ? mDragInfo.spanY : 1; @@ -2530,10 +2532,10 @@ public class Workspace extends PagedView spanY = mDragInfo.spanY; } - final long container = mLauncher.isHotseatLayout(cellLayout) ? + final int container = mLauncher.isHotseatLayout(cellLayout) ? LauncherSettings.Favorites.CONTAINER_HOTSEAT : LauncherSettings.Favorites.CONTAINER_DESKTOP; - final long screenId = getIdForScreen(cellLayout); + final int screenId = getIdForScreen(cellLayout); if (!mLauncher.isHotseatLayout(cellLayout) && screenId != getScreenIdForPageIndex(mCurrentPage) && !mLauncher.isInState(SPRING_LOADED)) { @@ -3006,7 +3008,7 @@ public class Workspace extends PagedView return childrenLayouts; } - public View getHomescreenIconByItemId(final long id) { + public View getHomescreenIconByItemId(final int id) { return getFirstMatch(new ItemOperator() { @Override @@ -3065,7 +3067,7 @@ public class Workspace extends PagedView for (final CellLayout layoutParent: cellLayouts) { final ViewGroup layout = layoutParent.getShortcutsAndWidgets(); - LongArrayMap idToViewMap = new LongArrayMap<>(); + IntSparseArrayMap idToViewMap = new IntSparseArrayMap<>(); ArrayList items = new ArrayList<>(); for (int j = 0; j < layout.getChildCount(); j++) { final View view = layout.getChildAt(j); @@ -3153,7 +3155,7 @@ public class Workspace extends PagedView void updateShortcuts(ArrayList shortcuts) { int total = shortcuts.size(); final HashSet updates = new HashSet<>(total); - final HashSet folderIds = new HashSet<>(); + final IntSet folderIds = new IntSet(); for (int i = 0; i < total; i++) { ShortcutInfo s = shortcuts.get(i); @@ -3193,7 +3195,7 @@ public class Workspace extends PagedView public void updateIconBadges(final Set updatedBadges) { final PackageUserKey packageUserKey = new PackageUserKey(null, null); - final HashSet folderIds = new HashSet<>(); + final IntSet folderIds = new IntSet(); mapOverItems(MAP_RECURSE, new ItemOperator() { @Override public boolean evaluate(ItemInfo info, View v) { diff --git a/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java b/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java index 81a0e1d5df..84edb3dac0 100644 --- a/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java +++ b/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java @@ -39,6 +39,7 @@ import com.android.launcher3.notification.NotificationListener; import com.android.launcher3.popup.PopupContainerWithArrow; import com.android.launcher3.shortcuts.DeepShortcutManager; import com.android.launcher3.touch.ItemLongClickListener; +import com.android.launcher3.util.IntArray; import com.android.launcher3.util.Thunk; import com.android.launcher3.widget.LauncherAppWidgetHostView; @@ -159,7 +160,7 @@ public class LauncherAccessibilityDelegate extends AccessibilityDelegate impleme beginAccessibleDrag(host, item); } else if (action == ADD_TO_WORKSPACE) { final int[] coordinates = new int[2]; - final long screenId = findSpaceOnWorkspace(item, coordinates); + final int screenId = findSpaceOnWorkspace(item, coordinates); mLauncher.getStateManager().goToState(NORMAL, true, new Runnable() { @Override @@ -191,7 +192,7 @@ public class LauncherAccessibilityDelegate extends AccessibilityDelegate impleme folder.getInfo().remove(info, false); final int[] coordinates = new int[2]; - final long screenId = findSpaceOnWorkspace(item, coordinates); + final int screenId = findSpaceOnWorkspace(item, coordinates); mLauncher.getModelWriter().moveItemInDatabase(info, LauncherSettings.Favorites.CONTAINER_DESKTOP, screenId, coordinates[0], coordinates[1]); @@ -210,7 +211,7 @@ public class LauncherAccessibilityDelegate extends AccessibilityDelegate impleme }); } else if (action == RESIZE) { final LauncherAppWidgetInfo info = (LauncherAppWidgetInfo) item; - final ArrayList actions = getSupportedResizeActions(host, info); + final IntArray actions = getSupportedResizeActions(host, info); CharSequence[] labels = new CharSequence[actions.size()]; for (int i = 0; i < actions.size(); i++) { labels[i] = mLauncher.getText(actions.get(i)); @@ -242,8 +243,8 @@ public class LauncherAccessibilityDelegate extends AccessibilityDelegate impleme return false; } - private ArrayList getSupportedResizeActions(View host, LauncherAppWidgetInfo info) { - ArrayList actions = new ArrayList<>(); + private IntArray getSupportedResizeActions(View host, LauncherAppWidgetInfo info) { + IntArray actions = new IntArray(); AppWidgetProviderInfo providerInfo = ((LauncherAppWidgetHostView) host).getAppWidgetInfo(); if (providerInfo == null) { @@ -392,10 +393,10 @@ public class LauncherAccessibilityDelegate extends AccessibilityDelegate impleme /** * Find empty space on the workspace and returns the screenId. */ - protected long findSpaceOnWorkspace(ItemInfo info, int[] outCoordinates) { + protected int findSpaceOnWorkspace(ItemInfo info, int[] outCoordinates) { Workspace workspace = mLauncher.getWorkspace(); - ArrayList workspaceScreens = workspace.getScreenOrder(); - long screenId; + IntArray workspaceScreens = workspace.getScreenOrder(); + int screenId; // First check if there is space on the current screen. int screenIndex = workspace.getCurrentPage(); diff --git a/src/com/android/launcher3/accessibility/ShortcutMenuAccessibilityDelegate.java b/src/com/android/launcher3/accessibility/ShortcutMenuAccessibilityDelegate.java index cfb0520293..f37f70b4ca 100644 --- a/src/com/android/launcher3/accessibility/ShortcutMenuAccessibilityDelegate.java +++ b/src/com/android/launcher3/accessibility/ShortcutMenuAccessibilityDelegate.java @@ -66,7 +66,7 @@ public class ShortcutMenuAccessibilityDelegate extends LauncherAccessibilityDele } final ShortcutInfo info = ((DeepShortcutView) host.getParent()).getFinalInfo(); final int[] coordinates = new int[2]; - final long screenId = findSpaceOnWorkspace(item, coordinates); + final int screenId = findSpaceOnWorkspace(item, coordinates); Runnable onComplete = new Runnable() { @Override public void run() { diff --git a/src/com/android/launcher3/allapps/FloatingHeaderView.java b/src/com/android/launcher3/allapps/FloatingHeaderView.java index 5348349f4d..90e195b2b3 100644 --- a/src/com/android/launcher3/allapps/FloatingHeaderView.java +++ b/src/com/android/launcher3/allapps/FloatingHeaderView.java @@ -25,16 +25,22 @@ import android.view.View; import android.view.ViewGroup; import android.view.animation.Interpolator; import android.widget.LinearLayout; - -import com.android.launcher3.R; -import com.android.launcher3.anim.PropertySetter; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.recyclerview.widget.RecyclerView; +import com.android.launcher3.R; +import com.android.launcher3.anim.PropertySetter; +import com.android.launcher3.uioverrides.plugins.PluginManagerWrapper; +import com.android.systemui.plugins.AllAppsRow; +import com.android.systemui.plugins.PluginListener; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; public class FloatingHeaderView extends LinearLayout implements - ValueAnimator.AnimatorUpdateListener { + ValueAnimator.AnimatorUpdateListener, PluginListener { private final Rect mClip = new Rect(0, 0, Integer.MAX_VALUE, Integer.MAX_VALUE); private final ValueAnimator mAnimator = ValueAnimator.ofInt(0, 0); @@ -64,6 +70,9 @@ public class FloatingHeaderView extends LinearLayout implements private AllAppsRecyclerView mMainRV; private AllAppsRecyclerView mWorkRV; private AllAppsRecyclerView mCurrentRV; + protected final Map mPluginRows; + // Contains just the values of the above map so we can iterate without extracting a new list. + protected final List mPluginRowViews; private ViewGroup mParent; private boolean mHeaderCollapsed; private int mSnappedScrolledY; @@ -82,6 +91,8 @@ public class FloatingHeaderView extends LinearLayout implements public FloatingHeaderView(@NonNull Context context, @Nullable AttributeSet attrs) { super(context, attrs); + mPluginRows = new HashMap<>(); + mPluginRowViews = new ArrayList<>(); } @Override @@ -90,6 +101,38 @@ public class FloatingHeaderView extends LinearLayout implements mTabLayout = findViewById(R.id.tabs); } + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + PluginManagerWrapper.INSTANCE.get(getContext()).addPluginListener(this, + AllAppsRow.class, true /* allowMultiple */); + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + PluginManagerWrapper.INSTANCE.get(getContext()).removePluginListener(this); + } + + @Override + public void onPluginConnected(AllAppsRow allAppsRowPlugin, Context context) { + mPluginRows.put(allAppsRowPlugin, null); + setupPluginRows(); + allAppsRowPlugin.setOnHeightUpdatedListener(this::onPluginRowHeightUpdated); + } + + protected void onPluginRowHeightUpdated() { + } + + @Override + public void onPluginDisconnected(AllAppsRow plugin) { + View pluginRowView = mPluginRows.get(plugin); + removeView(pluginRowView); + mPluginRows.remove(plugin); + mPluginRowViews.remove(pluginRowView); + onPluginRowHeightUpdated(); + } + public void setup(AllAppsContainerView.AdapterHolder[] mAH, boolean tabsHidden) { mTabsHidden = tabsHidden; mTabLayout.setVisibility(tabsHidden ? View.GONE : View.VISIBLE); @@ -97,9 +140,24 @@ public class FloatingHeaderView extends LinearLayout implements mWorkRV = setupRV(mWorkRV, mAH[AllAppsContainerView.AdapterHolder.WORK].recyclerView); mParent = (ViewGroup) mMainRV.getParent(); setMainActive(mMainRVActive || mWorkRV == null); + setupPluginRows(); reset(false); } + private void setupPluginRows() { + for (Map.Entry rowPluginEntry : mPluginRows.entrySet()) { + if (rowPluginEntry.getValue() == null) { + View pluginRow = rowPluginEntry.getKey().setup(this); + addView(pluginRow, indexOfChild(mTabLayout)); + rowPluginEntry.setValue(pluginRow); + mPluginRowViews.add(pluginRow); + } + } + for (View plugin : mPluginRowViews) { + plugin.setVisibility(mHeaderCollapsed ? GONE : VISIBLE); + } + } + private AllAppsRecyclerView setupRV(AllAppsRecyclerView old, AllAppsRecyclerView updated) { if (old != updated && updated != null ) { updated.addOnScrollListener(mOnScrollListener); diff --git a/src/com/android/launcher3/compat/UserManagerCompatVL.java b/src/com/android/launcher3/compat/UserManagerCompatVL.java index eec3438496..ef7284226b 100644 --- a/src/com/android/launcher3/compat/UserManagerCompatVL.java +++ b/src/com/android/launcher3/compat/UserManagerCompatVL.java @@ -17,32 +17,29 @@ package com.android.launcher3.compat; import android.content.Context; -import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.os.UserHandle; import android.os.UserManager; import android.util.ArrayMap; -import com.android.launcher3.util.LongArrayMap; +import android.util.LongSparseArray; + import java.util.ArrayList; import java.util.Collections; import java.util.List; public class UserManagerCompatVL extends UserManagerCompat { - private static final String USER_CREATION_TIME_KEY = "user_creation_time_"; protected final UserManager mUserManager; private final PackageManager mPm; - private final Context mContext; - protected LongArrayMap mUsers; - // Create a separate reverse map as LongArrayMap.indexOfValue checks if objects are same + protected LongSparseArray mUsers; + // Create a separate reverse map as LongSparseArray.indexOfValue checks if objects are same // and not {@link Object#equals} protected ArrayMap mUserToSerialMap; UserManagerCompatVL(Context context) { mUserManager = (UserManager) context.getSystemService(Context.USER_SERVICE); mPm = context.getPackageManager(); - mContext = context; } @Override @@ -94,7 +91,7 @@ public class UserManagerCompatVL extends UserManagerCompat { @Override public void enableAndResetCache() { synchronized (this) { - mUsers = new LongArrayMap<>(); + mUsers = new LongSparseArray<>(); mUserToSerialMap = new ArrayMap<>(); List users = mUserManager.getUserProfiles(); if (users != null) { diff --git a/src/com/android/launcher3/config/BaseFlags.java b/src/com/android/launcher3/config/BaseFlags.java index b67d35bd6c..dc60c8fff2 100644 --- a/src/com/android/launcher3/config/BaseFlags.java +++ b/src/com/android/launcher3/config/BaseFlags.java @@ -16,17 +16,44 @@ 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. * - * All the flags should be defined here with appropriate default values. To override a value, - * redefine it in {@link FeatureFlags}. + *

All the flags should be defined here with appropriate default values. * - * This class is kept package-private to prevent direct access. + *

This class is kept package-private to prevent direct access. */ +@Keep abstract class BaseFlags { - BaseFlags() {} + private static final Object sLock = new Object(); + @GuardedBy("sLock") + private static final List 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; public static final String AUTHORITY = "com.android.launcher3.settings".intern(); @@ -34,15 +61,16 @@ 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 static final boolean QSB_ON_FIRST_SCREEN = 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; - // When enabled the all-apps icon is not added to the hotseat. - public static final boolean NO_ALL_APPS_ICON = true; - // When true, custom widgets are loaded using CustomWidgetParser. public static final boolean ENABLE_CUSTOM_WIDGETS = false; @@ -55,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 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 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$; + } + } + } diff --git a/src/com/android/launcher3/config/FlagTogglerPreferenceFragment.java b/src/com/android/launcher3/config/FlagTogglerPreferenceFragment.java new file mode 100644 index 0000000000..0a1fd2fe07 --- /dev/null +++ b/src/com/android/launcher3/config/FlagTogglerPreferenceFragment.java @@ -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()); + } +} \ No newline at end of file diff --git a/src/com/android/launcher3/dragndrop/FolderAdaptiveIcon.java b/src/com/android/launcher3/dragndrop/FolderAdaptiveIcon.java index 5576d91642..9f0d678bf2 100644 --- a/src/com/android/launcher3/dragndrop/FolderAdaptiveIcon.java +++ b/src/com/android/launcher3/dragndrop/FolderAdaptiveIcon.java @@ -65,7 +65,7 @@ public class FolderAdaptiveIcon extends AdaptiveIconDrawable { } public static FolderAdaptiveIcon createFolderAdaptiveIcon( - Launcher launcher, long folderId, Point dragViewSize) { + Launcher launcher, int folderId, Point dragViewSize) { Preconditions.assertNonUiThread(); int margin = launcher.getResources() .getDimensionPixelSize(R.dimen.blur_size_medium_outline); diff --git a/src/com/android/launcher3/icons/BaseIconFactory.java b/src/com/android/launcher3/icons/BaseIconFactory.java new file mode 100644 index 0000000000..db723b7208 --- /dev/null +++ b/src/com/android/launcher3/icons/BaseIconFactory.java @@ -0,0 +1,303 @@ +package com.android.launcher3.icons; + +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.PaintFlagsDrawFilter; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.drawable.AdaptiveIconDrawable; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.os.Process; +import android.os.UserHandle; + +import com.android.launcher3.R; + +import static android.graphics.Paint.DITHER_FLAG; +import static android.graphics.Paint.FILTER_BITMAP_FLAG; +import static com.android.launcher3.icons.ShadowGenerator.BLUR_FACTOR; + +/** + * This class will be moved to androidx library. There shouldn't be any dependency outside + * this package. + */ +public class BaseIconFactory { + + private static final int DEFAULT_WRAPPER_BACKGROUND = Color.WHITE; + public static final boolean ATLEAST_OREO = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O; + + private final Rect mOldBounds = new Rect(); + private final Context mContext; + private final Canvas mCanvas; + private final PackageManager mPm; + private final ColorExtractor mColorExtractor; + private boolean mDisableColorExtractor; + + private int mFillResIconDpi; + private int mIconBitmapSize; + + private IconNormalizer mNormalizer; + private ShadowGenerator mShadowGenerator; + + private Drawable mWrapperIcon; + private int mWrapperBackgroundColor; + + protected BaseIconFactory(Context context, int fillResIconDpi, int iconBitmapSize) { + mContext = context.getApplicationContext(); + + mFillResIconDpi = fillResIconDpi; + mIconBitmapSize = iconBitmapSize; + + mPm = mContext.getPackageManager(); + mColorExtractor = new ColorExtractor(); + + mCanvas = new Canvas(); + mCanvas.setDrawFilter(new PaintFlagsDrawFilter(DITHER_FLAG, FILTER_BITMAP_FLAG)); + } + + protected void clear() { + mWrapperBackgroundColor = DEFAULT_WRAPPER_BACKGROUND; + mDisableColorExtractor = false; + } + + public ShadowGenerator getShadowGenerator() { + if (mShadowGenerator == null) { + mShadowGenerator = new ShadowGenerator(mContext); + } + return mShadowGenerator; + } + + public IconNormalizer getNormalizer() { + if (mNormalizer == null) { + mNormalizer = new IconNormalizer(mContext); + } + return mNormalizer; + } + + public BitmapInfo createIconBitmap(Intent.ShortcutIconResource iconRes) { + try { + Resources resources = mPm.getResourcesForApplication(iconRes.packageName); + if (resources != null) { + final int id = resources.getIdentifier(iconRes.resourceName, null, null); + // do not stamp old legacy shortcuts as the app may have already forgotten about it + return createBadgedIconBitmap( + resources.getDrawableForDensity(id, mFillResIconDpi), + Process.myUserHandle() /* only available on primary user */, + false /* do not apply legacy treatment */); + } + } catch (Exception e) { + // Icon not found. + } + return null; + } + + public BitmapInfo createIconBitmap(Bitmap icon) { + if (mIconBitmapSize == icon.getWidth() && mIconBitmapSize == icon.getHeight()) { + return BitmapInfo.fromBitmap(icon); + } + return BitmapInfo.fromBitmap( + createIconBitmap(new BitmapDrawable(mContext.getResources(), icon), 1f)); + } + + public BitmapInfo createBadgedIconBitmap(Drawable icon, UserHandle user, + boolean shrinkNonAdaptiveIcons) { + return createBadgedIconBitmap(icon, user, shrinkNonAdaptiveIcons, false, null); + } + + public BitmapInfo createBadgedIconBitmap(Drawable icon, UserHandle user, + boolean shrinkNonAdaptiveIcons, boolean isInstantApp) { + return createBadgedIconBitmap(icon, user, shrinkNonAdaptiveIcons, isInstantApp, null); + } + + /** + * Creates bitmap using the source drawable and various parameters. + * The bitmap is visually normalized with other icons and has enough spacing to add shadow. + * + * @param icon source of the icon + * @param user info can be used for a badge + * @param shrinkNonAdaptiveIcons {@code true} if non adaptive icons should be treated + * @param isInstantApp info can be used for a badge + * @param scale returns the scale result from normalization + * @return a bitmap suitable for disaplaying as an icon at various system UIs. + */ + public BitmapInfo createBadgedIconBitmap(Drawable icon, UserHandle user, + boolean shrinkNonAdaptiveIcons, boolean isInstantApp, float[] scale) { + if (scale == null) { + scale = new float[1]; + } + icon = normalizeAndWrapToAdaptiveIcon(icon, shrinkNonAdaptiveIcons, null, scale); + Bitmap bitmap = createIconBitmap(icon, scale[0]); + if (ATLEAST_OREO && icon instanceof AdaptiveIconDrawable) { + mCanvas.setBitmap(bitmap); + getShadowGenerator().recreateIcon(Bitmap.createBitmap(bitmap), mCanvas); + mCanvas.setBitmap(null); + } + + final Bitmap result; + if (user != null && !Process.myUserHandle().equals(user)) { + BitmapDrawable drawable = new FixedSizeBitmapDrawable(bitmap); + Drawable badged = mPm.getUserBadgedIcon(drawable, user); + if (badged instanceof BitmapDrawable) { + result = ((BitmapDrawable) badged).getBitmap(); + } else { + result = createIconBitmap(badged, 1f); + } + } else if (isInstantApp) { + badgeWithDrawable(bitmap, mContext.getDrawable(R.drawable.ic_instant_app_badge)); + result = bitmap; + } else { + result = bitmap; + } + return BitmapInfo.fromBitmap(result, mDisableColorExtractor ? null : mColorExtractor); + } + + public Bitmap createScaledBitmapWithoutShadow(Drawable icon, boolean shrinkNonAdaptiveIcons) { + RectF iconBounds = new RectF(); + float[] scale = new float[1]; + icon = normalizeAndWrapToAdaptiveIcon(icon, shrinkNonAdaptiveIcons, iconBounds, scale); + return createIconBitmap(icon, + Math.min(scale[0], ShadowGenerator.getScaleForBounds(iconBounds))); + } + + /** + * Sets the background color used for wrapped adaptive icon + */ + public void setWrapperBackgroundColor(int color) { + mWrapperBackgroundColor = (Color.alpha(color) < 255) ? DEFAULT_WRAPPER_BACKGROUND : color; + } + + /** + * Disables the dominant color extraction for all icons loaded. + */ + public void disableColorExtraction() { + mDisableColorExtractor = true; + } + + private Drawable normalizeAndWrapToAdaptiveIcon(Drawable icon, boolean shrinkNonAdaptiveIcons, + RectF outIconBounds, float[] outScale) { + float scale = 1f; + + if (shrinkNonAdaptiveIcons) { + boolean[] outShape = new boolean[1]; + if (mWrapperIcon == null) { + mWrapperIcon = mContext.getDrawable(R.drawable.adaptive_icon_drawable_wrapper) + .mutate(); + } + AdaptiveIconDrawable dr = (AdaptiveIconDrawable) mWrapperIcon; + dr.setBounds(0, 0, 1, 1); + scale = getNormalizer().getScale(icon, outIconBounds, dr.getIconMask(), outShape); + if (ATLEAST_OREO && !outShape[0] && !(icon instanceof AdaptiveIconDrawable)) { + FixedScaleDrawable fsd = ((FixedScaleDrawable) dr.getForeground()); + fsd.setDrawable(icon); + fsd.setScale(scale); + icon = dr; + scale = getNormalizer().getScale(icon, outIconBounds, null, null); + + ((ColorDrawable) dr.getBackground()).setColor(mWrapperBackgroundColor); + } + } else { + scale = getNormalizer().getScale(icon, outIconBounds, null, null); + } + + outScale[0] = scale; + return icon; + } + + /** + * Adds the {@param badge} on top of {@param target} using the badge dimensions. + */ + public void badgeWithDrawable(Bitmap target, Drawable badge) { + mCanvas.setBitmap(target); + badgeWithDrawable(mCanvas, badge); + mCanvas.setBitmap(null); + } + + /** + * Adds the {@param badge} on top of {@param target} using the badge dimensions. + */ + public void badgeWithDrawable(Canvas target, Drawable badge) { + int badgeSize = mContext.getResources().getDimensionPixelSize(R.dimen.profile_badge_size); + badge.setBounds(mIconBitmapSize - badgeSize, mIconBitmapSize - badgeSize, + mIconBitmapSize, mIconBitmapSize); + badge.draw(target); + } + + /** + * @param scale the scale to apply before drawing {@param icon} on the canvas + */ + private Bitmap createIconBitmap(Drawable icon, float scale) { + Bitmap bitmap = Bitmap.createBitmap(mIconBitmapSize, mIconBitmapSize, + Bitmap.Config.ARGB_8888); + mCanvas.setBitmap(bitmap); + mOldBounds.set(icon.getBounds()); + + if (ATLEAST_OREO && icon instanceof AdaptiveIconDrawable) { + int offset = Math.max((int) Math.ceil(BLUR_FACTOR * mIconBitmapSize), + Math.round(mIconBitmapSize * (1 - scale) / 2 )); + icon.setBounds(offset, offset, mIconBitmapSize - offset, mIconBitmapSize - offset); + icon.draw(mCanvas); + } else { + if (icon instanceof BitmapDrawable) { + BitmapDrawable bitmapDrawable = (BitmapDrawable) icon; + Bitmap b = bitmapDrawable.getBitmap(); + if (bitmap != null && b.getDensity() == Bitmap.DENSITY_NONE) { + bitmapDrawable.setTargetDensity(mContext.getResources().getDisplayMetrics()); + } + } + int width = mIconBitmapSize; + int height = mIconBitmapSize; + + int intrinsicWidth = icon.getIntrinsicWidth(); + int intrinsicHeight = icon.getIntrinsicHeight(); + if (intrinsicWidth > 0 && intrinsicHeight > 0) { + // Scale the icon proportionally to the icon dimensions + final float ratio = (float) intrinsicWidth / intrinsicHeight; + if (intrinsicWidth > intrinsicHeight) { + height = (int) (width / ratio); + } else if (intrinsicHeight > intrinsicWidth) { + width = (int) (height * ratio); + } + } + final int left = (mIconBitmapSize - width) / 2; + final int top = (mIconBitmapSize - height) / 2; + icon.setBounds(left, top, left + width, top + height); + mCanvas.save(); + mCanvas.scale(scale, scale, mIconBitmapSize / 2, mIconBitmapSize / 2); + icon.draw(mCanvas); + mCanvas.restore(); + + } + icon.setBounds(mOldBounds); + mCanvas.setBitmap(null); + return bitmap; + } + + /** + * An extension of {@link BitmapDrawable} which returns the bitmap pixel size as intrinsic size. + * This allows the badging to be done based on the action bitmap size rather than + * the scaled bitmap size. + */ + private static class FixedSizeBitmapDrawable extends BitmapDrawable { + + public FixedSizeBitmapDrawable(Bitmap bitmap) { + super(null, bitmap); + } + + @Override + public int getIntrinsicHeight() { + return getBitmap().getWidth(); + } + + @Override + public int getIntrinsicWidth() { + return getBitmap().getWidth(); + } + } +} diff --git a/src/com/android/launcher3/icons/IconCacheUpdateHandler.java b/src/com/android/launcher3/icons/IconCacheUpdateHandler.java index 8b3af01042..07451b9504 100644 --- a/src/com/android/launcher3/icons/IconCacheUpdateHandler.java +++ b/src/com/android/launcher3/icons/IconCacheUpdateHandler.java @@ -29,6 +29,7 @@ import android.util.SparseBooleanArray; import com.android.launcher3.Utilities; import com.android.launcher3.icons.BaseIconCache.IconDB; +import com.android.launcher3.util.IntArray; import java.util.ArrayList; import java.util.Collections; @@ -205,7 +206,7 @@ public class IconCacheUpdateHandler { public void finish() { // Commit all deletes - ArrayList deleteIds = new ArrayList<>(); + IntArray deleteIds = new IntArray(); int count = mItemsToDelete.size(); for (int i = 0; i < count; i++) { if (mItemsToDelete.valueAt(i)) { diff --git a/src/com/android/launcher3/icons/LauncherIcons.java b/src/com/android/launcher3/icons/LauncherIcons.java index b4cbf65304..69614a3cb7 100644 --- a/src/com/android/launcher3/icons/LauncherIcons.java +++ b/src/com/android/launcher3/icons/LauncherIcons.java @@ -16,28 +16,11 @@ package com.android.launcher3.icons; -import static android.graphics.Paint.DITHER_FLAG; -import static android.graphics.Paint.FILTER_BITMAP_FLAG; - -import static com.android.launcher3.icons.ShadowGenerator.BLUR_FACTOR; - import android.content.ComponentName; import android.content.Context; import android.content.Intent; -import android.content.Intent.ShortcutIconResource; -import android.content.pm.PackageManager; -import android.content.res.Resources; import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.PaintFlagsDrawFilter; -import android.graphics.Rect; -import android.graphics.RectF; -import android.graphics.drawable.AdaptiveIconDrawable; -import android.graphics.drawable.BitmapDrawable; -import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; -import android.graphics.drawable.PaintDrawable; import android.os.Build; import android.os.Process; import android.os.UserHandle; @@ -48,7 +31,6 @@ import com.android.launcher3.graphics.BitmapRenderer; import com.android.launcher3.InvariantDeviceProfile; import com.android.launcher3.ItemInfoWithIcon; import com.android.launcher3.LauncherAppState; -import com.android.launcher3.R; import com.android.launcher3.Utilities; import com.android.launcher3.model.PackageItemInfo; import com.android.launcher3.shortcuts.DeepShortcutManager; @@ -59,14 +41,14 @@ import com.android.launcher3.util.Themes; import androidx.annotation.Nullable; /** - * Helper methods for generating various launcher icons + * Wrapper class to provide access to {@link BaseIconFactory} and also to provide pool of this class + * that are threadsafe. */ -public class LauncherIcons implements AutoCloseable { +public class LauncherIcons extends BaseIconFactory implements AutoCloseable { - private static final int DEFAULT_WRAPPER_BACKGROUND = Color.WHITE; - - public static final Object sPoolSync = new Object(); + private static final Object sPoolSync = new Object(); private static LauncherIcons sPool; + private LauncherIcons next; /** * Return a new Message instance from the global pool. Allows us to @@ -81,7 +63,8 @@ public class LauncherIcons implements AutoCloseable { return m; } } - return new LauncherIcons(context); + InvariantDeviceProfile idp = LauncherAppState.getIDP(context); + return new LauncherIcons(context, idp.fillResIconDpi, idp.iconBitmapSize); } /** @@ -90,8 +73,7 @@ public class LauncherIcons implements AutoCloseable { public void recycle() { synchronized (sPoolSync) { // Clear any temporary state variables - mWrapperBackgroundColor = DEFAULT_WRAPPER_BACKGROUND; - mDisableColorExtractor = false; + clear(); next = sPool; sPool = this; @@ -103,269 +85,41 @@ public class LauncherIcons implements AutoCloseable { recycle(); } - private final Rect mOldBounds = new Rect(); private final Context mContext; - private final Canvas mCanvas; - private final PackageManager mPm; - private final ColorExtractor mColorExtractor; - private boolean mDisableColorExtractor; - private final int mFillResIconDpi; private final int mIconBitmapSize; - private IconNormalizer mNormalizer; - private ShadowGenerator mShadowGenerator; - - private Drawable mWrapperIcon; - private int mWrapperBackgroundColor = DEFAULT_WRAPPER_BACKGROUND; - - // sometimes we store linked lists of these things - private LauncherIcons next; - - private LauncherIcons(Context context) { + private LauncherIcons(Context context, int fillResIconDpi, int iconBitmapSize) { + super(context, fillResIconDpi, iconBitmapSize); mContext = context.getApplicationContext(); - mPm = mContext.getPackageManager(); - mColorExtractor = new ColorExtractor(); - - InvariantDeviceProfile idp = LauncherAppState.getIDP(mContext); - mFillResIconDpi = idp.fillResIconDpi; - mIconBitmapSize = idp.iconBitmapSize; - - mCanvas = new Canvas(); - mCanvas.setDrawFilter(new PaintFlagsDrawFilter(DITHER_FLAG, FILTER_BITMAP_FLAG)); + mFillResIconDpi = fillResIconDpi; + mIconBitmapSize = iconBitmapSize; } - public ShadowGenerator getShadowGenerator() { - if (mShadowGenerator == null) { - mShadowGenerator = new ShadowGenerator(mContext); - } - return mShadowGenerator; + public BitmapInfo createBadgedIconBitmap(Drawable icon, UserHandle user, + int iconAppTargetSdk) { + return createBadgedIconBitmap(icon, user, iconAppTargetSdk, false); } - public IconNormalizer getNormalizer() { - if (mNormalizer == null) { - mNormalizer = new IconNormalizer(mContext); - } - return mNormalizer; - } - - /** - * Returns a bitmap suitable for the all apps view. If the package or the resource do not - * exist, it returns null. - */ - public BitmapInfo createIconBitmap(ShortcutIconResource iconRes) { - try { - Resources resources = mPm.getResourcesForApplication(iconRes.packageName); - if (resources != null) { - final int id = resources.getIdentifier(iconRes.resourceName, null, null); - // do not stamp old legacy shortcuts as the app may have already forgotten about it - return createBadgedIconBitmap( - resources.getDrawableForDensity(id, mFillResIconDpi), - Process.myUserHandle() /* only available on primary user */, - 0 /* do not apply legacy treatment */); - } - } catch (Exception e) { - // Icon not found. - } - return null; - } - - /** - * Returns a bitmap which is of the appropriate size to be displayed as an icon - */ - public BitmapInfo createIconBitmap(Bitmap icon) { - if (mIconBitmapSize == icon.getWidth() && mIconBitmapSize == icon.getHeight()) { - return BitmapInfo.fromBitmap(icon); - } - return BitmapInfo.fromBitmap( - createIconBitmap(new BitmapDrawable(mContext.getResources(), icon), 1f)); - } - - /** - * Returns a bitmap suitable for displaying as an icon at various launcher UIs like all apps - * view or workspace. The icon is badged for {@param user}. - * The bitmap is also visually normalized with other icons. - */ - public BitmapInfo createBadgedIconBitmap(Drawable icon, UserHandle user, int iconAppTargetSdk) { - return createBadgedIconBitmap(icon, user, iconAppTargetSdk, false, null); - } - - public BitmapInfo createBadgedIconBitmap(Drawable icon, UserHandle user, int iconAppTargetSdk, - boolean isInstantApp) { + public BitmapInfo createBadgedIconBitmap(Drawable icon, UserHandle user, + int iconAppTargetSdk, boolean isInstantApp) { return createBadgedIconBitmap(icon, user, iconAppTargetSdk, isInstantApp, null); } - /** - * Returns a bitmap suitable for displaying as an icon at various launcher UIs like all apps - * view or workspace. The icon is badged for {@param user}. - * The bitmap is also visually normalized with other icons. - */ - public BitmapInfo createBadgedIconBitmap(Drawable icon, UserHandle user, int iconAppTargetSdk, - boolean isInstantApp, float[] scale) { - if (scale == null) { - scale = new float[1]; - } - icon = normalizeAndWrapToAdaptiveIcon(icon, iconAppTargetSdk, null, scale); - Bitmap bitmap = createIconBitmap(icon, scale[0]); - if (Utilities.ATLEAST_OREO && icon instanceof AdaptiveIconDrawable) { - mCanvas.setBitmap(bitmap); - getShadowGenerator().recreateIcon(Bitmap.createBitmap(bitmap), mCanvas); - mCanvas.setBitmap(null); - } - - final Bitmap result; - if (user != null && !Process.myUserHandle().equals(user)) { - BitmapDrawable drawable = new FixedSizeBitmapDrawable(bitmap); - Drawable badged = mPm.getUserBadgedIcon(drawable, user); - if (badged instanceof BitmapDrawable) { - result = ((BitmapDrawable) badged).getBitmap(); - } else { - result = createIconBitmap(badged, 1f); - } - } else if (isInstantApp) { - badgeWithDrawable(bitmap, mContext.getDrawable(R.drawable.ic_instant_app_badge)); - result = bitmap; - } else { - result = bitmap; - } - return BitmapInfo.fromBitmap(result, mDisableColorExtractor ? null : mColorExtractor); + public BitmapInfo createBadgedIconBitmap(Drawable icon, UserHandle user, + int iconAppTargetSdk, boolean isInstantApp, float[] scale) { + boolean shrinkNonAdaptiveIcons = Utilities.ATLEAST_P || + (Utilities.ATLEAST_OREO && iconAppTargetSdk >= Build.VERSION_CODES.O); + return createBadgedIconBitmap(icon, user, shrinkNonAdaptiveIcons, isInstantApp, scale); } - /** - * Creates a normalized bitmap suitable for the all apps view. The bitmap is also visually - * normalized with other icons and has enough spacing to add shadow. - */ public Bitmap createScaledBitmapWithoutShadow(Drawable icon, int iconAppTargetSdk) { - RectF iconBounds = new RectF(); - float[] scale = new float[1]; - icon = normalizeAndWrapToAdaptiveIcon(icon, iconAppTargetSdk, iconBounds, scale); - return createIconBitmap(icon, - Math.min(scale[0], ShadowGenerator.getScaleForBounds(iconBounds))); + boolean shrinkNonAdaptiveIcons = Utilities.ATLEAST_P || + (Utilities.ATLEAST_OREO && iconAppTargetSdk >= Build.VERSION_CODES.O); + return createScaledBitmapWithoutShadow(icon, shrinkNonAdaptiveIcons); } - /** - * Sets the background color used for wrapped adaptive icon - */ - public void setWrapperBackgroundColor(int color) { - mWrapperBackgroundColor = (Color.alpha(color) < 255) ? DEFAULT_WRAPPER_BACKGROUND : color; - } - - /** - * Disables the dominant color extraction for all icons loaded through this session (until - * this instance is recycled). - */ - public void disableColorExtraction() { - mDisableColorExtractor = true; - } - - private Drawable normalizeAndWrapToAdaptiveIcon(Drawable icon, int iconAppTargetSdk, - RectF outIconBounds, float[] outScale) { - float scale = 1f; - if ((Utilities.ATLEAST_OREO && iconAppTargetSdk >= Build.VERSION_CODES.O) || - Utilities.ATLEAST_P) { - boolean[] outShape = new boolean[1]; - if (mWrapperIcon == null) { - mWrapperIcon = mContext.getDrawable(R.drawable.adaptive_icon_drawable_wrapper) - .mutate(); - } - AdaptiveIconDrawable dr = (AdaptiveIconDrawable) mWrapperIcon; - dr.setBounds(0, 0, 1, 1); - scale = getNormalizer().getScale(icon, outIconBounds, dr.getIconMask(), outShape); - if (Utilities.ATLEAST_OREO && !outShape[0] && !(icon instanceof AdaptiveIconDrawable)) { - FixedScaleDrawable fsd = ((FixedScaleDrawable) dr.getForeground()); - fsd.setDrawable(icon); - fsd.setScale(scale); - icon = dr; - scale = getNormalizer().getScale(icon, outIconBounds, null, null); - - ((ColorDrawable) dr.getBackground()).setColor(mWrapperBackgroundColor); - } - } else { - scale = getNormalizer().getScale(icon, outIconBounds, null, null); - } - - outScale[0] = scale; - return icon; - } - - /** - * Adds the {@param badge} on top of {@param target} using the badge dimensions. - */ - public void badgeWithDrawable(Bitmap target, Drawable badge) { - mCanvas.setBitmap(target); - badgeWithDrawable(mCanvas, badge); - mCanvas.setBitmap(null); - } - - /** - * Adds the {@param badge} on top of {@param target} using the badge dimensions. - */ - private void badgeWithDrawable(Canvas target, Drawable badge) { - int badgeSize = mContext.getResources().getDimensionPixelSize(R.dimen.profile_badge_size); - badge.setBounds(mIconBitmapSize - badgeSize, mIconBitmapSize - badgeSize, - mIconBitmapSize, mIconBitmapSize); - badge.draw(target); - } - - /** - * @param scale the scale to apply before drawing {@param icon} on the canvas - */ - private Bitmap createIconBitmap(Drawable icon, float scale) { - int width = mIconBitmapSize; - int height = mIconBitmapSize; - - if (icon instanceof PaintDrawable) { - PaintDrawable painter = (PaintDrawable) icon; - painter.setIntrinsicWidth(width); - painter.setIntrinsicHeight(height); - } else if (icon instanceof BitmapDrawable) { - // Ensure the bitmap has a density. - BitmapDrawable bitmapDrawable = (BitmapDrawable) icon; - Bitmap bitmap = bitmapDrawable.getBitmap(); - if (bitmap != null && bitmap.getDensity() == Bitmap.DENSITY_NONE) { - bitmapDrawable.setTargetDensity(mContext.getResources().getDisplayMetrics()); - } - } - - int sourceWidth = icon.getIntrinsicWidth(); - int sourceHeight = icon.getIntrinsicHeight(); - if (sourceWidth > 0 && sourceHeight > 0) { - // Scale the icon proportionally to the icon dimensions - final float ratio = (float) sourceWidth / sourceHeight; - if (sourceWidth > sourceHeight) { - height = (int) (width / ratio); - } else if (sourceHeight > sourceWidth) { - width = (int) (height * ratio); - } - } - // no intrinsic size --> use default size - int textureWidth = mIconBitmapSize; - int textureHeight = mIconBitmapSize; - - Bitmap bitmap = Bitmap.createBitmap(textureWidth, textureHeight, - Bitmap.Config.ARGB_8888); - mCanvas.setBitmap(bitmap); - - final int left = (textureWidth-width) / 2; - final int top = (textureHeight-height) / 2; - - mOldBounds.set(icon.getBounds()); - if (Utilities.ATLEAST_OREO && icon instanceof AdaptiveIconDrawable) { - int offset = Math.max((int) Math.ceil(BLUR_FACTOR * textureWidth), Math.max(left, top)); - int size = Math.max(width, height); - icon.setBounds(offset, offset, size - offset, size - offset); - } else { - icon.setBounds(left, top, left+width, top+height); - } - mCanvas.save(); - mCanvas.scale(scale, scale, textureWidth / 2, textureHeight / 2); - icon.draw(mCanvas); - mCanvas.restore(); - icon.setBounds(mOldBounds); - mCanvas.setBitmap(null); - - return bitmap; - } + // below methods should also migrate to BaseIconFactory public BitmapInfo createShortcutIcon(ShortcutInfoCompat shortcutInfo) { return createShortcutIcon(shortcutInfo, true /* badged */); @@ -433,26 +187,4 @@ public class LauncherIcons implements AutoCloseable { return pkgInfo; } } - - /** - * An extension of {@link BitmapDrawable} which returns the bitmap pixel size as intrinsic size. - * This allows the badging to be done based on the action bitmap size rather than - * the scaled bitmap size. - */ - private static class FixedSizeBitmapDrawable extends BitmapDrawable { - - public FixedSizeBitmapDrawable(Bitmap bitmap) { - super(null, bitmap); - } - - @Override - public int getIntrinsicHeight() { - return getBitmap().getWidth(); - } - - @Override - public int getIntrinsicWidth() { - return getBitmap().getWidth(); - } - } } diff --git a/src/com/android/launcher3/model/AddWorkspaceItemsTask.java b/src/com/android/launcher3/model/AddWorkspaceItemsTask.java index fefe07d9be..756d44e814 100644 --- a/src/com/android/launcher3/model/AddWorkspaceItemsTask.java +++ b/src/com/android/launcher3/model/AddWorkspaceItemsTask.java @@ -34,6 +34,8 @@ import com.android.launcher3.LauncherSettings; import com.android.launcher3.ShortcutInfo; import com.android.launcher3.Utilities; import com.android.launcher3.util.GridOccupancy; +import com.android.launcher3.util.IntArray; + import java.util.ArrayList; import java.util.List; @@ -59,12 +61,12 @@ public class AddWorkspaceItemsTask extends BaseModelUpdateTask { Context context = app.getContext(); final ArrayList addedItemsFinal = new ArrayList<>(); - final ArrayList addedWorkspaceScreensFinal = new ArrayList<>(); + final IntArray addedWorkspaceScreensFinal = new IntArray(); // Get the list of workspace screens. We need to append to this list and // can not use sBgWorkspaceScreens because loadWorkspace() may not have been // called. - ArrayList workspaceScreens = LauncherModel.loadWorkspaceScreensDb(context); + IntArray workspaceScreens = LauncherModel.loadWorkspaceScreensDb(context); synchronized(dataModel) { List filteredItems = new ArrayList<>(); @@ -90,10 +92,9 @@ public class AddWorkspaceItemsTask extends BaseModelUpdateTask { for (ItemInfo item : filteredItems) { // Find appropriate space for the item. - Pair coords = findSpaceForItem(app, dataModel, workspaceScreens, + int[] coords = findSpaceForItem(app, dataModel, workspaceScreens, addedWorkspaceScreensFinal, item.spanX, item.spanY); - long screenId = coords.first; - int[] cordinates = coords.second; + int screenId = coords[0]; ItemInfo itemInfo; if (item instanceof ShortcutInfo || item instanceof FolderInfo || @@ -108,7 +109,7 @@ public class AddWorkspaceItemsTask extends BaseModelUpdateTask { // Add the shortcut to the db getModelWriter().addItemToDatabase(itemInfo, LauncherSettings.Favorites.CONTAINER_DESKTOP, screenId, - cordinates[0], cordinates[1]); + coords[1], coords[2]); // Save the ShortcutInfo for binding in the workspace addedItemsFinal.add(itemInfo); @@ -126,7 +127,7 @@ public class AddWorkspaceItemsTask extends BaseModelUpdateTask { final ArrayList addNotAnimated = new ArrayList<>(); if (!addedItemsFinal.isEmpty()) { ItemInfo info = addedItemsFinal.get(addedItemsFinal.size() - 1); - long lastScreenId = info.screenId; + int lastScreenId = info.screenId; for (ItemInfo i : addedItemsFinal) { if (i.screenId == lastScreenId) { addAnimated.add(i); @@ -142,7 +143,7 @@ public class AddWorkspaceItemsTask extends BaseModelUpdateTask { } } - protected void updateScreens(Context context, ArrayList workspaceScreens) { + protected void updateScreens(Context context, IntArray workspaceScreens) { LauncherModel.updateWorkspaceScreenOrder(context, workspaceScreens); } @@ -204,13 +205,10 @@ public class AddWorkspaceItemsTask extends BaseModelUpdateTask { /** * Find a position on the screen for the given size or adds a new screen. - * @return screenId and the coordinates for the item. + * @return screenId and the coordinates for the item in an int array of size 3. */ - protected Pair findSpaceForItem( - LauncherAppState app, BgDataModel dataModel, - ArrayList workspaceScreens, - ArrayList addedWorkspaceScreensFinal, - int spanX, int spanY) { + protected int[] findSpaceForItem( LauncherAppState app, BgDataModel dataModel, + IntArray workspaceScreens, IntArray addedWorkspaceScreensFinal, int spanX, int spanY) { LongSparseArray> screenItems = new LongSparseArray<>(); // Use sBgItemsIdMap as all the items are already loaded. @@ -228,7 +226,7 @@ public class AddWorkspaceItemsTask extends BaseModelUpdateTask { } // Find appropriate space for the item. - long screenId = 0; + int screenId = 0; int[] cordinates = new int[2]; boolean found = false; @@ -258,7 +256,7 @@ public class AddWorkspaceItemsTask extends BaseModelUpdateTask { // Still no position found. Add a new screen to the end. screenId = LauncherSettings.Settings.call(app.getContext().getContentResolver(), LauncherSettings.Settings.METHOD_NEW_SCREEN_ID) - .getLong(LauncherSettings.Settings.EXTRA_VALUE); + .getInt(LauncherSettings.Settings.EXTRA_VALUE); // Save the screen id for binding in the workspace workspaceScreens.add(screenId); @@ -270,7 +268,7 @@ public class AddWorkspaceItemsTask extends BaseModelUpdateTask { throw new RuntimeException("Can't find space to add the item"); } } - return Pair.create(screenId, cordinates); + return new int[] {screenId, cordinates[0], cordinates[1]}; } private boolean findNextAvailableIconSpaceInScreen( diff --git a/src/com/android/launcher3/model/BgDataModel.java b/src/com/android/launcher3/model/BgDataModel.java index fff1e69913..81eefc4d8b 100644 --- a/src/com/android/launcher3/model/BgDataModel.java +++ b/src/com/android/launcher3/model/BgDataModel.java @@ -36,7 +36,8 @@ import com.android.launcher3.shortcuts.DeepShortcutManager; import com.android.launcher3.shortcuts.ShortcutInfoCompat; import com.android.launcher3.shortcuts.ShortcutKey; import com.android.launcher3.util.ComponentKey; -import com.android.launcher3.util.LongArrayMap; +import com.android.launcher3.util.IntArray; +import com.android.launcher3.util.IntSparseArrayMap; import com.android.launcher3.util.MultiHashMap; import com.google.protobuf.nano.MessageNano; @@ -62,7 +63,7 @@ public class BgDataModel { * Map of all the ItemInfos (shortcuts, folders, and widgets) created by * LauncherModel to their ids */ - public final LongArrayMap itemsIdMap = new LongArrayMap<>(); + public final IntSparseArrayMap itemsIdMap = new IntSparseArrayMap<>(); /** * List of all the folders and shortcuts directly on the home screen (no widgets @@ -78,12 +79,12 @@ public class BgDataModel { /** * Map of id to FolderInfos of all the folders created by LauncherModel */ - public final LongArrayMap folders = new LongArrayMap<>(); + public final IntSparseArrayMap folders = new IntSparseArrayMap<>(); /** * Ordered list of workspace screens ids. */ - public final ArrayList workspaceScreens = new ArrayList<>(); + public final IntArray workspaceScreens = new IntArray(); /** * Map of ShortcutKey to the number of times it is pinned. @@ -132,7 +133,7 @@ public class BgDataModel { writer.println(prefix + "Data Model:"); writer.print(prefix + " ---- workspace screens: "); for (int i = 0; i < workspaceScreens.size(); i++) { - writer.print(" " + workspaceScreens.get(i).toString()); + writer.print(" " + workspaceScreens.get(i)); } writer.println(); writer.println(prefix + " ---- workspace items "); @@ -169,7 +170,7 @@ public class BgDataModel { // Add top parent nodes. (L1) DumpTargetWrapper hotseat = new DumpTargetWrapper(ContainerType.HOTSEAT, 0); - LongArrayMap workspaces = new LongArrayMap<>(); + IntSparseArrayMap workspaces = new IntSparseArrayMap<>(); for (int i = 0; i < workspaceScreens.size(); i++) { workspaces.put(workspaceScreens.get(i), new DumpTargetWrapper(ContainerType.WORKSPACE, i)); @@ -346,7 +347,7 @@ public class BgDataModel { * Return an existing FolderInfo object if we have encountered this ID previously, * or make a new one. */ - public synchronized FolderInfo findOrMakeFolder(long id) { + public synchronized FolderInfo findOrMakeFolder(int id) { // See if a placeholder was created for us already FolderInfo folderInfo = folders.get(id); if (folderInfo == null) { diff --git a/src/com/android/launcher3/model/GridSizeMigrationTask.java b/src/com/android/launcher3/model/GridSizeMigrationTask.java index 12daea50fd..2c1aa745f0 100644 --- a/src/com/android/launcher3/model/GridSizeMigrationTask.java +++ b/src/com/android/launcher3/model/GridSizeMigrationTask.java @@ -11,8 +11,8 @@ import android.content.pm.PackageManager; import android.database.Cursor; import android.graphics.Point; import android.net.Uri; -import android.text.TextUtils; import android.util.Log; + import com.android.launcher3.InvariantDeviceProfile; import com.android.launcher3.ItemInfo; import com.android.launcher3.LauncherAppState; @@ -27,7 +27,9 @@ import com.android.launcher3.compat.AppWidgetManagerCompat; import com.android.launcher3.compat.PackageInstallerCompat; import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.util.GridOccupancy; -import com.android.launcher3.util.LongArrayMap; +import com.android.launcher3.util.IntArray; +import com.android.launcher3.util.IntSparseArrayMap; + import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; @@ -59,7 +61,7 @@ public class GridSizeMigrationTask { private final InvariantDeviceProfile mIdp; private final ContentValues mTempValues = new ContentValues(); - protected final ArrayList mEntryToRemove = new ArrayList<>(); + protected final IntArray mEntryToRemove = new IntArray(); private final ArrayList mUpdateOperations = new ArrayList<>(); protected final ArrayList mCarryOver = new ArrayList<>(); private final HashSet mValidPackages; @@ -108,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 { @@ -118,7 +121,7 @@ public class GridSizeMigrationTask { if (!mEntryToRemove.isEmpty()) { if (DEBUG) { - Log.d(TAG, "Removing items: " + TextUtils.join(", ", mEntryToRemove)); + Log.d(TAG, "Removing items: " + mEntryToRemove.toConcatString()); } mContext.getContentResolver().delete(LauncherSettings.Favorites.CONTENT_URI, Utilities.createDbSelectionQuery( @@ -134,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 { @@ -177,12 +181,13 @@ public class GridSizeMigrationTask { * @return true if any DB change was made */ protected boolean migrateWorkspace() throws Exception { - ArrayList allScreens = LauncherModel.loadWorkspaceScreensDb(mContext); + IntArray allScreens = LauncherModel.loadWorkspaceScreensDb(mContext); if (allScreens.isEmpty()) { throw new Exception("Unable to get workspace screens"); } - for (long screenId : allScreens) { + for (int i = 0; i < allScreens.size(); i++) { + int screenId = allScreens.get(i); if (DEBUG) { Log.d(TAG, "Migrating " + screenId); } @@ -190,7 +195,7 @@ public class GridSizeMigrationTask { } if (!mCarryOver.isEmpty()) { - LongArrayMap itemMap = new LongArrayMap<>(); + IntSparseArrayMap itemMap = new IntSparseArrayMap<>(); for (DbEntry e : mCarryOver) { itemMap.put(e.id, e); } @@ -205,10 +210,10 @@ public class GridSizeMigrationTask { new GridOccupancy(mTrgX, mTrgY), deepCopy(mCarryOver), 0, true); placement.find(); if (placement.finalPlacedItems.size() > 0) { - long newScreenId = LauncherSettings.Settings.call( + int newScreenId = LauncherSettings.Settings.call( mContext.getContentResolver(), LauncherSettings.Settings.METHOD_NEW_SCREEN_ID) - .getLong(LauncherSettings.Settings.EXTRA_VALUE); + .getInt(LauncherSettings.Settings.EXTRA_VALUE); allScreens.add(newScreenId); for (DbEntry item : placement.finalPlacedItems) { @@ -230,10 +235,11 @@ public class GridSizeMigrationTask { int count = allScreens.size(); for (int i = 0; i < count; i++) { ContentValues v = new ContentValues(); - long screenId = allScreens.get(i); + 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(); @@ -249,9 +255,10 @@ public class GridSizeMigrationTask { * 3) If all those items from the above list can be placed on this screen, place them * (otherwise they are placed on a new screen). */ - protected void migrateScreen(long screenId) { + protected void migrateScreen(int screenId) { // If we are migrating the first screen, do not touch the first row. - int startY = (FeatureFlags.QSB_ON_FIRST_SCREEN && screenId == Workspace.FIRST_SCREEN_ID) + int startY = + (FeatureFlags.QSB_ON_FIRST_SCREEN.get() && screenId == Workspace.FIRST_SCREEN_ID) ? 1 : 0; ArrayList items = loadWorkspaceEntries(screenId); @@ -276,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 itemsOnScreen = tryRemove(x, y, startY, deepCopy(items), outLoss); + ArrayList 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; @@ -302,7 +311,7 @@ public class GridSizeMigrationTask { removedRow, removedCol, screenId)); } - LongArrayMap itemMap = new LongArrayMap<>(); + IntSparseArrayMap itemMap = new IntSparseArrayMap<>(); for (DbEntry e : deepCopy(items)) { itemMap.put(e.id, e); } @@ -359,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. @@ -434,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 @@ -546,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; @@ -613,9 +625,9 @@ public class GridSizeMigrationTask { ArrayList entries = new ArrayList<>(); while (c.moveToNext()) { DbEntry entry = new DbEntry(); - entry.id = c.getLong(indexId); + entry.id = c.getInt(indexId); entry.itemType = c.getInt(indexItemType); - entry.screenId = c.getLong(indexScreen); + entry.screenId = c.getInt(indexScreen); if (entry.screenId >= mSrcHotseatSize) { mEntryToRemove.add(entry.id); @@ -661,7 +673,7 @@ public class GridSizeMigrationTask { /** * Loads entries for a particular screen id. */ - protected ArrayList loadWorkspaceEntries(long screen) { + protected ArrayList loadWorkspaceEntries(int screen) { Cursor c = queryWorkspace( new String[]{ Favorites._ID, // 0 @@ -689,7 +701,7 @@ public class GridSizeMigrationTask { ArrayList entries = new ArrayList<>(); while (c.moveToNext()) { DbEntry entry = new DbEntry(); - entry.id = c.getLong(indexId); + entry.id = c.getInt(indexId); entry.itemType = c.getInt(indexItemType); entry.cellX = c.getInt(indexCellX); entry.cellY = c.getInt(indexCellY); @@ -762,7 +774,7 @@ public class GridSizeMigrationTask { /** * @return the number of valid items in the folder. */ - private int getFolderItemsCount(long folderId) { + private int getFolderItemsCount(int folderId) { Cursor c = queryWorkspace( new String[]{Favorites._ID, Favorites.INTENT}, Favorites.CONTAINER + " = " + folderId); @@ -773,7 +785,7 @@ public class GridSizeMigrationTask { verifyIntent(c.getString(1)); total++; } catch (Exception e) { - mEntryToRemove.add(c.getLong(0)); + mEntryToRemove.add(c.getInt(0)); } } c.close(); @@ -811,7 +823,8 @@ public class GridSizeMigrationTask { public float weight; - public DbEntry() { } + public DbEntry() { + } public DbEntry copy() { DbEntry entry = new DbEntry(); @@ -883,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) { @@ -892,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; } @@ -903,7 +918,8 @@ public class GridSizeMigrationTask { HashSet 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. @@ -916,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; } @@ -966,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 LongArrayMap removeBrokenHotseatItems(Context context) throws Exception { + public static IntSparseArrayMap removeBrokenHotseatItems(Context context) + throws Exception { GridSizeMigrationTask task = new GridSizeMigrationTask( context, LauncherAppState.getIDP(context), getValidPackages(context), Integer.MAX_VALUE, Integer.MAX_VALUE); @@ -977,7 +996,7 @@ public class GridSizeMigrationTask { ArrayList items = task.loadHotseatEntries(); // Delete any entry marked for deletion by above load. task.applyOperations(); - LongArrayMap positions = new LongArrayMap<>(); + IntSparseArrayMap positions = new IntSparseArrayMap<>(); for (DbEntry item : items) { positions.put(item.screenId, item); } diff --git a/src/com/android/launcher3/model/LoaderCursor.java b/src/com/android/launcher3/model/LoaderCursor.java index 87aef02f76..94cf5c286a 100644 --- a/src/com/android/launcher3/model/LoaderCursor.java +++ b/src/com/android/launcher3/model/LoaderCursor.java @@ -48,11 +48,11 @@ import com.android.launcher3.icons.LauncherIcons; import com.android.launcher3.logging.FileLog; import com.android.launcher3.util.ContentWriter; import com.android.launcher3.util.GridOccupancy; -import com.android.launcher3.util.LongArrayMap; +import com.android.launcher3.util.IntArray; +import com.android.launcher3.util.IntSparseArrayMap; import java.net.URISyntaxException; import java.security.InvalidParameterException; -import java.util.ArrayList; /** * Extension of {@link Cursor} with utility methods for workspace loading. @@ -68,9 +68,9 @@ public class LoaderCursor extends CursorWrapper { private final IconCache mIconCache; private final InvariantDeviceProfile mIDP; - private final ArrayList itemsToRemove = new ArrayList<>(); - private final ArrayList restoredRows = new ArrayList<>(); - private final LongArrayMap occupied = new LongArrayMap<>(); + private final IntArray itemsToRemove = new IntArray(); + private final IntArray restoredRows = new IntArray(); + private final IntSparseArrayMap occupied = new IntSparseArrayMap<>(); private final int iconPackageIndex; private final int iconResourceIndex; @@ -90,8 +90,8 @@ public class LoaderCursor extends CursorWrapper { // Properties loaded per iteration public long serialNumber; public UserHandle user; - public long id; - public long container; + public int id; + public int container; public int itemType; public int restoreFlag; @@ -126,7 +126,7 @@ public class LoaderCursor extends CursorWrapper { // Load common properties. itemType = getInt(itemTypeIndex); container = getInt(containerIndex); - id = getLong(idIndex); + id = getInt(idIndex); serialNumber = getInt(profileIdIndex); user = allUsers.get(serialNumber); restoreFlag = getInt(restoredIndex); @@ -293,7 +293,7 @@ public class LoaderCursor extends CursorWrapper { */ public ContentWriter updater() { return new ContentWriter(mContext, new ContentWriter.CommitParams( - BaseColumns._ID + "= ?", new String[]{Long.toString(id)})); + BaseColumns._ID + "= ?", new String[]{Integer.toString(id)})); } /** @@ -383,11 +383,11 @@ public class LoaderCursor extends CursorWrapper { /** * check & update map of what's occupied; used to discard overlapping/invalid items */ - protected boolean checkItemPlacement(ItemInfo item, ArrayList workspaceScreens) { - long containerIndex = item.screenId; + protected boolean checkItemPlacement(ItemInfo item, IntArray workspaceScreens) { + int containerIndex = item.screenId; if (item.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) { final GridOccupancy hotseatOccupancy = - occupied.get((long) LauncherSettings.Favorites.CONTAINER_HOTSEAT); + occupied.get(LauncherSettings.Favorites.CONTAINER_HOTSEAT); if (item.screenId >= mIDP.numHotseatIcons) { Log.e(TAG, "Error loading shortcut " + item @@ -404,17 +404,17 @@ public class LoaderCursor extends CursorWrapper { + item.cellY + ") already occupied"); return false; } else { - hotseatOccupancy.cells[(int) item.screenId][0] = true; + hotseatOccupancy.cells[item.screenId][0] = true; return true; } } else { final GridOccupancy occupancy = new GridOccupancy(mIDP.numHotseatIcons, 1); - occupancy.cells[(int) item.screenId][0] = true; - occupied.put((long) LauncherSettings.Favorites.CONTAINER_HOTSEAT, occupancy); + occupancy.cells[item.screenId][0] = true; + occupied.put(LauncherSettings.Favorites.CONTAINER_HOTSEAT, occupancy); return true; } } else if (item.container == LauncherSettings.Favorites.CONTAINER_DESKTOP) { - if (!workspaceScreens.contains((Long) item.screenId)) { + if (!workspaceScreens.contains(item.screenId)) { // The item has an invalid screen id. return false; } @@ -440,7 +440,8 @@ public class LoaderCursor extends CursorWrapper { if (item.screenId == Workspace.FIRST_SCREEN_ID) { // 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.QSB_ON_FIRST_SCREEN); + screen.markCells(0, 0, countX + 1, 1, + FeatureFlags.QSB_ON_FIRST_SCREEN.get()); } occupied.put(item.screenId, screen); } diff --git a/src/com/android/launcher3/model/LoaderResults.java b/src/com/android/launcher3/model/LoaderResults.java index 57789363a0..2c15df1925 100644 --- a/src/com/android/launcher3/model/LoaderResults.java +++ b/src/com/android/launcher3/model/LoaderResults.java @@ -29,9 +29,10 @@ import com.android.launcher3.LauncherModel.Callbacks; import com.android.launcher3.LauncherSettings; import com.android.launcher3.MainThreadExecutor; import com.android.launcher3.PagedView; -import com.android.launcher3.Utilities; import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.util.ComponentKey; +import com.android.launcher3.util.IntArray; +import com.android.launcher3.util.IntSet; import com.android.launcher3.util.LooperIdleLock; import com.android.launcher3.util.MultiHashMap; import com.android.launcher3.util.ViewOnDrawExecutor; @@ -52,7 +53,7 @@ import java.util.concurrent.Executor; public class LoaderResults { private static final String TAG = "LoaderResults"; - private static final long INVALID_SCREEN_ID = -1L; + private static final int INVALID_SCREEN_ID = -1; private static final int ITEMS_CHUNK = 6; // batch size for the workspace icons private final Executor mUiExecutor; @@ -92,7 +93,7 @@ public class LoaderResults { // Save a copy of all the bg-thread collections ArrayList workspaceItems = new ArrayList<>(); ArrayList appWidgets = new ArrayList<>(); - final ArrayList orderedScreenIds = new ArrayList<>(); + final IntArray orderedScreenIds = new IntArray(); synchronized (mBgDataModel) { workspaceItems.addAll(mBgDataModel.workspaceItems); @@ -112,7 +113,7 @@ public class LoaderResults { currentScreen = currScreen; } final boolean validFirstPage = currentScreen >= 0; - final long currentScreenId = + final int currentScreenId = validFirstPage ? orderedScreenIds.get(currentScreen) : INVALID_SCREEN_ID; // Separate the items that are on the current screen, and all the other remaining items @@ -209,7 +210,7 @@ public class LoaderResults { /** Filters the set of items who are directly or indirectly (via another container) on the * specified screen. */ - public static void filterCurrentWorkspaceItems(long currentScreenId, + public static void filterCurrentWorkspaceItems(int currentScreenId, ArrayList allWorkspaceItems, ArrayList currentScreenItems, ArrayList otherScreenItems) { @@ -225,13 +226,10 @@ public class LoaderResults { // Order the set of items by their containers first, this allows use to walk through the // list sequentially, build up a list of containers that are in the specified screen, // as well as all items in those containers. - Set itemsOnScreen = new HashSet<>(); - Collections.sort(allWorkspaceItems, new Comparator() { - @Override - public int compare(ItemInfo lhs, ItemInfo rhs) { - return Utilities.longCompare(lhs.container, rhs.container); - } - }); + IntSet itemsOnScreen = new IntSet(); + Collections.sort(allWorkspaceItems, + (lhs, rhs) -> Integer.compare(lhs.container, rhs.container)); + for (T info : allWorkspaceItems) { if (info.container == LauncherSettings.Favorites.CONTAINER_DESKTOP) { if (info.screenId == currentScreenId) { @@ -267,15 +265,15 @@ public class LoaderResults { // Within containers, order by their spatial position in that container switch ((int) lhs.container) { case LauncherSettings.Favorites.CONTAINER_DESKTOP: { - long lr = (lhs.screenId * screenCellCount + + int lr = (lhs.screenId * screenCellCount + lhs.cellY * screenCols + lhs.cellX); - long rr = (rhs.screenId * screenCellCount + + int rr = (rhs.screenId * screenCellCount + rhs.cellY * screenCols + rhs.cellX); - return Utilities.longCompare(lr, rr); + return Integer.compare(lr, rr); } case LauncherSettings.Favorites.CONTAINER_HOTSEAT: { // We currently use the screen id as the rank - return Utilities.longCompare(lhs.screenId, rhs.screenId); + return Integer.compare(lhs.screenId, rhs.screenId); } default: if (FeatureFlags.IS_DOGFOOD_BUILD) { @@ -286,7 +284,7 @@ public class LoaderResults { } } else { // Between containers, order by hotseat, desktop - return Utilities.longCompare(lhs.container, rhs.container); + return Integer.compare(lhs.container, rhs.container); } } }); @@ -296,6 +294,11 @@ public class LoaderResults { final ArrayList appWidgets, final Executor executor) { + if (com.android.launcher3.Utilities.IS_RUNNING_IN_TEST_HARNESS + && com.android.launcher3.Utilities.IS_DEBUG_DEVICE) { + android.util.Log.d("b/117332845", + android.util.Log.getStackTraceString(new Throwable())); + } // Bind the workspace items int N = workspaceItems.size(); for (int i = 0; i < N; i += ITEMS_CHUNK) { diff --git a/src/com/android/launcher3/model/LoaderTask.java b/src/com/android/launcher3/model/LoaderTask.java index e0da6b172b..1ec7af2bb8 100644 --- a/src/com/android/launcher3/model/LoaderTask.java +++ b/src/com/android/launcher3/model/LoaderTask.java @@ -70,6 +70,7 @@ import com.android.launcher3.shortcuts.DeepShortcutManager; import com.android.launcher3.shortcuts.ShortcutInfoCompat; import com.android.launcher3.shortcuts.ShortcutKey; import com.android.launcher3.util.ComponentKey; +import com.android.launcher3.util.IntArray; import com.android.launcher3.util.LooperIdleLock; import com.android.launcher3.util.MultiHashMap; import com.android.launcher3.util.PackageManagerHelper; @@ -124,6 +125,11 @@ public class LoaderTask implements Runnable { mPackageInstaller = PackageInstallerCompat.getInstance(mApp.getContext()); mAppWidgetManager = AppWidgetManagerCompat.getInstance(mApp.getContext()); mIconCache = mApp.getIconCache(); + if (com.android.launcher3.Utilities.IS_RUNNING_IN_TEST_HARNESS + && com.android.launcher3.Utilities.IS_DEBUG_DEVICE) { + android.util.Log.d("b/117332845", + android.util.Log.getStackTraceString(new Throwable())); + } } protected synchronized void waitForIdle() { @@ -150,7 +156,7 @@ public class LoaderTask implements Runnable { allItems.addAll(mBgDataModel.workspaceItems); allItems.addAll(mBgDataModel.appWidgets); } - long firstScreen = mBgDataModel.workspaceScreens.isEmpty() + int firstScreen = mBgDataModel.workspaceScreens.isEmpty() ? -1 // In this case, we can still look at the items in the hotseat. : mBgDataModel.workspaceScreens.get(0); filterCurrentWorkspaceItems(firstScreen, allItems, firstScreenItems, @@ -714,11 +720,11 @@ public class LoaderTask implements Runnable { // Remove dead items if (c.commitDeleted()) { // Remove any empty folder - ArrayList deletedFolderIds = (ArrayList) LauncherSettings.Settings + int[] deletedFolderIds = LauncherSettings.Settings .call(contentResolver, LauncherSettings.Settings.METHOD_DELETE_EMPTY_FOLDERS) - .getSerializable(LauncherSettings.Settings.EXTRA_VALUE); - for (long folderId : deletedFolderIds) { + .getIntArray(LauncherSettings.Settings.EXTRA_VALUE); + for (int folderId : deletedFolderIds) { mBgDataModel.workspaceItems.remove(mBgDataModel.folders.get(folderId)); mBgDataModel.folders.remove(folderId); mBgDataModel.itemsIdMap.remove(folderId); @@ -773,18 +779,18 @@ public class LoaderTask implements Runnable { } // Remove any empty screens - ArrayList unusedScreens = new ArrayList<>(mBgDataModel.workspaceScreens); + IntArray unusedScreens = mBgDataModel.workspaceScreens.clone(); for (ItemInfo item: mBgDataModel.itemsIdMap) { - long screenId = item.screenId; + int screenId = item.screenId; if (item.container == LauncherSettings.Favorites.CONTAINER_DESKTOP && unusedScreens.contains(screenId)) { - unusedScreens.remove(screenId); + unusedScreens.removeValue(screenId); } } // If there are any empty screens remove them, and update. if (unusedScreens.size() != 0) { - mBgDataModel.workspaceScreens.removeAll(unusedScreens); + mBgDataModel.workspaceScreens.removeAllValues(unusedScreens); LauncherModel.updateWorkspaceScreenOrder(context, mBgDataModel.workspaceScreens); } } diff --git a/src/com/android/launcher3/model/ModelWriter.java b/src/com/android/launcher3/model/ModelWriter.java index 9f6e4f85d4..f7961d5d87 100644 --- a/src/com/android/launcher3/model/ModelWriter.java +++ b/src/com/android/launcher3/model/ModelWriter.java @@ -79,7 +79,7 @@ public class ModelWriter { } private void updateItemInfoProps( - ItemInfo item, long container, long screenId, int cellX, int cellY) { + ItemInfo item, int container, int screenId, int cellX, int cellY) { item.container = container; item.cellX = cellX; item.cellY = cellY; @@ -98,7 +98,7 @@ public class ModelWriter { * */ public void addOrMoveItemInDatabase(ItemInfo item, - long container, long screenId, int cellX, int cellY) { + int container, int screenId, int cellX, int cellY) { if (item.container == ItemInfo.NO_ID) { // From all apps addItemToDatabase(item, container, screenId, cellX, cellY); @@ -108,7 +108,12 @@ public class ModelWriter { } } - private void checkItemInfoLocked(long itemId, ItemInfo item, StackTraceElement[] stackTrace) { + private void checkItemInfoLocked(int itemId, ItemInfo item, StackTraceElement[] stackTrace) { + if (com.android.launcher3.Utilities.IS_RUNNING_IN_TEST_HARNESS + && com.android.launcher3.Utilities.IS_DEBUG_DEVICE) { + android.util.Log.d("b/117332845", + "Checking item: " + android.util.Log.getStackTraceString(new Throwable())); + } ItemInfo modelItem = mBgDataModel.itemsIdMap.get(itemId); if (modelItem != null && item != modelItem) { // check all the data is consistent @@ -149,7 +154,7 @@ public class ModelWriter { * Move an item in the DB to a new */ public void moveItemInDatabase(final ItemInfo item, - long container, long screenId, int cellX, int cellY) { + int container, int screenId, int cellX, int cellY) { updateItemInfoProps(item, container, screenId, cellX, cellY); final ContentWriter writer = new ContentWriter(mContext) @@ -166,7 +171,7 @@ public class ModelWriter { * Move items in the DB to a new . We assume that the * cellX, cellY have already been updated on the ItemInfos. */ - public void moveItemsInDatabase(final ArrayList items, long container, int screen) { + public void moveItemsInDatabase(final ArrayList items, int container, int screen) { ArrayList contentValues = new ArrayList<>(); int count = items.size(); @@ -190,7 +195,7 @@ public class ModelWriter { * Move and/or resize item in the DB to a new */ public void modifyItemInDatabase(final ItemInfo item, - long container, long screenId, int cellX, int cellY, int spanX, int spanY) { + int container, int screenId, int cellX, int cellY, int spanX, int spanY) { updateItemInfoProps(item, container, screenId, cellX, cellY); item.spanX = spanX; item.spanY = spanY; @@ -221,14 +226,14 @@ public class ModelWriter { * cellY fields of the item. Also assigns an ID to the item. */ public void addItemToDatabase(final ItemInfo item, - long container, long screenId, int cellX, int cellY) { + int container, int screenId, int cellX, int cellY) { updateItemInfoProps(item, container, screenId, cellX, cellY); final ContentWriter writer = new ContentWriter(mContext); final ContentResolver cr = mContext.getContentResolver(); item.onAddToDatabase(writer); - item.id = Settings.call(cr, Settings.METHOD_NEW_ITEM_ID).getLong(Settings.EXTRA_VALUE); + item.id = Settings.call(cr, Settings.METHOD_NEW_ITEM_ID).getInt(Settings.EXTRA_VALUE); writer.put(Favorites._ID, item.id); ModelVerifier verifier = new ModelVerifier(); @@ -355,9 +360,14 @@ public class ModelWriter { private class UpdateItemRunnable extends UpdateItemBaseRunnable { private final ItemInfo mItem; private final ContentWriter mWriter; - private final long mItemId; + private final int mItemId; UpdateItemRunnable(ItemInfo item, ContentWriter writer) { + if (com.android.launcher3.Utilities.IS_RUNNING_IN_TEST_HARNESS + && com.android.launcher3.Utilities.IS_DEBUG_DEVICE) { + android.util.Log.d("b/117332845", + android.util.Log.getStackTraceString(new Throwable())); + } mItem = item; mWriter = writer; mItemId = item.id; @@ -386,7 +396,7 @@ public class ModelWriter { int count = mItems.size(); for (int i = 0; i < count; i++) { ItemInfo item = mItems.get(i); - final long itemId = item.id; + final int itemId = item.id; final Uri uri = Favorites.getContentUri(itemId); ContentValues values = mValues.get(i); @@ -409,7 +419,7 @@ public class ModelWriter { mStackTrace = new Throwable().getStackTrace(); } - protected void updateItemArrays(ItemInfo item, long itemId) { + protected void updateItemArrays(ItemInfo item, int itemId) { // Lock on mBgLock *after* the db operation synchronized (mBgDataModel) { checkItemInfoLocked(itemId, item, mStackTrace); diff --git a/src/com/android/launcher3/model/PackageUpdatedTask.java b/src/com/android/launcher3/model/PackageUpdatedTask.java index c004e217b1..201a63e116 100644 --- a/src/com/android/launcher3/model/PackageUpdatedTask.java +++ b/src/com/android/launcher3/model/PackageUpdatedTask.java @@ -44,8 +44,8 @@ import com.android.launcher3.logging.FileLog; import com.android.launcher3.shortcuts.DeepShortcutManager; import com.android.launcher3.shortcuts.ShortcutInfoCompat; import com.android.launcher3.util.FlagOp; +import com.android.launcher3.util.IntSparseArrayMap; import com.android.launcher3.util.ItemInfoMatcher; -import com.android.launcher3.util.LongArrayMap; import com.android.launcher3.util.PackageManagerHelper; import com.android.launcher3.util.PackageUserKey; @@ -171,7 +171,7 @@ public class PackageUpdatedTask extends BaseModelUpdateTask { } } - final LongArrayMap removedShortcuts = new LongArrayMap<>(); + final IntSparseArrayMap removedShortcuts = new IntSparseArrayMap<>(); // Update shortcut infos if (mOp == OP_ADD || flagOp != FlagOp.NO_OP) { diff --git a/src/com/android/launcher3/notification/NotificationListener.java b/src/com/android/launcher3/notification/NotificationListener.java index 4c85c8b17a..f27b72819a 100644 --- a/src/com/android/launcher3/notification/NotificationListener.java +++ b/src/com/android/launcher3/notification/NotificationListener.java @@ -16,7 +16,7 @@ package com.android.launcher3.notification; -import static com.android.launcher3.SettingsActivity.NOTIFICATION_BADGING; +import static com.android.launcher3.util.SecureSettingsObserver.newNotificationSettingsObserver; import android.annotation.TargetApi; import android.app.Notification; @@ -28,13 +28,13 @@ import android.os.Message; import android.service.notification.NotificationListenerService; import android.service.notification.StatusBarNotification; import android.text.TextUtils; -import android.util.ArraySet; import android.util.Log; import android.util.Pair; import com.android.launcher3.LauncherModel; +import com.android.launcher3.util.IntSet; import com.android.launcher3.util.PackageUserKey; -import com.android.launcher3.util.SettingsObserver; +import com.android.launcher3.util.SecureSettingsObserver; import java.util.ArrayList; import java.util.Arrays; @@ -42,7 +42,6 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Set; import androidx.annotation.Nullable; @@ -78,7 +77,7 @@ public class NotificationListener extends NotificationListenerService { /** The last notification key that was dismissed from launcher UI */ private String mLastKeyDismissedByLauncher; - private SettingsObserver mNotificationBadgingObserver; + private SecureSettingsObserver mNotificationBadgingObserver; private final Handler.Callback mWorkerCallback = new Handler.Callback() { @Override @@ -195,19 +194,20 @@ public class NotificationListener extends NotificationListenerService { super.onListenerConnected(); sIsConnected = true; - mNotificationBadgingObserver = new SettingsObserver.Secure(getContentResolver()) { - @Override - public void onSettingChanged(boolean isNotificationBadgingEnabled) { - if (!isNotificationBadgingEnabled && sIsConnected) { - requestUnbind(); - } - } - }; - mNotificationBadgingObserver.register(NOTIFICATION_BADGING); + mNotificationBadgingObserver = + newNotificationSettingsObserver(this, this::onNotificationBadgingChanged); + mNotificationBadgingObserver.register(); + mNotificationBadgingObserver.dispatchOnChange(); onNotificationFullRefresh(); } + private void onNotificationBadgingChanged(boolean isNotificationBadgingEnabled) { + if (!isNotificationBadgingEnabled && sIsConnected) { + requestUnbind(); + } + } + private void onNotificationFullRefresh() { mWorkerHandler.obtainMessage(MSG_NOTIFICATION_FULL_REFRESH).sendToTarget(); } @@ -346,7 +346,7 @@ public class NotificationListener extends NotificationListenerService { private List filterNotifications( StatusBarNotification[] notifications) { if (notifications == null) return null; - Set removedNotifications = new ArraySet<>(); + IntSet removedNotifications = new IntSet(); for (int i = 0; i < notifications.length; i++) { if (shouldBeFilteredOut(notifications[i])) { removedNotifications.add(i); diff --git a/src/com/android/launcher3/popup/PopupContainerWithArrow.java b/src/com/android/launcher3/popup/PopupContainerWithArrow.java index 6877cc4ad5..b9e6a98b45 100644 --- a/src/com/android/launcher3/popup/PopupContainerWithArrow.java +++ b/src/com/android/launcher3/popup/PopupContainerWithArrow.java @@ -391,13 +391,10 @@ public class PopupContainerWithArrow extends ArrowPopup implements DragSource, if (view instanceof DeepShortcutView) { // Expanded system shortcut, with both icon and text shown on white background. final DeepShortcutView shortcutView = (DeepShortcutView) view; - shortcutView.getIconView().setBackgroundResource(info.iconResId); - shortcutView.getBubbleText().setText(info.labelResId); + info.setIconAndLabelFor(shortcutView.getIconView(), shortcutView.getBubbleText()); } else if (view instanceof ImageView) { // Only the system shortcut icon shows on a gray background header. - final ImageView shortcutIcon = (ImageView) view; - shortcutIcon.setImageResource(info.iconResId); - shortcutIcon.setContentDescription(getContext().getText(info.labelResId)); + info.setIconAndContentDescriptionFor((ImageView) view); } view.setTag(info); view.setOnClickListener(info.getOnClickListener(mLauncher, diff --git a/src/com/android/launcher3/popup/SystemShortcut.java b/src/com/android/launcher3/popup/SystemShortcut.java index 693e532424..b80ba8abe1 100644 --- a/src/com/android/launcher3/popup/SystemShortcut.java +++ b/src/com/android/launcher3/popup/SystemShortcut.java @@ -3,10 +3,15 @@ package com.android.launcher3.popup; import static com.android.launcher3.userevent.nano.LauncherLogProto.Action; import static com.android.launcher3.userevent.nano.LauncherLogProto.ControlType; +import android.content.Context; import android.content.Intent; import android.graphics.Rect; +import android.graphics.drawable.Drawable; import android.os.Bundle; import android.view.View; +import android.view.accessibility.AccessibilityNodeInfo; +import android.widget.ImageView; +import android.widget.TextView; import com.android.launcher3.AbstractFloatingView; import com.android.launcher3.BaseDraggingActivity; @@ -23,18 +28,82 @@ import com.android.launcher3.widget.WidgetsBottomSheet; import java.util.List; /** - * Represents a system shortcut for a given app. The shortcut should have a static label and - * icon, and an onClickListener that depends on the item that the shortcut services. + * Represents a system shortcut for a given app. The shortcut should have a label and icon, and an + * onClickListener that depends on the item that the shortcut services. * * Example system shortcuts, defined as inner classes, include Widgets and AppInfo. */ public abstract class SystemShortcut extends ItemInfo { - public final int iconResId; - public final int labelResId; + private final int mIconResId; + private final int mLabelResId; + private final Drawable mIcon; + private final CharSequence mLabel; + private final CharSequence mContentDescription; + private final int mAccessibilityActionId; public SystemShortcut(int iconResId, int labelResId) { - this.iconResId = iconResId; - this.labelResId = labelResId; + mIconResId = iconResId; + mLabelResId = labelResId; + mAccessibilityActionId = labelResId; + mIcon = null; + mLabel = null; + mContentDescription = null; + } + + public SystemShortcut(Drawable icon, CharSequence label, CharSequence contentDescription, + int accessibilityActionId) { + mIcon = icon; + mLabel = label; + mContentDescription = contentDescription; + mAccessibilityActionId = accessibilityActionId; + mIconResId = 0; + mLabelResId = 0; + } + + public SystemShortcut(SystemShortcut other) { + mIconResId = other.mIconResId; + mLabelResId = other.mLabelResId; + mIcon = other.mIcon; + mLabel = other.mLabel; + mContentDescription = other.mContentDescription; + mAccessibilityActionId = other.mAccessibilityActionId; + } + + public void setIconAndLabelFor(View iconView, TextView labelView) { + if (mIcon != null) { + iconView.setBackground(mIcon); + } else { + iconView.setBackgroundResource(mIconResId); + } + + if (mLabel != null) { + labelView.setText(mLabel); + } else { + labelView.setText(mLabelResId); + } + } + + public void setIconAndContentDescriptionFor(ImageView view) { + if (mIcon != null) { + view.setImageDrawable(mIcon); + } else { + view.setImageResource(mIconResId); + } + + view.setContentDescription(getContentDescription(view.getContext())); + } + + private CharSequence getContentDescription(Context context) { + return mContentDescription != null ? mContentDescription : context.getText(mLabelResId); + } + + public AccessibilityNodeInfo.AccessibilityAction createAccessibilityAction(Context context) { + return new AccessibilityNodeInfo.AccessibilityAction(mAccessibilityActionId, + getContentDescription(context)); + } + + public boolean hasHandlerForAction(int action) { + return mAccessibilityActionId == action; } public abstract View.OnClickListener getOnClickListener(T activity, ItemInfo itemInfo); diff --git a/src/com/android/launcher3/provider/ImportDataTask.java b/src/com/android/launcher3/provider/ImportDataTask.java index 16c7417aa5..e1b26980c1 100644 --- a/src/com/android/launcher3/provider/ImportDataTask.java +++ b/src/com/android/launcher3/provider/ImportDataTask.java @@ -32,8 +32,9 @@ import android.net.Uri; import android.os.Process; import android.text.TextUtils; import android.util.ArrayMap; -import android.util.LongSparseArray; import android.util.SparseBooleanArray; +import android.util.SparseIntArray; + import com.android.launcher3.AutoInstallsLayout.LayoutParserCallback; import com.android.launcher3.DefaultLayoutParser; import com.android.launcher3.LauncherAppState; @@ -50,7 +51,9 @@ import com.android.launcher3.compat.UserManagerCompat; import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.logging.FileLog; import com.android.launcher3.model.GridSizeMigrationTask; -import com.android.launcher3.util.LongArrayMap; +import com.android.launcher3.util.IntArray; +import com.android.launcher3.util.IntSparseArrayMap; + import java.net.URISyntaxException; import java.util.ArrayList; import java.util.HashSet; @@ -85,7 +88,7 @@ public class ImportDataTask { } public boolean importWorkspace() throws Exception { - ArrayList allScreens = LauncherDbUtils.getScreenIdsFromCursor( + IntArray allScreens = LauncherDbUtils.getScreenIdsFromCursor( mContext.getContentResolver().query(mOtherScreensUri, null, null, null, LauncherSettings.WorkspaceScreens.SCREEN_RANK)); FileLog.d(TAG, "Importing DB from " + mOtherFavoritesUri); @@ -102,12 +105,12 @@ public class ImportDataTask { // Build screen update ArrayList screenOps = new ArrayList<>(); int count = allScreens.size(); - LongSparseArray screenIdMap = new LongSparseArray<>(count); + SparseIntArray screenIdMap = new SparseIntArray(count); for (int i = 0; i < count; i++) { ContentValues v = new ContentValues(); v.put(LauncherSettings.WorkspaceScreens._ID, i); v.put(LauncherSettings.WorkspaceScreens.SCREEN_RANK, i); - screenIdMap.put(allScreens.get(i), (long) i); + screenIdMap.put(allScreens.get(i), i); screenOps.add(ContentProviderOperation.newInsert( LauncherSettings.WorkspaceScreens.CONTENT_URI).withValues(v).build()); } @@ -128,16 +131,16 @@ public class ImportDataTask { * 3) In the end fills any holes in hotseat with items from default hotseat layout. */ private void importWorkspaceItems( - long firsetScreenId, LongSparseArray screenIdMap) throws Exception { + int firstScreenId, SparseIntArray screenIdMap) throws Exception { String profileId = Long.toString(UserManagerCompat.getInstance(mContext) .getSerialNumberForUser(Process.myUserHandle())); boolean createEmptyRowOnFirstScreen; - if (FeatureFlags.QSB_ON_FIRST_SCREEN) { + 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", - new String[]{profileId, Long.toString(firsetScreenId)}, + new String[]{profileId, Integer.toString(firstScreenId)}, null)) { // First row of first screen is not empty createEmptyRowOnFirstScreen = c.moveToNext(); @@ -190,7 +193,7 @@ public class ImportDataTask { int type = c.getInt(itemTypeIndex); int container = c.getInt(containerIndex); - long screen = c.getLong(screenIndex); + int screen = c.getInt(screenIndex); int cellX = c.getInt(cellXIndex); int cellY = c.getInt(cellYIndex); @@ -199,7 +202,7 @@ public class ImportDataTask { switch (container) { case Favorites.CONTAINER_DESKTOP: { - Long newScreenId = screenIdMap.get(screen); + Integer newScreenId = screenIdMap.get(screen); if (newScreenId == null) { FileLog.d(TAG, String.format("Skipping item %d, type %d not on a valid screen %d", id, type, screen)); continue; @@ -306,15 +309,15 @@ public class ImportDataTask { insertOperations.clear(); } - LongArrayMap hotseatItems = GridSizeMigrationTask.removeBrokenHotseatItems(mContext); + IntSparseArrayMap hotseatItems = GridSizeMigrationTask.removeBrokenHotseatItems(mContext); int myHotseatCount = LauncherAppState.getIDP(mContext).numHotseatIcons; if (hotseatItems.size() < myHotseatCount) { // Insufficient hotseat items. Add a few more. HotseatParserCallback parserCallback = new HotseatParserCallback( hotseatTargetApps, hotseatItems, insertOperations, maxId + 1, myHotseatCount); new HotseatLayoutParser(mContext, - parserCallback).loadLayout(null, new ArrayList()); - mHotseatSize = (int) hotseatItems.keyAt(hotseatItems.size() - 1) + 1; + parserCallback).loadLayout(null, new IntArray()); + mHotseatSize = hotseatItems.keyAt(hotseatItems.size() - 1) + 1; if (!insertOperations.isEmpty()) { mContext.getContentResolver().applyBatch(LauncherProvider.AUTHORITY, @@ -405,13 +408,13 @@ public class ImportDataTask { */ private static class HotseatParserCallback implements LayoutParserCallback { private final HashSet mExistingApps; - private final LongArrayMap mExistingItems; + private final IntSparseArrayMap mExistingItems; private final ArrayList mOutOps; private final int mRequiredSize; private int mStartItemId; HotseatParserCallback( - HashSet existingApps, LongArrayMap existingItems, + HashSet existingApps, IntSparseArrayMap existingItems, ArrayList outOps, int startItemId, int requiredSize) { mExistingApps = existingApps; mExistingItems = existingItems; @@ -421,12 +424,12 @@ public class ImportDataTask { } @Override - public long generateNewItemId() { + public int generateNewItemId() { return mStartItemId++; } @Override - public long insertAndCheck(SQLiteDatabase db, ContentValues values) { + public int insertAndCheck(SQLiteDatabase db, ContentValues values) { if (mExistingItems.size() >= mRequiredSize) { // No need to add more items. return 0; @@ -445,7 +448,7 @@ public class ImportDataTask { mExistingApps.add(pkg); // find next vacant spot. - long screen = 0; + int screen = 0; while (mExistingItems.get(screen) != null) { screen++; } diff --git a/src/com/android/launcher3/provider/LauncherDbUtils.java b/src/com/android/launcher3/provider/LauncherDbUtils.java index 74373d3072..ab0703fed2 100644 --- a/src/com/android/launcher3/provider/LauncherDbUtils.java +++ b/src/com/android/launcher3/provider/LauncherDbUtils.java @@ -26,6 +26,7 @@ import android.util.Log; import com.android.launcher3.LauncherAppState; import com.android.launcher3.LauncherSettings.Favorites; import com.android.launcher3.LauncherSettings.WorkspaceScreens; +import com.android.launcher3.util.IntArray; import java.util.ArrayList; import java.util.Collection; @@ -47,7 +48,7 @@ public class LauncherDbUtils { public static boolean prepareScreenZeroToHostQsb(Context context, SQLiteDatabase db) { try (SQLiteTransaction t = new SQLiteTransaction(db)) { // Get the existing screens - ArrayList screenIds = getScreenIdsFromCursor(db.query(WorkspaceScreens.TABLE_NAME, + IntArray screenIds = getScreenIdsFromCursor(db.query(WorkspaceScreens.TABLE_NAME, null, null, null, null, null, WorkspaceScreens.SCREEN_RANK)); if (screenIds.isEmpty()) { @@ -57,10 +58,10 @@ public class LauncherDbUtils { } if (screenIds.get(0) != 0) { // First screen is not 0, we need to rename screens - if (screenIds.indexOf(0L) > -1) { + if (screenIds.contains(0)) { // There is already a screen 0. First rename it to a different screen. - long newScreenId = 1; - while (screenIds.indexOf(newScreenId) > -1) newScreenId++; + int newScreenId = 1; + while (screenIds.contains(newScreenId)) newScreenId++; renameScreen(db, 0, newScreenId); } @@ -86,8 +87,8 @@ public class LauncherDbUtils { } } - private static void renameScreen(SQLiteDatabase db, long oldScreen, long newScreen) { - String[] whereParams = new String[] { Long.toString(oldScreen) }; + private static void renameScreen(SQLiteDatabase db, int oldScreen, int newScreen) { + String[] whereParams = new String[] { Integer.toString(oldScreen) }; ContentValues values = new ContentValues(); values.put(WorkspaceScreens._ID, newScreen); @@ -101,19 +102,18 @@ public class LauncherDbUtils { /** * Parses the cursor containing workspace screens table and returns the list of screen IDs */ - public static ArrayList getScreenIdsFromCursor(Cursor sc) { + public static IntArray getScreenIdsFromCursor(Cursor sc) { try { return iterateCursor(sc, - sc.getColumnIndexOrThrow(WorkspaceScreens._ID), - new ArrayList()); + sc.getColumnIndexOrThrow(WorkspaceScreens._ID), new IntArray()); } finally { sc.close(); } } - public static > T iterateCursor(Cursor c, int columnIndex, T out) { + public static IntArray iterateCursor(Cursor c, int columnIndex, IntArray out) { while (c.moveToNext()) { - out.add(c.getLong(columnIndex)); + out.add(c.getInt(columnIndex)); } return out; } diff --git a/src/com/android/launcher3/provider/LossyScreenMigrationTask.java b/src/com/android/launcher3/provider/LossyScreenMigrationTask.java index 51890d1943..9166b8358d 100644 --- a/src/com/android/launcher3/provider/LossyScreenMigrationTask.java +++ b/src/com/android/launcher3/provider/LossyScreenMigrationTask.java @@ -27,7 +27,7 @@ import com.android.launcher3.LauncherSettings.Favorites; import com.android.launcher3.Utilities; import com.android.launcher3.Workspace; import com.android.launcher3.model.GridSizeMigrationTask; -import com.android.launcher3.util.LongArrayMap; +import com.android.launcher3.util.IntSparseArrayMap; import java.util.ArrayList; @@ -39,8 +39,8 @@ public class LossyScreenMigrationTask extends GridSizeMigrationTask { private final SQLiteDatabase mDb; - private final LongArrayMap mOriginalItems; - private final LongArrayMap mUpdates; + private final IntSparseArrayMap mOriginalItems; + private final IntSparseArrayMap mUpdates; protected LossyScreenMigrationTask( Context context, InvariantDeviceProfile idp, SQLiteDatabase db) { @@ -50,8 +50,8 @@ public class LossyScreenMigrationTask extends GridSizeMigrationTask { new Point(idp.numColumns, idp.numRows)); mDb = db; - mOriginalItems = new LongArrayMap<>(); - mUpdates = new LongArrayMap<>(); + mOriginalItems = new IntSparseArrayMap<>(); + mUpdates = new IntSparseArrayMap<>(); } @Override @@ -65,7 +65,7 @@ public class LossyScreenMigrationTask extends GridSizeMigrationTask { } @Override - protected ArrayList loadWorkspaceEntries(long screen) { + protected ArrayList loadWorkspaceEntries(int screen) { ArrayList result = super.loadWorkspaceEntries(screen); for (DbEntry entry : result) { mOriginalItems.put(entry.id, entry.copy()); @@ -90,7 +90,7 @@ public class LossyScreenMigrationTask extends GridSizeMigrationTask { tempValues.clear(); update.addToContentValues(tempValues); mDb.update(Favorites.TABLE_NAME, tempValues, "_id = ?", - new String[] {Long.toString(update.id)}); + new String[] {Integer.toString(update.id)}); } } diff --git a/src/com/android/launcher3/provider/RestoreDbTask.java b/src/com/android/launcher3/provider/RestoreDbTask.java index 5230160085..17c66b4d9d 100644 --- a/src/com/android/launcher3/provider/RestoreDbTask.java +++ b/src/com/android/launcher3/provider/RestoreDbTask.java @@ -92,7 +92,7 @@ public class RestoreDbTask { new String[]{Integer.toString(Favorites.ITEM_TYPE_APPWIDGET)}); long myProfileId = helper.getDefaultUserSerial(); - if (Utilities.longCompare(oldProfileId, myProfileId) != 0) { + if (myProfileId != oldProfileId) { FileLog.d(TAG, "Changing primary user id from " + oldProfileId + " to " + myProfileId); migrateProfileId(db, myProfileId); } diff --git a/src/com/android/launcher3/qsb/QsbContainerView.java b/src/com/android/launcher3/qsb/QsbContainerView.java index 5b8ae58369..ac1fafb42e 100644 --- a/src/com/android/launcher3/qsb/QsbContainerView.java +++ b/src/com/android/launcher3/qsb/QsbContainerView.java @@ -213,7 +213,7 @@ public class QsbContainerView extends FrameLayout { } public boolean isQsbEnabled() { - return FeatureFlags.QSB_ON_FIRST_SCREEN; + return FeatureFlags.QSB_ON_FIRST_SCREEN.get(); } protected Bundle createBindOptions() { diff --git a/src/com/android/launcher3/qsb/QsbWidgetHostView.java b/src/com/android/launcher3/qsb/QsbWidgetHostView.java index cff5126375..f5ecda3e30 100644 --- a/src/com/android/launcher3/qsb/QsbWidgetHostView.java +++ b/src/com/android/launcher3/qsb/QsbWidgetHostView.java @@ -16,7 +16,6 @@ package com.android.launcher3.qsb; -import android.appwidget.AppWidgetHostView; import android.content.Context; import android.view.LayoutInflater; import android.view.View; @@ -26,17 +25,20 @@ import android.widget.RemoteViews; import com.android.launcher3.Launcher; import com.android.launcher3.R; +import com.android.launcher3.widget.NavigableAppWidgetHostView; /** * Appwidget host view with QSB specific logic. */ -public class QsbWidgetHostView extends AppWidgetHostView { +public class QsbWidgetHostView extends NavigableAppWidgetHostView { @ViewDebug.ExportedProperty(category = "launcher") private int mPreviousOrientation; public QsbWidgetHostView(Context context) { super(context); + setFocusable(true); + setBackgroundResource(R.drawable.qsb_host_view_focus_bg); } @Override @@ -89,4 +91,9 @@ public class QsbWidgetHostView extends AppWidgetHostView { Launcher.getLauncher(v2.getContext()).startSearch("", false, null, true)); return v; } + + @Override + protected boolean shouldAllowDirectClick() { + return true; + } } diff --git a/src/com/android/launcher3/touch/TouchEventTranslator.java b/src/com/android/launcher3/touch/TouchEventTranslator.java index 8333d32468..bf0c84c4ad 100644 --- a/src/com/android/launcher3/touch/TouchEventTranslator.java +++ b/src/com/android/launcher3/touch/TouchEventTranslator.java @@ -82,7 +82,7 @@ public class TouchEventTranslator { public void dispatchDownEvents(MotionEvent ev) { for(int i = 0; i < ev.getPointerCount() && i < mDownEvents.size(); i++) { int pid = ev.getPointerId(i); - put(pid, ev.getX(i), 0, mDownEvents.get(i).timeStamp, ev); + put(pid, i, ev.getX(i), 0, mDownEvents.get(i).timeStamp, ev); } } @@ -103,7 +103,7 @@ public class TouchEventTranslator { } generateEvent(ev.getAction(), ev); } else { - put(pid, x, y, ev); + put(pid, index, x, y, ev); } break; case MotionEvent.ACTION_MOVE: @@ -128,11 +128,11 @@ public class TouchEventTranslator { } } - private TouchEventTranslator put(int id, float x, float y, MotionEvent ev) { - return put(id, x, y, ev.getEventTime(), ev); + private TouchEventTranslator put(int id, int index, float x, float y, MotionEvent ev) { + return put(id, index, x, y, ev.getEventTime(), ev); } - private TouchEventTranslator put(int id, float x, float y, long ms, MotionEvent ev) { + private TouchEventTranslator put(int id, int index, float x, float y, long ms, MotionEvent ev) { checkFingerExistence(id, false); boolean isInitialDown = (mFingers.size() == 0); @@ -155,7 +155,7 @@ public class TouchEventTranslator { } else { action = MotionEvent.ACTION_POINTER_DOWN; // Set the id of the changed pointer. - action |= id << MotionEvent.ACTION_POINTER_INDEX_SHIFT; + action |= index << MotionEvent.ACTION_POINTER_INDEX_SHIFT; } generateEvent(action, ms, ev); return this; @@ -209,7 +209,7 @@ public class TouchEventTranslator { public void printSamples(String msg, MotionEvent ev) { System.out.printf("%s %s", msg, MotionEvent.actionToString(ev.getActionMasked())); final int pointerCount = ev.getPointerCount(); - System.out.printf("#%d/%d", ev.getPointerId(ev.getActionIndex()), pointerCount); + System.out.printf("#%d/%d", ev.getActionIndex(), pointerCount); System.out.printf(" t=%d:", ev.getEventTime()); for (int p = 0; p < pointerCount; p++) { System.out.printf(" id=%d: (%f,%f)", @@ -242,6 +242,10 @@ public class TouchEventTranslator { if (DEBUG) { printSamples(TAG + " generateEvent", event); } + if (event.getPointerId(event.getActionIndex()) < 0) { + printSamples(TAG + "generateEvent", event); + throw new IllegalStateException(event.getActionIndex() + " not found in MotionEvent"); + } mListener.accept(event); event.recycle(); } diff --git a/src/com/android/launcher3/util/IntArray.java b/src/com/android/launcher3/util/IntArray.java new file mode 100644 index 0000000000..b2fb32a6cf --- /dev/null +++ b/src/com/android/launcher3/util/IntArray.java @@ -0,0 +1,236 @@ +/* + * 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.util; + +import java.util.Arrays; + +/** + * Copy of the platform hidden implementation of android.util.IntArray. + * Implements a growing array of int primitives. + */ +public class IntArray implements Cloneable { + private static final int MIN_CAPACITY_INCREMENT = 12; + + private static final int[] EMPTY_INT = new int[0]; + + /* package private */ int[] mValues; + /* package private */ int mSize; + + private IntArray(int[] array, int size) { + mValues = array; + mSize = size; + } + + /** + * Creates an empty IntArray with the default initial capacity. + */ + public IntArray() { + this(10); + } + + /** + * Creates an empty IntArray with the specified initial capacity. + */ + public IntArray(int initialCapacity) { + if (initialCapacity == 0) { + mValues = EMPTY_INT; + } else { + mValues = new int[initialCapacity]; + } + mSize = 0; + } + + /** + * Creates an IntArray wrapping the given primitive int array. + */ + public static IntArray wrap(int... array) { + return new IntArray(array, array.length); + } + + /** + * Appends the specified value to the end of this array. + */ + public void add(int value) { + add(mSize, value); + } + + /** + * Inserts a value at the specified position in this array. If the specified index is equal to + * the length of the array, the value is added at the end. + * + * @throws IndexOutOfBoundsException when index < 0 || index > size() + */ + public void add(int index, int value) { + ensureCapacity(1); + int rightSegment = mSize - index; + mSize++; + checkBounds(mSize, index); + + if (rightSegment != 0) { + // Move by 1 all values from the right of 'index' + System.arraycopy(mValues, index, mValues, index + 1, rightSegment); + } + + mValues[index] = value; + } + + /** + * Adds the values in the specified array to this array. + */ + public void addAll(IntArray values) { + final int count = values.mSize; + ensureCapacity(count); + + System.arraycopy(values.mValues, 0, mValues, mSize, count); + mSize += count; + } + + /** + * Ensures capacity to append at least count values. + */ + private void ensureCapacity(int count) { + final int currentSize = mSize; + final int minCapacity = currentSize + count; + if (minCapacity >= mValues.length) { + final int targetCap = currentSize + (currentSize < (MIN_CAPACITY_INCREMENT / 2) ? + MIN_CAPACITY_INCREMENT : currentSize >> 1); + final int newCapacity = targetCap > minCapacity ? targetCap : minCapacity; + final int[] newValues = new int[newCapacity]; + System.arraycopy(mValues, 0, newValues, 0, currentSize); + mValues = newValues; + } + } + + /** + * Removes all values from this array. + */ + public void clear() { + mSize = 0; + } + + @Override + public IntArray clone() { + return wrap(toArray()); + } + + /** + * Returns the value at the specified position in this array. + */ + public int get(int index) { + checkBounds(mSize, index); + return mValues[index]; + } + + /** + * Sets the value at the specified position in this array. + */ + public void set(int index, int value) { + checkBounds(mSize, index); + mValues[index] = value; + } + + /** + * Returns the index of the first occurrence of the specified value in this + * array, or -1 if this array does not contain the value. + */ + public int indexOf(int value) { + final int n = mSize; + for (int i = 0; i < n; i++) { + if (mValues[i] == value) { + return i; + } + } + return -1; + } + + public boolean contains(int value) { + return indexOf(value) >= 0; + } + + public boolean isEmpty() { + return mSize == 0; + } + + /** + * Removes the value at the specified index from this array. + */ + public void removeIndex(int index) { + checkBounds(mSize, index); + System.arraycopy(mValues, index + 1, mValues, index, mSize - index - 1); + mSize--; + } + + /** + * Removes the values if it exists + */ + public void removeValue(int value) { + int index = indexOf(value); + if (index >= 0) { + removeIndex(index); + } + } + + /** + * Removes the values if it exists + */ + public void removeAllValues(IntArray values) { + for (int i = 0; i < values.mSize; i++) { + removeValue(values.mValues[i]); + } + } + + /** + * Returns the number of values in this array. + */ + public int size() { + return mSize; + } + + /** + * Returns a new array with the contents of this IntArray. + */ + public int[] toArray() { + return mSize == 0 ? EMPTY_INT : Arrays.copyOf(mValues, mSize); + } + + /** + * Returns a comma separate list of all values. + */ + public String toConcatString() { + StringBuilder b = new StringBuilder(); + for (int i = 0; i < mSize ; i++) { + if (i > 0) { + b.append(", "); + } + b.append(mValues[i]); + } + return b.toString(); + } + + /** + * Throws {@link ArrayIndexOutOfBoundsException} if the index is out of bounds. + * + * @param len length of the array. Must be non-negative + * @param index the index to check + * @throws ArrayIndexOutOfBoundsException if the {@code index} is out of bounds of the array + */ + private static void checkBounds(int len, int index) { + if (index < 0 || len <= index) { + throw new ArrayIndexOutOfBoundsException("length=" + len + "; index=" + index); + } + } +} \ No newline at end of file diff --git a/src/com/android/launcher3/util/IntSet.java b/src/com/android/launcher3/util/IntSet.java new file mode 100644 index 0000000000..63499b06d6 --- /dev/null +++ b/src/com/android/launcher3/util/IntSet.java @@ -0,0 +1,51 @@ +/* + * 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.util; + +import java.util.Arrays; + +/** + * A wrapper over IntArray implementing a growing set of int primitives. + */ +public class IntSet { + + final IntArray mArray = new IntArray(); + + /** + * Appends the specified value to the set if it does not exist. + */ + public void add(int value) { + int index = Arrays.binarySearch(mArray.mValues, 0, mArray.mSize, value); + if (index < 0) { + mArray.add(-index - 1, value); + } + } + + public boolean contains(int value) { + return Arrays.binarySearch(mArray.mValues, 0, mArray.mSize, value) >= 0; + } + + public boolean isEmpty() { + return mArray.isEmpty(); + } + + /** + * Returns the number of values in this set. + */ + public int size() { + return mArray.size(); + } +} diff --git a/src/com/android/launcher3/util/LongArrayMap.java b/src/com/android/launcher3/util/IntSparseArrayMap.java similarity index 80% rename from src/com/android/launcher3/util/LongArrayMap.java rename to src/com/android/launcher3/util/IntSparseArrayMap.java index a337e85bd8..9d5391be0e 100644 --- a/src/com/android/launcher3/util/LongArrayMap.java +++ b/src/com/android/launcher3/util/IntSparseArrayMap.java @@ -16,16 +16,16 @@ package com.android.launcher3.util; -import android.util.LongSparseArray; +import android.util.SparseArray; import java.util.Iterator; /** - * Extension of {@link LongSparseArray} with some utility methods. + * Extension of {@link SparseArray} with some utility methods. */ -public class LongArrayMap extends LongSparseArray implements Iterable { +public class IntSparseArrayMap extends SparseArray implements Iterable { - public boolean containsKey(long key) { + public boolean containsKey(int key) { return indexOfKey(key) >= 0; } @@ -34,8 +34,8 @@ public class LongArrayMap extends LongSparseArray implements Iterable { } @Override - public LongArrayMap clone() { - return (LongArrayMap) super.clone(); + public IntSparseArrayMap clone() { + return (IntSparseArrayMap) super.clone(); } @Override diff --git a/src/com/android/launcher3/util/ItemInfoMatcher.java b/src/com/android/launcher3/util/ItemInfoMatcher.java index 19cf6c1e54..c3570fe453 100644 --- a/src/com/android/launcher3/util/ItemInfoMatcher.java +++ b/src/com/android/launcher3/util/ItemInfoMatcher.java @@ -105,7 +105,7 @@ public interface ItemInfoMatcher { keys.contains(ShortcutKey.fromItemInfo(info)); } - static ItemInfoMatcher ofItemIds(LongArrayMap ids, Boolean matchDefault) { + static ItemInfoMatcher ofItemIds(IntSparseArrayMap ids, Boolean matchDefault) { return (info, cn) -> ids.get(info.id, matchDefault); } } diff --git a/src/com/android/launcher3/util/ListViewHighlighter.java b/src/com/android/launcher3/util/ListViewHighlighter.java index 360546e53f..c9fe228d58 100644 --- a/src/com/android/launcher3/util/ListViewHighlighter.java +++ b/src/com/android/launcher3/util/ListViewHighlighter.java @@ -75,7 +75,7 @@ public class ListViewHighlighter implements OnScrollListener, RecyclerListener, @Override public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { - highlightIfVisible(firstVisibleItem, firstVisibleItem + visibleItemCount); + highlightIfVisible(firstVisibleItem, firstVisibleItem + visibleItemCount - 1); } private boolean highlightIfVisible(int start, int end) { diff --git a/src/com/android/launcher3/util/MultiValueAlpha.java b/src/com/android/launcher3/util/MultiValueAlpha.java index f810f483d0..07f835d4a1 100644 --- a/src/com/android/launcher3/util/MultiValueAlpha.java +++ b/src/com/android/launcher3/util/MultiValueAlpha.java @@ -19,6 +19,8 @@ package com.android.launcher3.util; import android.util.Property; import android.view.View; +import java.util.Arrays; + /** * Utility class to handle separating a single value as a factor of multiple values */ @@ -55,6 +57,11 @@ public class MultiValueAlpha { } } + @Override + public String toString() { + return Arrays.toString(mMyProperties); + } + public AlphaProperty getProperty(int index) { return mMyProperties[index]; } @@ -97,5 +104,10 @@ public class MultiValueAlpha { public float getValue() { return mValue; } + + @Override + public String toString() { + return Float.toString(mValue); + } } } diff --git a/src/com/android/launcher3/util/SecureSettingsObserver.java b/src/com/android/launcher3/util/SecureSettingsObserver.java new file mode 100644 index 0000000000..48aa02b799 --- /dev/null +++ b/src/com/android/launcher3/util/SecureSettingsObserver.java @@ -0,0 +1,82 @@ +/* + * 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.launcher3.util; + +import android.content.ContentResolver; +import android.content.Context; +import android.database.ContentObserver; +import android.os.Handler; +import android.provider.Settings; + +/** + * Utility class to listen for secure settings changes + */ +public class SecureSettingsObserver extends ContentObserver { + + /** Hidden field Settings.Secure.NOTIFICATION_BADGING */ + public static final String NOTIFICATION_BADGING = "notification_badging"; + + private final ContentResolver mResolver; + private final String mKeySetting; + private final int mDefaultValue; + private final OnChangeListener mOnChangeListener; + + public SecureSettingsObserver(ContentResolver resolver, OnChangeListener listener, + String keySetting, int defaultValue) { + super(new Handler()); + + mResolver = resolver; + mOnChangeListener = listener; + mKeySetting = keySetting; + mDefaultValue = defaultValue; + } + + @Override + public void onChange(boolean selfChange) { + mOnChangeListener.onSettingsChanged(getValue()); + } + + public boolean getValue() { + return Settings.Secure.getInt(mResolver, mKeySetting, mDefaultValue) == 1; + } + + public void register() { + mResolver.registerContentObserver(Settings.Secure.getUriFor(mKeySetting), false, this); + } + + public ContentResolver getResolver() { + return mResolver; + } + + public void dispatchOnChange() { + onChange(true); + } + + public void unregister() { + mResolver.unregisterContentObserver(this); + } + + public interface OnChangeListener { + void onSettingsChanged(boolean isEnabled); + } + + public static SecureSettingsObserver newNotificationSettingsObserver(Context context, + OnChangeListener listener) { + return new SecureSettingsObserver( + context.getContentResolver(), listener, NOTIFICATION_BADGING, 1); + } +} diff --git a/src/com/android/launcher3/util/SettingsObserver.java b/src/com/android/launcher3/util/SettingsObserver.java deleted file mode 100644 index 6baa24293e..0000000000 --- a/src/com/android/launcher3/util/SettingsObserver.java +++ /dev/null @@ -1,100 +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.launcher3.util; - -import android.content.ContentResolver; -import android.database.ContentObserver; -import android.os.Handler; -import android.provider.Settings; - -public interface SettingsObserver { - - /** - * Registers the content observer to call {@link #onSettingChanged(boolean)} when any of the - * passed settings change. The value passed to onSettingChanged() is based on the key setting. - */ - void register(String keySetting, String ... dependentSettings); - void unregister(); - void onSettingChanged(boolean keySettingEnabled); - - - abstract class Secure extends ContentObserver implements SettingsObserver { - private ContentResolver mResolver; - private String mKeySetting; - - public Secure(ContentResolver resolver) { - super(new Handler()); - mResolver = resolver; - } - - @Override - public void register(String keySetting, String ... dependentSettings) { - mKeySetting = keySetting; - mResolver.registerContentObserver( - Settings.Secure.getUriFor(mKeySetting), false, this); - for (String setting : dependentSettings) { - mResolver.registerContentObserver( - Settings.Secure.getUriFor(setting), false, this); - } - onChange(true); - } - - @Override - public void unregister() { - mResolver.unregisterContentObserver(this); - } - - @Override - public void onChange(boolean selfChange) { - super.onChange(selfChange); - onSettingChanged(Settings.Secure.getInt(mResolver, mKeySetting, 1) == 1); - } - } - - abstract class System extends ContentObserver implements SettingsObserver { - private ContentResolver mResolver; - private String mKeySetting; - - public System(ContentResolver resolver) { - super(new Handler()); - mResolver = resolver; - } - - @Override - public void register(String keySetting, String ... dependentSettings) { - mKeySetting = keySetting; - mResolver.registerContentObserver( - Settings.System.getUriFor(mKeySetting), false, this); - for (String setting : dependentSettings) { - mResolver.registerContentObserver( - Settings.System.getUriFor(setting), false, this); - } - onChange(true); - } - - @Override - public void unregister() { - mResolver.unregisterContentObserver(this); - } - - @Override - public void onChange(boolean selfChange) { - super.onChange(selfChange); - onSettingChanged(Settings.System.getInt(mResolver, mKeySetting, 1) == 1); - } - } -} diff --git a/src/com/android/launcher3/views/BaseDragLayer.java b/src/com/android/launcher3/views/BaseDragLayer.java index 1faca152b6..4545a1ee7c 100644 --- a/src/com/android/launcher3/views/BaseDragLayer.java +++ b/src/com/android/launcher3/views/BaseDragLayer.java @@ -16,6 +16,10 @@ package com.android.launcher3.views; +import static android.view.MotionEvent.ACTION_CANCEL; +import static android.view.MotionEvent.ACTION_DOWN; +import static android.view.MotionEvent.ACTION_UP; + import static com.android.launcher3.Utilities.SINGLE_FRAME_MS; import android.content.Context; @@ -35,6 +39,7 @@ import com.android.launcher3.util.MultiValueAlpha; import com.android.launcher3.util.MultiValueAlpha.AlphaProperty; import com.android.launcher3.util.TouchController; +import java.io.PrintWriter; import java.util.ArrayList; /** @@ -79,6 +84,9 @@ public abstract class BaseDragLayer protected TouchController mActiveController; private TouchCompleteListener mTouchCompleteListener; + // Object controlling the current touch interaction + private Object mCurrentTouchOwner; + public BaseDragLayer(Context context, AttributeSet attrs, int alphaChannelCount) { super(context, attrs); mActivity = (T) ActivityContext.lookupContext(context); @@ -94,7 +102,7 @@ public abstract class BaseDragLayer public boolean onInterceptTouchEvent(MotionEvent ev) { int action = ev.getAction(); - if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) { + if (action == ACTION_UP || action == ACTION_CANCEL) { if (mTouchCompleteListener != null) { mTouchCompleteListener.onTouchComplete(); } @@ -177,7 +185,7 @@ public abstract class BaseDragLayer @Override public boolean onTouchEvent(MotionEvent ev) { int action = ev.getAction(); - if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) { + if (action == ACTION_UP || action == ACTION_CANCEL) { if (mTouchCompleteListener != null) { mTouchCompleteListener.onTouchComplete(); } @@ -192,6 +200,37 @@ public abstract class BaseDragLayer } } + @Override + public boolean dispatchTouchEvent(MotionEvent ev) { + return verifyTouchDispatch(this, ev) && super.dispatchTouchEvent(ev); + } + + /** + * Returns true if the {@param caller} is allowed to dispatch {@param ev} on this view, + * false otherwise. + */ + public boolean verifyTouchDispatch(Object caller, MotionEvent ev) { + int action = ev.getAction(); + if (action == ACTION_DOWN) { + if (mCurrentTouchOwner != null) { + // Another touch in progress. + ev.setAction(ACTION_CANCEL); + super.dispatchTouchEvent(ev); + ev.setAction(action); + } + mCurrentTouchOwner = caller; + return true; + } + if (mCurrentTouchOwner != caller) { + // Someone else is controlling the touch + return false; + } + if (action == ACTION_UP || action == ACTION_CANCEL) { + mCurrentTouchOwner = null; + } + return true; + } + /** * Determine the rect of the descendant in this DragLayer's coordinates * @@ -319,6 +358,10 @@ public abstract class BaseDragLayer return mMultiValueAlpha.getProperty(index); } + public void dumpAlpha(PrintWriter writer) { + writer.println(" dragLayerAlpha : " + mMultiValueAlpha ); + } + public static class LayoutParams extends InsettableFrameLayout.LayoutParams { public int x, y; public boolean customPosition = false; diff --git a/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java b/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java index 12859c78f4..95f8daabe5 100644 --- a/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java +++ b/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java @@ -16,16 +16,13 @@ package com.android.launcher3.widget; -import android.appwidget.AppWidgetHostView; import android.appwidget.AppWidgetProviderInfo; import android.content.Context; import android.content.res.Configuration; import android.graphics.PointF; -import android.graphics.Rect; import android.os.Handler; import android.os.SystemClock; import android.util.SparseBooleanArray; -import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; @@ -49,12 +46,10 @@ import com.android.launcher3.Utilities; import com.android.launcher3.dragndrop.DragLayer; import com.android.launcher3.views.BaseDragLayer.TouchCompleteListener; -import java.util.ArrayList; - /** * {@inheritDoc} */ -public class LauncherAppWidgetHostView extends AppWidgetHostView +public class LauncherAppWidgetHostView extends NavigableAppWidgetHostView implements TouchCompleteListener, View.OnLongClickListener { // Related to the auto-advancing of widgets @@ -75,9 +70,6 @@ public class LauncherAppWidgetHostView extends AppWidgetHostView private float mSlop; - @ViewDebug.ExportedProperty(category = "launcher") - private boolean mChildrenFocused; - private boolean mIsScrollable; private boolean mIsAttachedToWindow; private boolean mIsAutoAdvanceRegistered; @@ -267,98 +259,6 @@ public class LauncherAppWidgetHostView extends AppWidgetHostView } } - @Override - public int getDescendantFocusability() { - return mChildrenFocused ? ViewGroup.FOCUS_BEFORE_DESCENDANTS - : ViewGroup.FOCUS_BLOCK_DESCENDANTS; - } - - @Override - public boolean dispatchKeyEvent(KeyEvent event) { - if (mChildrenFocused && event.getKeyCode() == KeyEvent.KEYCODE_ESCAPE - && event.getAction() == KeyEvent.ACTION_UP) { - mChildrenFocused = false; - requestFocus(); - return true; - } - return super.dispatchKeyEvent(event); - } - - @Override - public boolean onKeyDown(int keyCode, KeyEvent event) { - if (!mChildrenFocused && keyCode == KeyEvent.KEYCODE_ENTER) { - event.startTracking(); - return true; - } - return super.onKeyDown(keyCode, event); - } - - @Override - public boolean onKeyUp(int keyCode, KeyEvent event) { - if (event.isTracking()) { - if (!mChildrenFocused && keyCode == KeyEvent.KEYCODE_ENTER) { - mChildrenFocused = true; - ArrayList focusableChildren = getFocusables(FOCUS_FORWARD); - focusableChildren.remove(this); - int childrenCount = focusableChildren.size(); - switch (childrenCount) { - case 0: - mChildrenFocused = false; - break; - case 1: { - if (getTag() instanceof ItemInfo) { - ItemInfo item = (ItemInfo) getTag(); - if (item.spanX == 1 && item.spanY == 1) { - focusableChildren.get(0).performClick(); - mChildrenFocused = false; - return true; - } - } - // continue; - } - default: - focusableChildren.get(0).requestFocus(); - return true; - } - } - } - return super.onKeyUp(keyCode, event); - } - - @Override - protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) { - if (gainFocus) { - mChildrenFocused = false; - dispatchChildFocus(false); - } - super.onFocusChanged(gainFocus, direction, previouslyFocusedRect); - } - - @Override - public void requestChildFocus(View child, View focused) { - super.requestChildFocus(child, focused); - dispatchChildFocus(mChildrenFocused && focused != null); - if (focused != null) { - focused.setFocusableInTouchMode(false); - } - } - - @Override - public void clearChildFocus(View child) { - super.clearChildFocus(child); - dispatchChildFocus(false); - } - - @Override - public boolean dispatchUnhandledMove(View focused, int direction) { - return mChildrenFocused; - } - - private void dispatchChildFocus(boolean childIsFocused) { - // The host view's background changes when selected, to indicate the focus is inside. - setSelected(childIsFocused); - } - public void switchToErrorView() { // Update the widget with 0 Layout id, to reset the view to error view. updateAppWidget(new RemoteViews(getAppWidgetInfo().provider.getPackageName(), 0)); @@ -502,4 +402,13 @@ public class LauncherAppWidgetHostView extends AppWidgetHostView mLauncher.removeItem(this, info, false /* deleteFromDb */); mLauncher.bindAppWidget(info); } + + @Override + protected boolean shouldAllowDirectClick() { + if (getTag() instanceof ItemInfo) { + ItemInfo item = (ItemInfo) getTag(); + return item.spanX == 1 && item.spanY == 1; + } + return false; + } } diff --git a/src/com/android/launcher3/widget/NavigableAppWidgetHostView.java b/src/com/android/launcher3/widget/NavigableAppWidgetHostView.java new file mode 100644 index 0000000000..68b1595eed --- /dev/null +++ b/src/com/android/launcher3/widget/NavigableAppWidgetHostView.java @@ -0,0 +1,136 @@ +/* + * 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.widget; + +import android.appwidget.AppWidgetHostView; +import android.content.Context; +import android.graphics.Rect; +import android.view.KeyEvent; +import android.view.View; +import android.view.ViewDebug; +import android.view.ViewGroup; + +import java.util.ArrayList; + +/** + * Extension of AppWidgetHostView with support for controlled keyboard navigation. + */ +public abstract class NavigableAppWidgetHostView extends AppWidgetHostView { + + @ViewDebug.ExportedProperty(category = "launcher") + private boolean mChildrenFocused; + + public NavigableAppWidgetHostView(Context context) { + super(context); + } + + @Override + public int getDescendantFocusability() { + return mChildrenFocused ? ViewGroup.FOCUS_BEFORE_DESCENDANTS + : ViewGroup.FOCUS_BLOCK_DESCENDANTS; + } + + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + if (mChildrenFocused && event.getKeyCode() == KeyEvent.KEYCODE_ESCAPE + && event.getAction() == KeyEvent.ACTION_UP) { + mChildrenFocused = false; + requestFocus(); + return true; + } + return super.dispatchKeyEvent(event); + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + if (!mChildrenFocused && keyCode == KeyEvent.KEYCODE_ENTER) { + event.startTracking(); + return true; + } + return super.onKeyDown(keyCode, event); + } + + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + if (event.isTracking()) { + if (!mChildrenFocused && keyCode == KeyEvent.KEYCODE_ENTER) { + mChildrenFocused = true; + ArrayList focusableChildren = getFocusables(FOCUS_FORWARD); + focusableChildren.remove(this); + int childrenCount = focusableChildren.size(); + switch (childrenCount) { + case 0: + mChildrenFocused = false; + break; + case 1: { + if (shouldAllowDirectClick()) { + focusableChildren.get(0).performClick(); + mChildrenFocused = false; + return true; + } + // continue; + } + default: + focusableChildren.get(0).requestFocus(); + return true; + } + } + } + return super.onKeyUp(keyCode, event); + } + + /** + * For a widget with only a single interactive element, return true if whole widget should act + * as a single interactive element, and clicking 'enter' should activate the child element + * directly. Otherwise clicking 'enter' will only move the focus inside the widget. + */ + protected abstract boolean shouldAllowDirectClick(); + + @Override + protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) { + if (gainFocus) { + mChildrenFocused = false; + dispatchChildFocus(false); + } + super.onFocusChanged(gainFocus, direction, previouslyFocusedRect); + } + + @Override + public void requestChildFocus(View child, View focused) { + super.requestChildFocus(child, focused); + dispatchChildFocus(mChildrenFocused && focused != null); + if (focused != null) { + focused.setFocusableInTouchMode(false); + } + } + + @Override + public void clearChildFocus(View child) { + super.clearChildFocus(child); + dispatchChildFocus(false); + } + + @Override + public boolean dispatchUnhandledMove(View focused, int direction) { + return mChildrenFocused; + } + + private void dispatchChildFocus(boolean childIsFocused) { + // The host view's background changes when selected, to indicate the focus is inside. + setSelected(childIsFocused); + } +} diff --git a/src_flags/com/android/launcher3/config/FeatureFlags.java b/src_flags/com/android/launcher3/config/FeatureFlags.java index 3ffb6c9370..73c699633a 100644 --- a/src_flags/com/android/launcher3/config/FeatureFlags.java +++ b/src_flags/com/android/launcher3/config/FeatureFlags.java @@ -16,10 +16,13 @@ package com.android.launcher3.config; +import android.content.Context; + /** * Defines a set of flags used to control various launcher behaviors */ public final class FeatureFlags extends BaseFlags { - - private FeatureFlags() {} + private FeatureFlags() { + // Prevent instantiation + } } diff --git a/src_plugins/README.md b/src_plugins/README.md new file mode 100644 index 0000000000..c7ce1daca3 --- /dev/null +++ b/src_plugins/README.md @@ -0,0 +1,3 @@ +This directory contains plugin interfaces that launcher listens for and plugins implement. In other words, these are the hooks that specify what plugins launcher currently supports. + +Details about how to create a new plugin interface, or to use existing interfaces to write a plugin can be found at go/gnl/plugins. diff --git a/src_plugins/com/android/systemui/plugins/AllAppsRow.java b/src_plugins/com/android/systemui/plugins/AllAppsRow.java new file mode 100644 index 0000000000..c003fc1036 --- /dev/null +++ b/src_plugins/com/android/systemui/plugins/AllAppsRow.java @@ -0,0 +1,51 @@ +/* + * 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.systemui.plugins; + +import android.view.View; +import android.view.ViewGroup; + +import com.android.systemui.plugins.annotations.ProvidesInterface; + +/** + * Implement this plugin interface to add a row of views to the top of the all apps drawer. + */ +@ProvidesInterface(action = AllAppsRow.ACTION, version = AllAppsRow.VERSION) +public interface AllAppsRow extends Plugin { + String ACTION = "com.android.systemui.action.PLUGIN_ALL_APPS_ACTIONS"; + int VERSION = 1; + + /** + * Setup the row and return the parent view. + * @param parent The ViewGroup to which launcher will add this row. + */ + View setup(ViewGroup parent); + + /** + * @return The height to reserve in all apps for your views. + */ + int getExpectedHeight(); + + /** + * Update launcher whenever {@link #getExpectedHeight()} changes. + */ + void setOnHeightUpdatedListener(OnHeightUpdatedListener onHeightUpdatedListener); + + interface OnHeightUpdatedListener { + void onHeightUpdated(); + } +} diff --git a/src_ui_overrides/com/android/launcher3/uioverrides/plugins/PluginManagerWrapper.java b/src_ui_overrides/com/android/launcher3/uioverrides/plugins/PluginManagerWrapper.java new file mode 100644 index 0000000000..31dbb347ca --- /dev/null +++ b/src_ui_overrides/com/android/launcher3/uioverrides/plugins/PluginManagerWrapper.java @@ -0,0 +1,40 @@ +/* + * 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.uioverrides.plugins; + +import android.content.Context; + +import com.android.launcher3.util.MainThreadInitializedObject; +import com.android.systemui.plugins.Plugin; +import com.android.systemui.plugins.PluginListener; + +public class PluginManagerWrapper { + + public static final MainThreadInitializedObject INSTANCE = + new MainThreadInitializedObject<>(PluginManagerWrapper::new); + + private PluginManagerWrapper(Context c) { + } + + public void addPluginListener(PluginListener listener, Class pluginClass) { + } + + public void addPluginListener(PluginListener listener, Class pluginClass, + boolean allowMultiple) { + } + + public void removePluginListener(PluginListener listener) { + } +} diff --git a/tests/Android.mk b/tests/Android.mk index 7cba33ac39..d808873d11 100644 --- a/tests/Android.mk +++ b/tests/Android.mk @@ -27,7 +27,8 @@ LOCAL_STATIC_JAVA_LIBRARIES := \ LOCAL_SRC_FILES := $(call all-java-files-under, tapl) \ ../quickstep/src/com/android/quickstep/SwipeUpSetting.java \ - ../src/com/android/launcher3/TestProtocol.java + ../src/com/android/launcher3/util/SecureSettingsObserver.java \ + ../src/com/android/launcher3/TestProtocol.java \ LOCAL_SDK_VERSION := current LOCAL_MODULE := ub-launcher-aosp-tapl diff --git a/tests/AndroidManifest-common.xml b/tests/AndroidManifest-common.xml index 89ca7f337d..439058cbda 100644 --- a/tests/AndroidManifest-common.xml +++ b/tests/AndroidManifest-common.xml @@ -67,5 +67,33 @@ + + + + + + + + + + + + diff --git a/tests/src/com/android/launcher3/model/AddWorkspaceItemsTaskTest.java b/tests/src/com/android/launcher3/model/AddWorkspaceItemsTaskTest.java index 38e211b69c..6673ba9280 100644 --- a/tests/src/com/android/launcher3/model/AddWorkspaceItemsTaskTest.java +++ b/tests/src/com/android/launcher3/model/AddWorkspaceItemsTaskTest.java @@ -23,7 +23,8 @@ import com.android.launcher3.LauncherProvider; import com.android.launcher3.LauncherSettings; import com.android.launcher3.ShortcutInfo; import com.android.launcher3.util.GridOccupancy; -import com.android.launcher3.util.LongArrayMap; +import com.android.launcher3.util.IntArray; +import com.android.launcher3.util.IntSparseArrayMap; import org.junit.Before; import org.junit.Test; @@ -43,15 +44,15 @@ public class AddWorkspaceItemsTaskTest extends BaseModelUpdateTaskTestCase { private final ComponentName mComponent1 = new ComponentName("a", "b"); private final ComponentName mComponent2 = new ComponentName("b", "b"); - private ArrayList existingScreens; - private ArrayList newScreens; - private LongArrayMap screenOccupancy; + private IntArray existingScreens; + private IntArray newScreens; + private IntSparseArrayMap screenOccupancy; @Before public void initData() throws Exception { - existingScreens = new ArrayList<>(); - screenOccupancy = new LongArrayMap<>(); - newScreens = new ArrayList<>(); + existingScreens = new IntArray(); + screenOccupancy = new IntSparseArrayMap<>(); + newScreens = new IntArray(); idp.numColumns = 5; idp.numRows = 5; @@ -65,7 +66,7 @@ public class AddWorkspaceItemsTaskTest extends BaseModelUpdateTaskTestCase { return new AddWorkspaceItemsTask(list) { @Override - protected void updateScreens(Context context, ArrayList workspaceScreens) { } + protected void updateScreens(Context context, IntArray workspaceScreens) { } }; } @@ -77,18 +78,18 @@ public class AddWorkspaceItemsTaskTest extends BaseModelUpdateTaskTestCase { // Second screen has 2 holes of sizes 3x2 and 2x3 setupWorkspaceWithHoles(nextId, 2, new Rect(2, 0, 5, 2), new Rect(0, 2, 2, 5)); - Pair spaceFound = newTask() + int[] spaceFound = newTask() .findSpaceForItem(appState, bgDataModel, existingScreens, newScreens, 1, 1); - assertEquals(2L, (long) spaceFound.first); - assertTrue(screenOccupancy.get(spaceFound.first) - .isRegionVacant(spaceFound.second[0], spaceFound.second[1], 1, 1)); + assertEquals(2, spaceFound[0]); + assertTrue(screenOccupancy.get(spaceFound[0]) + .isRegionVacant(spaceFound[1], spaceFound[2], 1, 1)); // Find a larger space spaceFound = newTask() .findSpaceForItem(appState, bgDataModel, existingScreens, newScreens, 2, 3); - assertEquals(2L, (long) spaceFound.first); - assertTrue(screenOccupancy.get(spaceFound.first) - .isRegionVacant(spaceFound.second[0], spaceFound.second[1], 2, 3)); + assertEquals(2, spaceFound[0]); + assertTrue(screenOccupancy.get(spaceFound[0]) + .isRegionVacant(spaceFound[1], spaceFound[2], 2, 3)); } @Test @@ -97,11 +98,11 @@ public class AddWorkspaceItemsTaskTest extends BaseModelUpdateTaskTestCase { setupWorkspaceWithHoles(1, 1, new Rect(2, 0, 5, 2), new Rect(0, 2, 2, 5)); commitScreensToDb(); - ArrayList oldScreens = new ArrayList<>(existingScreens); - Pair spaceFound = newTask() + IntArray oldScreens = existingScreens.clone(); + int[] spaceFound = newTask() .findSpaceForItem(appState, bgDataModel, existingScreens, newScreens, 3, 3); - assertFalse(oldScreens.contains(spaceFound.first)); - assertTrue(newScreens.contains(spaceFound.first)); + assertFalse(oldScreens.contains(spaceFound[0])); + assertTrue(newScreens.contains(spaceFound[0])); } @Test @@ -135,7 +136,7 @@ public class AddWorkspaceItemsTaskTest extends BaseModelUpdateTaskTestCase { // only info2 should be added because info was already added to the workspace // in setupWorkspaceWithHoles() - verify(callbacks).bindAppsAdded(any(ArrayList.class), notAnimated.capture(), + verify(callbacks).bindAppsAdded(any(IntArray.class), notAnimated.capture(), animated.capture()); assertTrue(notAnimated.getValue().isEmpty()); @@ -143,7 +144,7 @@ public class AddWorkspaceItemsTaskTest extends BaseModelUpdateTaskTestCase { assertTrue(animated.getValue().contains(info2)); } - private int setupWorkspaceWithHoles(int startId, long screenId, Rect... holes) { + private int setupWorkspaceWithHoles(int startId, int screenId, Rect... holes) { GridOccupancy occupancy = new GridOccupancy(idp.numColumns, idp.numRows); occupancy.markCells(0, 0, idp.numColumns, idp.numRows, true); for (Rect r : holes) { @@ -183,7 +184,7 @@ public class AddWorkspaceItemsTaskTest extends BaseModelUpdateTaskTestCase { int count = existingScreens.size(); for (int i = 0; i < count; i++) { ContentValues v = new ContentValues(); - long screenId = existingScreens.get(i); + int screenId = existingScreens.get(i); v.put(LauncherSettings.WorkspaceScreens._ID, screenId); v.put(LauncherSettings.WorkspaceScreens.SCREEN_RANK, i); ops.add(ContentProviderOperation.newInsert(uri).withValues(v).build()); diff --git a/tests/src/com/android/launcher3/model/CacheDataUpdatedTaskTest.java b/tests/src/com/android/launcher3/model/CacheDataUpdatedTaskTest.java index db80044454..5fe9018e14 100644 --- a/tests/src/com/android/launcher3/model/CacheDataUpdatedTaskTest.java +++ b/tests/src/com/android/launcher3/model/CacheDataUpdatedTaskTest.java @@ -53,7 +53,7 @@ public class CacheDataUpdatedTaskTest extends BaseModelUpdateTaskTestCase { // Verify that only the app icons of app1 (id 1 & 2) are updated. Custom shortcut (id 7) // is not updated - verifyUpdate(1L, 2L); + verifyUpdate(1, 2); // Verify that only app1 var updated in allAppsList assertFalse(allAppsList.data.isEmpty()); @@ -80,11 +80,11 @@ public class CacheDataUpdatedTaskTest extends BaseModelUpdateTaskTestCase { // app3 has only restored apps (id 5, 6) and shortcuts (id 9). Verify that only apps were // were updated - verifyUpdate(5L, 6L); + verifyUpdate(5, 6); } - private void verifyUpdate(Long... idsUpdated) { - HashSet updates = new HashSet<>(Arrays.asList(idsUpdated)); + private void verifyUpdate(Integer... idsUpdated) { + HashSet updates = new HashSet<>(Arrays.asList(idsUpdated)); for (ItemInfo info : bgDataModel.itemsIdMap) { if (updates.contains(info.id)) { assertEquals(NEW_LABEL_PREFIX + info.id, info.title); diff --git a/tests/src/com/android/launcher3/model/GridSizeMigrationTaskTest.java b/tests/src/com/android/launcher3/model/GridSizeMigrationTaskTest.java index b2fd1d9687..7fa401b14a 100644 --- a/tests/src/com/android/launcher3/model/GridSizeMigrationTaskTest.java +++ b/tests/src/com/android/launcher3/model/GridSizeMigrationTaskTest.java @@ -17,6 +17,7 @@ import com.android.launcher3.LauncherModel; import com.android.launcher3.LauncherProvider; import com.android.launcher3.LauncherSettings; import com.android.launcher3.model.GridSizeMigrationTask.MultiStepMigrationTask; +import com.android.launcher3.util.IntArray; import com.android.launcher3.util.TestLauncherProvider; import org.junit.Before; @@ -24,7 +25,6 @@ import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; -import java.util.ArrayList; import java.util.HashSet; import java.util.LinkedList; @@ -43,8 +43,8 @@ public class GridSizeMigrationTaskTest { new ProviderTestRule.Builder(TestLauncherProvider.class, LauncherProvider.AUTHORITY) .build(); - private static final long DESKTOP = LauncherSettings.Favorites.CONTAINER_DESKTOP; - private static final long HOTSEAT = LauncherSettings.Favorites.CONTAINER_HOTSEAT; + private static final int DESKTOP = LauncherSettings.Favorites.CONTAINER_DESKTOP; + private static final int HOTSEAT = LauncherSettings.Favorites.CONTAINER_HOTSEAT; private static final int APPLICATION = LauncherSettings.Favorites.ITEM_TYPE_APPLICATION; private static final int SHORTCUT = LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT; @@ -75,7 +75,7 @@ public class GridSizeMigrationTaskTest { @Test public void testHotseatMigration_apps_dropped() throws Exception { - long[] hotseatItems = { + int[] hotseatItems = { addItem(APPLICATION, 0, HOTSEAT, 0, 0), addItem(SHORTCUT, 1, HOTSEAT, 0, 0), -1, @@ -92,7 +92,7 @@ public class GridSizeMigrationTaskTest { @Test public void testHotseatMigration_shortcuts_dropped() throws Exception { - long[] hotseatItems = { + int[] hotseatItems = { addItem(APPLICATION, 0, HOTSEAT, 0, 0), addItem(30, 1, HOTSEAT, 0, 0), -1, @@ -107,11 +107,11 @@ public class GridSizeMigrationTaskTest { verifyHotseat(hotseatItems[1], hotseatItems[3], hotseatItems[4]); } - private void verifyHotseat(long... sortedIds) { + private void verifyHotseat(int... sortedIds) { int screenId = 0; int total = 0; - for (long id : sortedIds) { + for (int id : sortedIds) { Cursor c = mProviderRule.getResolver().query(LauncherSettings.Favorites.CONTENT_URI, new String[]{LauncherSettings.Favorites._ID}, "container=-101 and screen=" + screenId, null, null, null); @@ -139,7 +139,7 @@ public class GridSizeMigrationTaskTest { @Test public void testWorkspace_empty_row_column_removed() throws Exception { - long[][][] ids = createGrid(new int[][][]{{ + int[][][] ids = createGrid(new int[][][]{{ { 0, 0, -1, 1}, { 3, 1, -1, 4}, { -1, -1, -1, -1}, @@ -150,7 +150,7 @@ public class GridSizeMigrationTaskTest { new Point(4, 4), new Point(3, 3)).migrateWorkspace(); // Column 2 and row 2 got removed. - verifyWorkspace(new long[][][] {{ + verifyWorkspace(new int[][][] {{ {ids[0][0][0], ids[0][0][1], ids[0][0][3]}, {ids[0][1][0], ids[0][1][1], ids[0][1][3]}, {ids[0][3][0], ids[0][3][1], ids[0][3][3]}, @@ -159,7 +159,7 @@ public class GridSizeMigrationTaskTest { @Test public void testWorkspace_new_screen_created() throws Exception { - long[][][] ids = createGrid(new int[][][]{{ + int[][][] ids = createGrid(new int[][][]{{ { 0, 0, 0, 1}, { 3, 1, 0, 4}, { -1, -1, -1, -1}, @@ -170,7 +170,7 @@ public class GridSizeMigrationTaskTest { new Point(4, 4), new Point(3, 3)).migrateWorkspace(); // Items in the second column get moved to new screen - verifyWorkspace(new long[][][] {{ + verifyWorkspace(new int[][][] {{ {ids[0][0][0], ids[0][0][1], ids[0][0][3]}, {ids[0][1][0], ids[0][1][1], ids[0][1][3]}, {ids[0][3][0], ids[0][3][1], ids[0][3][3]}, @@ -181,7 +181,7 @@ public class GridSizeMigrationTaskTest { @Test public void testWorkspace_items_merged_in_next_screen() throws Exception { - long[][][] ids = createGrid(new int[][][]{{ + int[][][] ids = createGrid(new int[][][]{{ { 0, 0, 0, 1}, { 3, 1, 0, 4}, { -1, -1, -1, -1}, @@ -196,7 +196,7 @@ public class GridSizeMigrationTaskTest { // Items in the second column of the first screen should get placed on the 3rd // row of the second screen - verifyWorkspace(new long[][][] {{ + verifyWorkspace(new int[][][] {{ {ids[0][0][0], ids[0][0][1], ids[0][0][3]}, {ids[0][1][0], ids[0][1][1], ids[0][1][3]}, {ids[0][3][0], ids[0][3][1], ids[0][3][3]}, @@ -211,7 +211,7 @@ public class GridSizeMigrationTaskTest { public void testWorkspace_items_not_merged_in_next_screen() throws Exception { // First screen has 2 items that need to be moved, but second screen has only one // empty space after migration (top-left corner) - long[][][] ids = createGrid(new int[][][]{{ + int[][][] ids = createGrid(new int[][][]{{ { 0, 0, 0, 1}, { 3, 1, 0, 4}, { -1, -1, -1, -1}, @@ -227,7 +227,7 @@ public class GridSizeMigrationTaskTest { new Point(4, 4), new Point(3, 3)).migrateWorkspace(); // Items in the second column of the first screen should get placed on a new screen. - verifyWorkspace(new long[][][] {{ + verifyWorkspace(new int[][][] {{ {ids[0][0][0], ids[0][0][1], ids[0][0][3]}, {ids[0][1][0], ids[0][1][1], ids[0][1][3]}, {ids[0][3][0], ids[0][3][1], ids[0][3][3]}, @@ -244,7 +244,7 @@ public class GridSizeMigrationTaskTest { public void testWorkspace_first_row_blocked() throws Exception { // The first screen has one item on the 4th column which needs moving, as the first row // will be kept empty. - long[][][] ids = createGrid(new int[][][]{{ + int[][][] ids = createGrid(new int[][][]{{ { -1, -1, -1, -1}, { 3, 1, 7, 0}, { 8, 7, 7, -1}, @@ -255,7 +255,7 @@ public class GridSizeMigrationTaskTest { new Point(4, 4), new Point(3, 4)).migrateWorkspace(); // Items in the second column of the first screen should get placed on a new screen. - verifyWorkspace(new long[][][] {{ + verifyWorkspace(new int[][][] {{ { -1, -1, -1}, {ids[0][1][0], ids[0][1][1], ids[0][1][2]}, {ids[0][2][0], ids[0][2][1], ids[0][2][2]}, @@ -268,7 +268,7 @@ public class GridSizeMigrationTaskTest { @Test public void testWorkspace_items_moved_to_empty_first_row() throws Exception { // Items will get moved to the next screen to keep the first screen empty. - long[][][] ids = createGrid(new int[][][]{{ + int[][][] ids = createGrid(new int[][][]{{ { -1, -1, -1, -1}, { 0, 1, 0, 0}, { 8, 7, 7, -1}, @@ -279,7 +279,7 @@ public class GridSizeMigrationTaskTest { new Point(4, 4), new Point(3, 3)).migrateWorkspace(); // Items in the second column of the first screen should get placed on a new screen. - verifyWorkspace(new long[][][] {{ + verifyWorkspace(new int[][][] {{ { -1, -1, -1}, {ids[0][2][0], ids[0][2][1], ids[0][2][2]}, {ids[0][3][0], ids[0][3][1], ids[0][3][2]}, @@ -289,7 +289,7 @@ public class GridSizeMigrationTaskTest { }}); } - private long[][][] createGrid(int[][][] typeArray) throws Exception { + private int[][][] createGrid(int[][][] typeArray) throws Exception { return createGrid(typeArray, 1); } @@ -300,14 +300,14 @@ public class GridSizeMigrationTaskTest { * two represent the workspace grid. * @return the same grid representation where each entry is the corresponding item id. */ - private long[][][] createGrid(int[][][] typeArray, long startScreen) throws Exception { + private int[][][] createGrid(int[][][] typeArray, int startScreen) throws Exception { LauncherSettings.Settings.call(mProviderRule.getResolver(), LauncherSettings.Settings.METHOD_CREATE_EMPTY_DB); - long[][][] ids = new long[typeArray.length][][]; + int[][][] ids = new int[typeArray.length][][]; for (int i = 0; i < typeArray.length; i++) { // Add screen to DB - long screenId = startScreen + i; + int screenId = startScreen + i; // Keep the screen id counter up to date LauncherSettings.Settings.call(mProviderRule.getResolver(), @@ -318,9 +318,9 @@ public class GridSizeMigrationTaskTest { v.put(LauncherSettings.WorkspaceScreens.SCREEN_RANK, i); mProviderRule.getResolver().insert(LauncherSettings.WorkspaceScreens.CONTENT_URI, v); - ids[i] = new long[typeArray[i].length][]; + ids[i] = new int[typeArray[i].length][]; for (int y = 0; y < typeArray[i].length; y++) { - ids[i][y] = new long[typeArray[i][y].length]; + ids[i][y] = new int[typeArray[i][y].length]; for (int x = 0; x < typeArray[i][y].length; x++) { if (typeArray[i][y][x] < 0) { // Empty cell @@ -339,16 +339,16 @@ public class GridSizeMigrationTaskTest { * @param ids A 3d array where the first dimension represents the screen, and the rest two * represent the workspace grid. */ - private void verifyWorkspace(long[][][] ids) { - ArrayList allScreens = LauncherModel.loadWorkspaceScreensDb(mContext); + private void verifyWorkspace(int[][][] ids) { + IntArray allScreens = LauncherModel.loadWorkspaceScreensDb(mContext); assertEquals(ids.length, allScreens.size()); int total = 0; for (int i = 0; i < ids.length; i++) { - long screenId = allScreens.get(i); + int screenId = allScreens.get(i); for (int y = 0; y < ids[i].length; y++) { for (int x = 0; x < ids[i][y].length; x++) { - long id = ids[i][y][x]; + int id = ids[i][y][x]; Cursor c = mProviderRule.getResolver().query( LauncherSettings.Favorites.CONTENT_URI, @@ -382,10 +382,10 @@ public class GridSizeMigrationTaskTest { * @param type {@link #APPLICATION} or {@link #SHORTCUT} or >= 2 for * folder (where the type represents the number of items in the folder). */ - private long addItem(int type, long screen, long container, int x, int y) throws Exception { - long id = LauncherSettings.Settings.call(mProviderRule.getResolver(), + private int addItem(int type, int screen, int container, int x, int y) throws Exception { + int id = LauncherSettings.Settings.call(mProviderRule.getResolver(), LauncherSettings.Settings.METHOD_NEW_ITEM_ID) - .getLong(LauncherSettings.Settings.EXTRA_VALUE); + .getInt(LauncherSettings.Settings.EXTRA_VALUE); ContentValues values = new ContentValues(); values.put(LauncherSettings.Favorites._ID, id); diff --git a/tests/src/com/android/launcher3/model/LoaderCursorTest.java b/tests/src/com/android/launcher3/model/LoaderCursorTest.java index a21f861771..ac1be1777a 100644 --- a/tests/src/com/android/launcher3/model/LoaderCursorTest.java +++ b/tests/src/com/android/launcher3/model/LoaderCursorTest.java @@ -18,6 +18,7 @@ import com.android.launcher3.ShortcutInfo; import com.android.launcher3.Utilities; import com.android.launcher3.compat.LauncherAppsCompat; import com.android.launcher3.icons.BitmapInfo; +import com.android.launcher3.util.IntArray; import org.junit.Before; import org.junit.Test; @@ -149,83 +150,83 @@ public class LoaderCursorTest { @Test public void checkItemPlacement_wrongWorkspaceScreen() { - ArrayList workspaceScreens = new ArrayList<>(Arrays.asList(1L, 3L)); + IntArray workspaceScreens = IntArray.wrap(1, 3); mIDP.numRows = 4; mIDP.numColumns = 4; mIDP.numHotseatIcons = 3; // Item on unknown screen are not placed assertFalse(mLoaderCursor.checkItemPlacement( - newItemInfo(0, 0, 1, 1, CONTAINER_DESKTOP, 4L), workspaceScreens)); + newItemInfo(0, 0, 1, 1, CONTAINER_DESKTOP, 4), workspaceScreens)); assertFalse(mLoaderCursor.checkItemPlacement( - newItemInfo(0, 0, 1, 1, CONTAINER_DESKTOP, 5L), workspaceScreens)); + newItemInfo(0, 0, 1, 1, CONTAINER_DESKTOP, 5), workspaceScreens)); assertFalse(mLoaderCursor.checkItemPlacement( - newItemInfo(0, 0, 1, 1, CONTAINER_DESKTOP, 2L), workspaceScreens)); + newItemInfo(0, 0, 1, 1, CONTAINER_DESKTOP, 2), workspaceScreens)); assertTrue(mLoaderCursor.checkItemPlacement( - newItemInfo(0, 0, 1, 1, CONTAINER_DESKTOP, 1L), workspaceScreens)); + newItemInfo(0, 0, 1, 1, CONTAINER_DESKTOP, 1), workspaceScreens)); assertTrue(mLoaderCursor.checkItemPlacement( - newItemInfo(0, 0, 1, 1, CONTAINER_DESKTOP, 3L), workspaceScreens)); + newItemInfo(0, 0, 1, 1, CONTAINER_DESKTOP, 3), workspaceScreens)); } @Test public void checkItemPlacement_outsideBounds() { - ArrayList workspaceScreens = new ArrayList<>(Arrays.asList(1L, 2L)); + IntArray workspaceScreens = IntArray.wrap(1, 2); mIDP.numRows = 4; mIDP.numColumns = 4; mIDP.numHotseatIcons = 3; // Item outside screen bounds are not placed assertFalse(mLoaderCursor.checkItemPlacement( - newItemInfo(4, 4, 1, 1, CONTAINER_DESKTOP, 1L), workspaceScreens)); + newItemInfo(4, 4, 1, 1, CONTAINER_DESKTOP, 1), workspaceScreens)); } @Test public void checkItemPlacement_overlappingItems() { - ArrayList workspaceScreens = new ArrayList<>(Arrays.asList(1L, 2L)); + IntArray workspaceScreens = IntArray.wrap(1, 2); mIDP.numRows = 4; mIDP.numColumns = 4; mIDP.numHotseatIcons = 3; // Overlapping items are not placed assertTrue(mLoaderCursor.checkItemPlacement( - newItemInfo(0, 0, 1, 1, CONTAINER_DESKTOP, 1L), workspaceScreens)); + newItemInfo(0, 0, 1, 1, CONTAINER_DESKTOP, 1), workspaceScreens)); assertFalse(mLoaderCursor.checkItemPlacement( - newItemInfo(0, 0, 1, 1, CONTAINER_DESKTOP, 1L), workspaceScreens)); + newItemInfo(0, 0, 1, 1, CONTAINER_DESKTOP, 1), workspaceScreens)); assertTrue(mLoaderCursor.checkItemPlacement( - newItemInfo(0, 0, 1, 1, CONTAINER_DESKTOP, 2L), workspaceScreens)); + newItemInfo(0, 0, 1, 1, CONTAINER_DESKTOP, 2), workspaceScreens)); assertFalse(mLoaderCursor.checkItemPlacement( - newItemInfo(0, 0, 1, 1, CONTAINER_DESKTOP, 2L), workspaceScreens)); + newItemInfo(0, 0, 1, 1, CONTAINER_DESKTOP, 2), workspaceScreens)); assertTrue(mLoaderCursor.checkItemPlacement( - newItemInfo(1, 1, 1, 1, CONTAINER_DESKTOP, 1L), workspaceScreens)); + newItemInfo(1, 1, 1, 1, CONTAINER_DESKTOP, 1), workspaceScreens)); assertTrue(mLoaderCursor.checkItemPlacement( - newItemInfo(2, 2, 2, 2, CONTAINER_DESKTOP, 1L), workspaceScreens)); + newItemInfo(2, 2, 2, 2, CONTAINER_DESKTOP, 1), workspaceScreens)); assertFalse(mLoaderCursor.checkItemPlacement( - newItemInfo(3, 2, 1, 2, CONTAINER_DESKTOP, 1L), workspaceScreens)); + newItemInfo(3, 2, 1, 2, CONTAINER_DESKTOP, 1), workspaceScreens)); } @Test public void checkItemPlacement_hotseat() { - ArrayList workspaceScreens = new ArrayList<>(); + IntArray workspaceScreens = new IntArray(); mIDP.numRows = 4; mIDP.numColumns = 4; mIDP.numHotseatIcons = 3; // Hotseat items are only placed based on screenId assertTrue(mLoaderCursor.checkItemPlacement( - newItemInfo(3, 3, 1, 1, CONTAINER_HOTSEAT, 1L), workspaceScreens)); + newItemInfo(3, 3, 1, 1, CONTAINER_HOTSEAT, 1), workspaceScreens)); assertTrue(mLoaderCursor.checkItemPlacement( - newItemInfo(3, 3, 1, 1, CONTAINER_HOTSEAT, 2L), workspaceScreens)); + newItemInfo(3, 3, 1, 1, CONTAINER_HOTSEAT, 2), workspaceScreens)); assertFalse(mLoaderCursor.checkItemPlacement( - newItemInfo(3, 3, 1, 1, CONTAINER_HOTSEAT, 3L), workspaceScreens)); + newItemInfo(3, 3, 1, 1, CONTAINER_HOTSEAT, 3), workspaceScreens)); } private ItemInfo newItemInfo(int cellX, int cellY, int spanX, int spanY, - long container, long screenId) { + int container, int screenId) { ItemInfo info = new ItemInfo(); info.cellX = cellX; info.cellY = cellY; diff --git a/tests/src/com/android/launcher3/model/PackageInstallStateChangedTaskTest.java b/tests/src/com/android/launcher3/model/PackageInstallStateChangedTaskTest.java index 435686ff8c..25100dc3d0 100644 --- a/tests/src/com/android/launcher3/model/PackageInstallStateChangedTaskTest.java +++ b/tests/src/com/android/launcher3/model/PackageInstallStateChangedTaskTest.java @@ -48,18 +48,18 @@ public class PackageInstallStateChangedTaskTest extends BaseModelUpdateTaskTestC public void testSessionUpdate_shortcuts_updated() throws Exception { executeTaskForTest(newTask("app3", 30)); - verifyProgressUpdate(30, 5L, 6L, 7L); + verifyProgressUpdate(30, 5, 6, 7); } @Test public void testSessionUpdate_widgets_updated() throws Exception { executeTaskForTest(newTask("app4", 30)); - verifyProgressUpdate(30, 8L, 9L); + verifyProgressUpdate(30, 8, 9); } - private void verifyProgressUpdate(int progress, Long... idsUpdated) { - HashSet updates = new HashSet<>(Arrays.asList(idsUpdated)); + private void verifyProgressUpdate(int progress, Integer... idsUpdated) { + HashSet updates = new HashSet<>(Arrays.asList(idsUpdated)); for (ItemInfo info : bgDataModel.itemsIdMap) { if (info instanceof ShortcutInfo) { assertEquals(updates.contains(info.id) ? progress: 0, diff --git a/tests/src/com/android/launcher3/testcomponent/TestCommandReceiver.java b/tests/src/com/android/launcher3/testcomponent/TestCommandReceiver.java new file mode 100644 index 0000000000..04c04f5b11 --- /dev/null +++ b/tests/src/com/android/launcher3/testcomponent/TestCommandReceiver.java @@ -0,0 +1,96 @@ +/* + * 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.testcomponent; + +import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DISABLED; +import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_ENABLED; +import static android.content.pm.PackageManager.DONT_KILL_APP; + +import android.app.Instrumentation; +import android.content.ComponentName; +import android.content.ContentProvider; +import android.content.ContentValues; +import android.database.Cursor; +import android.net.Uri; +import android.os.Bundle; + +import androidx.test.InstrumentationRegistry; + +/** + * Content provider to receive commands from tests + */ +public class TestCommandReceiver extends ContentProvider { + + public static final String ENABLE_TEST_LAUNCHER = "enable-test-launcher"; + public static final String DISABLE_TEST_LAUNCHER = "disable-test-launcher"; + + @Override + public boolean onCreate() { + return true; + } + + @Override + public int delete(Uri uri, String selection, String[] selectionArgs) { + throw new UnsupportedOperationException("unimplemented mock method"); + } + + @Override + public String getType(Uri uri) { + throw new UnsupportedOperationException("unimplemented mock method"); + } + + @Override + public Uri insert(Uri uri, ContentValues values) { + throw new UnsupportedOperationException("unimplemented mock method"); + } + + @Override + public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, + String sortOrder) { + throw new UnsupportedOperationException("unimplemented mock method"); + } + + @Override + public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { + throw new UnsupportedOperationException("unimplemented mock method"); + } + + @Override + public Bundle call(String method, String arg, Bundle extras) { + switch (method) { + case ENABLE_TEST_LAUNCHER: { + getContext().getPackageManager().setComponentEnabledSetting( + new ComponentName(getContext(), TestLauncherActivity.class), + COMPONENT_ENABLED_STATE_ENABLED, DONT_KILL_APP); + return null; + } + case DISABLE_TEST_LAUNCHER: { + getContext().getPackageManager().setComponentEnabledSetting( + new ComponentName(getContext(), TestLauncherActivity.class), + COMPONENT_ENABLED_STATE_DISABLED, DONT_KILL_APP); + return null; + } + + } + return super.call(method, arg, extras); + } + + public static Bundle callCommand(String command) { + Instrumentation inst = InstrumentationRegistry.getInstrumentation(); + Uri uri = Uri.parse("content://" + inst.getContext().getPackageName() + ".commands"); + return inst.getTargetContext().getContentResolver().call(uri, command, null, null); + } +} diff --git a/tests/src/com/android/launcher3/testcomponent/TestLauncherActivity.java b/tests/src/com/android/launcher3/testcomponent/TestLauncherActivity.java new file mode 100644 index 0000000000..357a2322d2 --- /dev/null +++ b/tests/src/com/android/launcher3/testcomponent/TestLauncherActivity.java @@ -0,0 +1,34 @@ +/* + * 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.testcomponent; + +import static android.content.Intent.ACTION_MAIN; +import static android.content.Intent.CATEGORY_LAUNCHER; +import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; +import static android.content.Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED; + +import android.app.LauncherActivity; +import android.content.Intent; + +public class TestLauncherActivity extends LauncherActivity { + + @Override + protected Intent getTargetIntent() { + return new Intent(ACTION_MAIN, null) + .addCategory(CATEGORY_LAUNCHER) + .addFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_RESET_TASK_IF_NEEDED); + } +} diff --git a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java index 9cbab5e183..c878699c50 100644 --- a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java +++ b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java @@ -15,26 +15,21 @@ */ package com.android.launcher3.ui; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; +import static androidx.test.InstrumentationRegistry.getInstrumentation; + import static org.junit.Assert.fail; import android.app.Instrumentation; import android.content.BroadcastReceiver; -import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.LauncherActivityInfo; -import android.graphics.Point; import android.os.Process; import android.os.RemoteException; -import android.os.SystemClock; -import android.util.Log; -import android.view.MotionEvent; +import android.view.Surface; import androidx.test.InstrumentationRegistry; -import androidx.test.uiautomator.By; import androidx.test.uiautomator.BySelector; import androidx.test.uiautomator.Direction; import androidx.test.uiautomator.UiDevice; @@ -43,23 +38,28 @@ import androidx.test.uiautomator.Until; import com.android.launcher3.Launcher; import com.android.launcher3.LauncherAppState; -import com.android.launcher3.LauncherAppWidgetProviderInfo; +import com.android.launcher3.LauncherModel; import com.android.launcher3.LauncherSettings; import com.android.launcher3.LauncherState; import com.android.launcher3.MainThreadExecutor; -import com.android.launcher3.R; -import com.android.launcher3.compat.AppWidgetManagerCompat; +import com.android.launcher3.Utilities; import com.android.launcher3.compat.LauncherAppsCompat; import com.android.launcher3.tapl.LauncherInstrumentation; -import com.android.launcher3.testcomponent.AppWidgetNoConfig; -import com.android.launcher3.testcomponent.AppWidgetWithConfig; +import com.android.launcher3.tapl.TestHelpers; import com.android.launcher3.util.Wait; import com.android.launcher3.util.rule.LauncherActivityRule; +import com.android.launcher3.util.rule.ShellCommandRule; import org.junit.After; import org.junit.Before; import org.junit.Rule; +import org.junit.rules.TestRule; +import org.junit.runners.model.Statement; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; import java.util.concurrent.Callable; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -76,39 +76,88 @@ public abstract class AbstractLauncherUiTest { public static final long SHORT_UI_TIMEOUT= 300; public static final long DEFAULT_UI_TIMEOUT = 10000; - public static final long DEFAULT_WORKER_TIMEOUT_SECS = 5; protected MainThreadExecutor mMainThreadExecutor = new MainThreadExecutor(); protected final UiDevice mDevice; protected final LauncherInstrumentation mLauncher; protected Context mTargetContext; protected String mTargetPackage; - protected final boolean mIsInLauncherProcess; - - private static final String TAG = "AbstractLauncherUiTest"; protected AbstractLauncherUiTest() { final Instrumentation instrumentation = getInstrumentation(); mDevice = UiDevice.getInstance(instrumentation); + try { + mDevice.setOrientationNatural(); + } catch (RemoteException e) { + throw new RuntimeException(e); + } + if (TestHelpers.isInLauncherProcess()) Utilities.enableRunningInTestHarnessForTests(); mLauncher = new LauncherInstrumentation(instrumentation); - - mIsInLauncherProcess = instrumentation.getTargetContext().getPackageName().equals( - mDevice.getLauncherPackageName()); } @Rule public LauncherActivityRule mActivityMonitor = new LauncherActivityRule(); + @Rule public ShellCommandRule mDefaultLauncherRule = + TestHelpers.isInLauncherProcess() ? ShellCommandRule.setDefaultLauncher() : null; + + @Rule public ShellCommandRule mDisableHeadsUpNotification = + ShellCommandRule.disableHeadsUpNotification(); + + // Annotation for tests that need to be run in portrait and landscape modes. + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.METHOD) + protected @interface PortraitLandscape { + } + + @Rule + public TestRule mPortraitLandscapeExecutor = + (base, description) -> false && description.getAnnotation(PortraitLandscape.class) + != null ? new Statement() { + @Override + public void evaluate() throws Throwable { + try { + // Create launcher activity if necessary and bring it to the front. + mDevice.pressHome(); + waitForLauncherCondition("Launcher activity wasn't created", + launcher -> launcher != null); + + executeOnLauncher(launcher -> + launcher.getRotationHelper().forceAllowRotationForTesting(true)); + + evaluateInPortrait(); + evaluateInLandscape(); + } finally { + mDevice.setOrientationNatural(); + executeOnLauncher(launcher -> + launcher.getRotationHelper().forceAllowRotationForTesting(false)); + mLauncher.setExpectedRotation(Surface.ROTATION_0); + } + } + + private void evaluateInPortrait() throws Throwable { + mDevice.setOrientationNatural(); + mLauncher.setExpectedRotation(Surface.ROTATION_0); + base.evaluate(); + } + + private void evaluateInLandscape() throws Throwable { + mDevice.setOrientationLeft(); + mLauncher.setExpectedRotation(Surface.ROTATION_90); + base.evaluate(); + } + } : base; + @Before public void setUp() throws Exception { mTargetContext = InstrumentationRegistry.getTargetContext(); mTargetPackage = mTargetContext.getPackageName(); - mDevice.executeShellCommand("settings put global heads_up_notifications_enabled 0"); } @After public void tearDown() throws Exception { - mDevice.executeShellCommand("settings put global heads_up_notifications_enabled 1"); + // Limits UI tests affecting tests running after them. + waitForModelLoaded(); } protected void lockRotation(boolean naturalOrientation) throws RemoteException { @@ -119,34 +168,6 @@ public abstract class AbstractLauncherUiTest { } } - protected Instrumentation getInstrumentation() { - return InstrumentationRegistry.getInstrumentation(); - } - - /** - * Opens all apps and returns the recycler view - */ - protected UiObject2 openAllApps() { - mDevice.waitForIdle(); - UiObject2 hotseat = mDevice.wait( - Until.findObject(getSelectorForId(R.id.hotseat)), 2500); - Point start = hotseat.getVisibleCenter(); - int endY = (int) (mDevice.getDisplayHeight() * 0.1f); - // 100 px/step - mDevice.swipe(start.x, start.y, start.x, endY, (start.y - endY) / 100); - return findViewById(R.id.apps_list_view); - } - - /** - * Opens widget tray and returns the recycler view. - */ - protected UiObject2 openWidgetsTray() { - mDevice.pressMenu(); // Enter overview mode. - mDevice.wait(Until.findObject( - By.text(mTargetContext.getString(R.string.widget_button_text))), DEFAULT_UI_TIMEOUT).click(); - return findViewById(R.id.widgets_list_view); - } - /** * Scrolls the {@param container} until it finds an object matching {@param condition}. * @return the matching object. @@ -168,73 +189,6 @@ public abstract class AbstractLauncherUiTest { } } - /** - * Drags an icon to the center of homescreen. - * @param icon object that is either app icon or shortcut icon - */ - protected void dragToWorkspace(UiObject2 icon, boolean expectedToShowShortcuts) { - Point center = icon.getVisibleCenter(); - - // Action Down - sendPointer(MotionEvent.ACTION_DOWN, center); - - UiObject2 dragLayer = findViewById(R.id.drag_layer); - - if (expectedToShowShortcuts) { - // Make sure shortcuts show up, and then move a bit to hide them. - assertNotNull(findViewById(R.id.deep_shortcuts_container)); - - Point moveLocation = new Point(center); - int distanceToMove = mTargetContext.getResources().getDimensionPixelSize( - R.dimen.deep_shortcuts_start_drag_threshold) + 50; - if (moveLocation.y - distanceToMove >= dragLayer.getVisibleBounds().top) { - moveLocation.y -= distanceToMove; - } else { - moveLocation.y += distanceToMove; - } - movePointer(center, moveLocation); - - assertNull(findViewById(R.id.deep_shortcuts_container)); - } - - // Wait until Remove/Delete target is visible - assertNotNull(findViewById(R.id.delete_target_text)); - - Point moveLocation = dragLayer.getVisibleCenter(); - - // Move to center - movePointer(center, moveLocation); - sendPointer(MotionEvent.ACTION_UP, center); - - // Wait until remove target is gone. - mDevice.wait(Until.gone(getSelectorForId(R.id.delete_target_text)), DEFAULT_UI_TIMEOUT); - } - - private void movePointer(Point from, Point to) { - while(!from.equals(to)) { - from.x = getNextMoveValue(to.x, from.x); - from.y = getNextMoveValue(to.y, from.y); - sendPointer(MotionEvent.ACTION_MOVE, from); - } - } - - private int getNextMoveValue(int targetValue, int oldValue) { - if (targetValue - oldValue > 10) { - return oldValue + 10; - } else if (targetValue - oldValue < -10) { - return oldValue - 10; - } else { - return targetValue; - } - } - - protected void sendPointer(int action, Point point) { - MotionEvent event = MotionEvent.obtain(SystemClock.uptimeMillis(), - SystemClock.uptimeMillis(), action, point.x, point.y, 0); - getInstrumentation().sendPointerSync(event); - event.recycle(); - } - /** * Removes all icons from homescreen and hotseat. */ @@ -247,6 +201,11 @@ public abstract class AbstractLauncherUiTest { } protected void resetLoaderState() { + if (com.android.launcher3.Utilities.IS_RUNNING_IN_TEST_HARNESS + && com.android.launcher3.Utilities.IS_DEBUG_DEVICE) { + android.util.Log.d("b/117332845", + "START " + android.util.Log.getStackTraceString(new Throwable())); + } try { mMainThreadExecutor.execute(new Runnable() { @Override @@ -257,6 +216,19 @@ public abstract class AbstractLauncherUiTest { } catch (Throwable t) { throw new IllegalArgumentException(t); } + waitForModelLoaded(); + if (com.android.launcher3.Utilities.IS_RUNNING_IN_TEST_HARNESS + && com.android.launcher3.Utilities.IS_DEBUG_DEVICE) { + android.util.Log.d("b/117332845", + "FINISH " + android.util.Log.getStackTraceString(new Throwable())); + } + } + + protected void waitForModelLoaded() { + waitForLauncherCondition("Launcher model didn't load", launcher -> { + final LauncherModel model = LauncherAppState.getInstance(mTargetContext).getModel(); + return model.getCallback() == null || model.isModelLoaded(); + }); } /** @@ -271,7 +243,7 @@ public abstract class AbstractLauncherUiTest { } protected T getFromLauncher(Function f) { - if (!mIsInLauncherProcess) return null; + if (!TestHelpers.isInLauncherProcess()) return null; return getOnUiThread(() -> f.apply(mActivityMonitor.getActivity())); } @@ -284,53 +256,23 @@ public abstract class AbstractLauncherUiTest { // Cannot be used in TaplTests between a Tapl call injecting a gesture and a tapl call expecting // the results of that gesture because the wait can hide flakeness. - protected boolean waitForState(LauncherState state) { - return waitForLauncherCondition(launcher -> launcher.getStateManager().getState() == state); + protected void waitForState(String message, LauncherState state) { + waitForLauncherCondition(message, + launcher -> launcher.getStateManager().getState() == state); } // Cannot be used in TaplTests after injecting any gesture using Tapl because this can hide // flakiness. - protected boolean waitForLauncherCondition(Function condition) { - return waitForLauncherCondition(condition, DEFAULT_ACTIVITY_TIMEOUT); + protected void waitForLauncherCondition(String message, Function condition) { + waitForLauncherCondition(message, condition, DEFAULT_ACTIVITY_TIMEOUT); } // Cannot be used in TaplTests after injecting any gesture using Tapl because this can hide // flakiness. - protected boolean waitForLauncherCondition( - Function condition, long timeout) { - if (!mIsInLauncherProcess) return true; - return Wait.atMost(() -> getFromLauncher(condition), timeout); - } - - /** - * Finds a widget provider which can fit on the home screen. - * @param hasConfigureScreen if true, a provider with a config screen is returned. - */ - protected LauncherAppWidgetProviderInfo findWidgetProvider(final boolean hasConfigureScreen) { - LauncherAppWidgetProviderInfo info = - getOnUiThread(new Callable() { - @Override - public LauncherAppWidgetProviderInfo call() throws Exception { - ComponentName cn = new ComponentName(getInstrumentation().getContext(), - hasConfigureScreen ? AppWidgetWithConfig.class : AppWidgetNoConfig.class); - Log.d(TAG, "findWidgetProvider componentName=" + cn.flattenToString()); - return AppWidgetManagerCompat.getInstance(mTargetContext) - .findProvider(cn, Process.myUserHandle()); - } - }); - if (info == null) { - throw new IllegalArgumentException("No valid widget provider"); - } - return info; - } - - protected UiObject2 findViewById(int id) { - return mDevice.wait(Until.findObject(getSelectorForId(id)), DEFAULT_UI_TIMEOUT); - } - - protected BySelector getSelectorForId(int id) { - String name = mTargetContext.getResources().getResourceEntryName(id); - return By.res(mTargetPackage, name); + protected void waitForLauncherCondition( + String message, Function condition, long timeout) { + if (!TestHelpers.isInLauncherProcess()) return; + Wait.atMost(message, () -> getFromLauncher(condition), timeout); } protected LauncherActivityInfo getSettingsApp() { diff --git a/tests/src/com/android/launcher3/ui/AllAppsIconToHomeTest.java b/tests/src/com/android/launcher3/ui/AllAppsIconToHomeTest.java index a50a8b1a1e..916007607f 100644 --- a/tests/src/com/android/launcher3/ui/AllAppsIconToHomeTest.java +++ b/tests/src/com/android/launcher3/ui/AllAppsIconToHomeTest.java @@ -11,9 +11,8 @@ import androidx.test.uiautomator.Until; import com.android.launcher3.util.Condition; import com.android.launcher3.util.Wait; -import com.android.launcher3.util.rule.ShellCommandRule; -import org.junit.Rule; +import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; @@ -24,15 +23,15 @@ import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) public class AllAppsIconToHomeTest extends AbstractLauncherUiTest { - @Rule public ShellCommandRule mDefaultLauncherRule = ShellCommandRule.setDefaultLauncher(); - @Test + @Ignore public void testDragIcon_portrait() throws Throwable { lockRotation(true); performTest(); } @Test + @Ignore public void testDragIcon_landscape() throws Throwable { lockRotation(false); performTest(); @@ -46,12 +45,12 @@ public class AllAppsIconToHomeTest extends AbstractLauncherUiTest { mDevice.waitForIdle(); // Open all apps and wait for load complete. - final UiObject2 appsContainer = openAllApps(); - assertTrue(Wait.atMost(Condition.minChildCount(appsContainer, 2), DEFAULT_UI_TIMEOUT)); + final UiObject2 appsContainer = TestViewHelpers.openAllApps(); + Wait.atMost(null, Condition.minChildCount(appsContainer, 2), DEFAULT_UI_TIMEOUT); // Drag icon to homescreen. UiObject2 icon = scrollAndFind(appsContainer, By.text(settingsApp.getLabel().toString())); - dragToWorkspace(icon, true); + TestViewHelpers.dragToWorkspace(icon, true); // Verify that the icon works on homescreen. mDevice.findObject(By.text(settingsApp.getLabel().toString())).click(); diff --git a/tests/src/com/android/launcher3/ui/ShortcutsLaunchTest.java b/tests/src/com/android/launcher3/ui/ShortcutsLaunchTest.java index f8a7bf79e4..d7a7f6bbe7 100644 --- a/tests/src/com/android/launcher3/ui/ShortcutsLaunchTest.java +++ b/tests/src/com/android/launcher3/ui/ShortcutsLaunchTest.java @@ -15,9 +15,8 @@ import android.view.MotionEvent; import com.android.launcher3.R; import com.android.launcher3.util.Condition; import com.android.launcher3.util.Wait; -import com.android.launcher3.util.rule.ShellCommandRule; -import org.junit.Rule; +import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; @@ -28,15 +27,15 @@ import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) public class ShortcutsLaunchTest extends AbstractLauncherUiTest { - @Rule public ShellCommandRule mDefaultLauncherRule = ShellCommandRule.setDefaultLauncher(); - @Test + @Ignore public void testAppLauncher_portrait() throws Exception { lockRotation(true); performTest(); } @Test + @Ignore public void testAppLauncher_landscape() throws Exception { lockRotation(false); performTest(); @@ -47,25 +46,25 @@ public class ShortcutsLaunchTest extends AbstractLauncherUiTest { LauncherActivityInfo testApp = getSettingsApp(); // Open all apps and wait for load complete - final UiObject2 appsContainer = openAllApps(); - assertTrue(Wait.atMost(Condition.minChildCount(appsContainer, 2), - DEFAULT_UI_TIMEOUT)); + final UiObject2 appsContainer = TestViewHelpers.openAllApps(); + Wait.atMost(null, Condition.minChildCount(appsContainer, 2), DEFAULT_UI_TIMEOUT); // Find settings app and verify shortcuts appear when long pressed UiObject2 icon = scrollAndFind(appsContainer, By.text(testApp.getLabel().toString())); // Press icon center until shortcuts appear Point iconCenter = icon.getVisibleCenter(); - sendPointer(MotionEvent.ACTION_DOWN, iconCenter); - UiObject2 deepShortcutsContainer = findViewById(R.id.deep_shortcuts_container); + TestViewHelpers.sendPointer(MotionEvent.ACTION_DOWN, iconCenter); + UiObject2 deepShortcutsContainer = TestViewHelpers.findViewById( + R.id.deep_shortcuts_container); assertNotNull(deepShortcutsContainer); - sendPointer(MotionEvent.ACTION_UP, iconCenter); + TestViewHelpers.sendPointer(MotionEvent.ACTION_UP, iconCenter); // Verify that launching a shortcut opens a page with the same text assertTrue(deepShortcutsContainer.getChildCount() > 0); // Pick second children as it starts showing shortcuts. UiObject2 shortcut = deepShortcutsContainer.getChildren().get(1) - .findObject(getSelectorForId(R.id.bubble_text)); + .findObject(TestViewHelpers.getSelectorForId(R.id.bubble_text)); shortcut.click(); assertTrue(mDevice.wait(Until.hasObject(By.pkg( testApp.getComponentName().getPackageName()) diff --git a/tests/src/com/android/launcher3/ui/ShortcutsToHomeTest.java b/tests/src/com/android/launcher3/ui/ShortcutsToHomeTest.java index 793bd8f46b..436c6991db 100644 --- a/tests/src/com/android/launcher3/ui/ShortcutsToHomeTest.java +++ b/tests/src/com/android/launcher3/ui/ShortcutsToHomeTest.java @@ -15,10 +15,8 @@ import android.view.MotionEvent; import com.android.launcher3.R; import com.android.launcher3.util.Condition; import com.android.launcher3.util.Wait; -import com.android.launcher3.util.rule.ShellCommandRule; import org.junit.Ignore; -import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @@ -29,8 +27,6 @@ import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) public class ShortcutsToHomeTest extends AbstractLauncherUiTest { - @Rule public ShellCommandRule mDefaultLauncherRule = ShellCommandRule.setDefaultLauncher(); - @Test @Ignore public void testDragIcon_portrait() throws Throwable { @@ -52,25 +48,25 @@ public class ShortcutsToHomeTest extends AbstractLauncherUiTest { LauncherActivityInfo testApp = getSettingsApp(); // Open all apps and wait for load complete. - final UiObject2 appsContainer = openAllApps(); - assertTrue(Wait.atMost(Condition.minChildCount(appsContainer, 2), - DEFAULT_UI_TIMEOUT)); + final UiObject2 appsContainer = TestViewHelpers.openAllApps(); + Wait.atMost(null, Condition.minChildCount(appsContainer, 2), DEFAULT_UI_TIMEOUT); // Find the app and long press it to show shortcuts. UiObject2 icon = scrollAndFind(appsContainer, By.text(testApp.getLabel().toString())); // Press icon center until shortcuts appear Point iconCenter = icon.getVisibleCenter(); - sendPointer(MotionEvent.ACTION_DOWN, iconCenter); - UiObject2 deepShortcutsContainer = findViewById(R.id.deep_shortcuts_container); + TestViewHelpers.sendPointer(MotionEvent.ACTION_DOWN, iconCenter); + UiObject2 deepShortcutsContainer = TestViewHelpers.findViewById( + R.id.deep_shortcuts_container); assertNotNull(deepShortcutsContainer); - sendPointer(MotionEvent.ACTION_UP, iconCenter); + TestViewHelpers.sendPointer(MotionEvent.ACTION_UP, iconCenter); // Drag the first shortcut to the home screen. assertTrue(deepShortcutsContainer.getChildCount() > 0); UiObject2 shortcut = deepShortcutsContainer.getChildren().get(1) - .findObject(getSelectorForId(R.id.bubble_text)); + .findObject(TestViewHelpers.getSelectorForId(R.id.bubble_text)); String shortcutName = shortcut.getText(); - dragToWorkspace(shortcut, false); + TestViewHelpers.dragToWorkspace(shortcut, false); // Verify that the shortcut works on home screen // (the app opens and has the same text as the shortcut). diff --git a/tests/src/com/android/launcher3/ui/TestViewHelpers.java b/tests/src/com/android/launcher3/ui/TestViewHelpers.java new file mode 100644 index 0000000000..524438642e --- /dev/null +++ b/tests/src/com/android/launcher3/ui/TestViewHelpers.java @@ -0,0 +1,186 @@ +/* + * 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.launcher3.ui; + +import static androidx.test.InstrumentationRegistry.getInstrumentation; +import static androidx.test.InstrumentationRegistry.getTargetContext; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +import android.content.ComponentName; +import android.content.Context; +import android.graphics.Point; +import android.os.Process; +import android.os.SystemClock; +import android.util.Log; +import android.view.MotionEvent; + +import androidx.test.uiautomator.By; +import androidx.test.uiautomator.BySelector; +import androidx.test.uiautomator.UiDevice; +import androidx.test.uiautomator.UiObject2; +import androidx.test.uiautomator.Until; + +import com.android.launcher3.LauncherAppWidgetProviderInfo; +import com.android.launcher3.R; +import com.android.launcher3.compat.AppWidgetManagerCompat; +import com.android.launcher3.testcomponent.AppWidgetNoConfig; +import com.android.launcher3.testcomponent.AppWidgetWithConfig; + +import java.util.concurrent.Callable; + +public class TestViewHelpers { + private static final String TAG = "TestViewHelpers"; + + private static UiDevice getDevice() { + return UiDevice.getInstance(getInstrumentation()); + } + + /** + * Opens all apps and returns the recycler view + */ + public static UiObject2 openAllApps() { + final UiDevice device = getDevice(); + device.waitForIdle(); + UiObject2 hotseat = device.wait( + Until.findObject(getSelectorForId(R.id.hotseat)), 2500); + Point start = hotseat.getVisibleCenter(); + int endY = (int) (device.getDisplayHeight() * 0.1f); + // 100 px/step + device.swipe(start.x, start.y, start.x, endY, (start.y - endY) / 100); + return findViewById(R.id.apps_list_view); + } + + public static UiObject2 findViewById(int id) { + return getDevice().wait(Until.findObject(getSelectorForId(id)), + AbstractLauncherUiTest.DEFAULT_UI_TIMEOUT); + } + + public static BySelector getSelectorForId(int id) { + final Context targetContext = getTargetContext(); + String name = targetContext.getResources().getResourceEntryName(id); + return By.res(targetContext.getPackageName(), name); + } + + /** + * Finds a widget provider which can fit on the home screen. + * + * @param test test suite. + * @param hasConfigureScreen if true, a provider with a config screen is returned. + */ + public static LauncherAppWidgetProviderInfo findWidgetProvider(AbstractLauncherUiTest test, + final boolean hasConfigureScreen) { + LauncherAppWidgetProviderInfo info = + test.getOnUiThread(new Callable() { + @Override + public LauncherAppWidgetProviderInfo call() throws Exception { + ComponentName cn = new ComponentName(getInstrumentation().getContext(), + hasConfigureScreen ? AppWidgetWithConfig.class + : AppWidgetNoConfig.class); + Log.d(TAG, "findWidgetProvider componentName=" + cn.flattenToString()); + return AppWidgetManagerCompat.getInstance(getTargetContext()) + .findProvider(cn, Process.myUserHandle()); + } + }); + if (info == null) { + throw new IllegalArgumentException("No valid widget provider"); + } + return info; + } + + /** + * Drags an icon to the center of homescreen. + * + * @param icon object that is either app icon or shortcut icon + */ + public static void dragToWorkspace(UiObject2 icon, boolean expectedToShowShortcuts) { + Point center = icon.getVisibleCenter(); + + // Action Down + sendPointer(MotionEvent.ACTION_DOWN, center); + + UiObject2 dragLayer = findViewById(R.id.drag_layer); + + if (expectedToShowShortcuts) { + // Make sure shortcuts show up, and then move a bit to hide them. + assertNotNull(findViewById(R.id.deep_shortcuts_container)); + + Point moveLocation = new Point(center); + int distanceToMove = + getTargetContext().getResources().getDimensionPixelSize( + R.dimen.deep_shortcuts_start_drag_threshold) + 50; + if (moveLocation.y - distanceToMove >= dragLayer.getVisibleBounds().top) { + moveLocation.y -= distanceToMove; + } else { + moveLocation.y += distanceToMove; + } + movePointer(center, moveLocation); + + assertNull(findViewById(R.id.deep_shortcuts_container)); + } + + // Wait until Remove/Delete target is visible + assertNotNull(findViewById(R.id.delete_target_text)); + + Point moveLocation = dragLayer.getVisibleCenter(); + + // Move to center + movePointer(center, moveLocation); + sendPointer(MotionEvent.ACTION_UP, center); + + // Wait until remove target is gone. + getDevice().wait(Until.gone(getSelectorForId(R.id.delete_target_text)), + AbstractLauncherUiTest.DEFAULT_UI_TIMEOUT); + } + + private static void movePointer(Point from, Point to) { + while (!from.equals(to)) { + from.x = getNextMoveValue(to.x, from.x); + from.y = getNextMoveValue(to.y, from.y); + sendPointer(MotionEvent.ACTION_MOVE, from); + } + } + + private static int getNextMoveValue(int targetValue, int oldValue) { + if (targetValue - oldValue > 10) { + return oldValue + 10; + } else if (targetValue - oldValue < -10) { + return oldValue - 10; + } else { + return targetValue; + } + } + + public static void sendPointer(int action, Point point) { + MotionEvent event = MotionEvent.obtain(SystemClock.uptimeMillis(), + SystemClock.uptimeMillis(), action, point.x, point.y, 0); + getInstrumentation().sendPointerSync(event); + event.recycle(); + } + + /** + * Opens widget tray and returns the recycler view. + */ + public static UiObject2 openWidgetsTray() { + final UiDevice device = getDevice(); + device.pressMenu(); // Enter overview mode. + device.wait(Until.findObject( + By.text(getTargetContext().getString(R.string.widget_button_text))), + AbstractLauncherUiTest.DEFAULT_UI_TIMEOUT).click(); + return findViewById(R.id.widgets_list_view); + } +} diff --git a/tests/src/com/android/launcher3/ui/WorkTabTest.java b/tests/src/com/android/launcher3/ui/WorkTabTest.java index 044b7f3382..c93c20a616 100644 --- a/tests/src/com/android/launcher3/ui/WorkTabTest.java +++ b/tests/src/com/android/launcher3/ui/WorkTabTest.java @@ -22,19 +22,14 @@ import static org.junit.Assert.assertTrue; import androidx.test.filters.LargeTest; import androidx.test.runner.AndroidJUnit4; -import com.android.launcher3.util.rule.ShellCommandRule; - import org.junit.After; import org.junit.Before; -import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @LargeTest @RunWith(AndroidJUnit4.class) public class WorkTabTest extends AbstractLauncherUiTest { - @Rule - public ShellCommandRule mDefaultLauncherRule = ShellCommandRule.setDefaultLauncher(); private int mProfileUserId; diff --git a/tests/src/com/android/launcher3/ui/widget/AddConfigWidgetTest.java b/tests/src/com/android/launcher3/ui/widget/AddConfigWidgetTest.java index 183e3908ee..80561fcc82 100644 --- a/tests/src/com/android/launcher3/ui/widget/AddConfigWidgetTest.java +++ b/tests/src/com/android/launcher3/ui/widget/AddConfigWidgetTest.java @@ -15,18 +15,20 @@ */ package com.android.launcher3.ui.widget; +import static androidx.test.InstrumentationRegistry.getInstrumentation; + import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotSame; -import static org.junit.Assert.assertTrue; import android.appwidget.AppWidgetManager; import android.content.Intent; +import android.view.View; + import androidx.test.filters.LargeTest; import androidx.test.runner.AndroidJUnit4; import androidx.test.uiautomator.By; import androidx.test.uiautomator.UiObject2; -import android.view.View; import com.android.launcher3.ItemInfo; import com.android.launcher3.LauncherAppWidgetInfo; @@ -34,12 +36,14 @@ import com.android.launcher3.LauncherAppWidgetProviderInfo; import com.android.launcher3.Workspace; import com.android.launcher3.testcomponent.WidgetConfigActivity; import com.android.launcher3.ui.AbstractLauncherUiTest; +import com.android.launcher3.ui.TestViewHelpers; import com.android.launcher3.util.Condition; import com.android.launcher3.util.Wait; import com.android.launcher3.util.rule.ShellCommandRule; import com.android.launcher3.widget.WidgetCell; import org.junit.Before; +import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @@ -51,7 +55,7 @@ import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) public class AddConfigWidgetTest extends AbstractLauncherUiTest { - @Rule public ShellCommandRule mGrantWidgetRule = ShellCommandRule.grandWidgetBind(); + @Rule public ShellCommandRule mGrantWidgetRule = ShellCommandRule.grantWidgetBind(); private LauncherAppWidgetProviderInfo mWidgetInfo; private AppWidgetManager mAppWidgetManager; @@ -62,26 +66,30 @@ public class AddConfigWidgetTest extends AbstractLauncherUiTest { @Before public void setUp() throws Exception { super.setUp(); - mWidgetInfo = findWidgetProvider(true /* hasConfigureScreen */); + mWidgetInfo = TestViewHelpers.findWidgetProvider(this, true /* hasConfigureScreen */); mAppWidgetManager = AppWidgetManager.getInstance(mTargetContext); } @Test + @Ignore public void testWidgetConfig() throws Throwable { runTest(false, true); } @Test + @Ignore public void testWidgetConfig_rotate() throws Throwable { runTest(true, true); } @Test + @Ignore public void testConfigCancelled() throws Throwable { runTest(false, false); } @Test + @Ignore public void testConfigCancelled_rotate() throws Throwable { runTest(true, false); } @@ -97,14 +105,14 @@ public class AddConfigWidgetTest extends AbstractLauncherUiTest { mActivityMonitor.startLauncher(); // Open widget tray and wait for load complete. - final UiObject2 widgetContainer = openWidgetsTray(); - assertTrue(Wait.atMost(Condition.minChildCount(widgetContainer, 2), DEFAULT_UI_TIMEOUT)); + final UiObject2 widgetContainer = TestViewHelpers.openWidgetsTray(); + Wait.atMost(null, Condition.minChildCount(widgetContainer, 2), DEFAULT_UI_TIMEOUT); // Drag widget to homescreen WidgetConfigStartupMonitor monitor = new WidgetConfigStartupMonitor(); UiObject2 widget = scrollAndFind(widgetContainer, By.clazz(WidgetCell.class) .hasDescendant(By.text(mWidgetInfo.getLabel(mTargetContext.getPackageManager())))); - dragToWorkspace(widget, false); + TestViewHelpers.dragToWorkspace(widget, false); // Widget id for which the config activity was opened mWidgetId = monitor.getWidgetId(); @@ -120,17 +128,16 @@ public class AddConfigWidgetTest extends AbstractLauncherUiTest { setResult(acceptConfig); if (acceptConfig) { - assertTrue(Wait.atMost(new WidgetSearchCondition(), DEFAULT_ACTIVITY_TIMEOUT)); + Wait.atMost(null, new WidgetSearchCondition(), DEFAULT_ACTIVITY_TIMEOUT); assertNotNull(mAppWidgetManager.getAppWidgetInfo(mWidgetId)); } else { // Verify that the widget id is deleted. - assertTrue(Wait.atMost(() -> mAppWidgetManager.getAppWidgetInfo(mWidgetId) == null, - DEFAULT_ACTIVITY_TIMEOUT)); + Wait.atMost(null, () -> mAppWidgetManager.getAppWidgetInfo(mWidgetId) == null, + DEFAULT_ACTIVITY_TIMEOUT); } } private void setResult(boolean success) { - getInstrumentation().getTargetContext().sendBroadcast( WidgetConfigActivity.getCommandIntent(WidgetConfigActivity.class, success ? "clickOK" : "clickCancel")); diff --git a/tests/src/com/android/launcher3/ui/widget/AddWidgetTest.java b/tests/src/com/android/launcher3/ui/widget/AddWidgetTest.java index d9fef8171f..7d3cf2b826 100644 --- a/tests/src/com/android/launcher3/ui/widget/AddWidgetTest.java +++ b/tests/src/com/android/launcher3/ui/widget/AddWidgetTest.java @@ -28,6 +28,7 @@ import com.android.launcher3.LauncherAppWidgetInfo; import com.android.launcher3.LauncherAppWidgetProviderInfo; import com.android.launcher3.Workspace.ItemOperator; import com.android.launcher3.ui.AbstractLauncherUiTest; +import com.android.launcher3.ui.TestViewHelpers; import com.android.launcher3.util.Condition; import com.android.launcher3.util.Wait; import com.android.launcher3.util.rule.ShellCommandRule; @@ -45,7 +46,7 @@ import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) public class AddWidgetTest extends AbstractLauncherUiTest { - @Rule public ShellCommandRule mGrantWidgetRule = ShellCommandRule.grandWidgetBind(); + @Rule public ShellCommandRule mGrantWidgetRule = ShellCommandRule.grantWidgetBind(); @Test @Ignore @@ -66,16 +67,16 @@ public class AddWidgetTest extends AbstractLauncherUiTest { mActivityMonitor.startLauncher(); final LauncherAppWidgetProviderInfo widgetInfo = - findWidgetProvider(false /* hasConfigureScreen */); + TestViewHelpers.findWidgetProvider(this, false /* hasConfigureScreen */); // Open widget tray and wait for load complete. - final UiObject2 widgetContainer = openWidgetsTray(); - assertTrue(Wait.atMost(Condition.minChildCount(widgetContainer, 2), DEFAULT_UI_TIMEOUT)); + final UiObject2 widgetContainer = TestViewHelpers.openWidgetsTray(); + Wait.atMost(null, Condition.minChildCount(widgetContainer, 2), DEFAULT_UI_TIMEOUT); // Drag widget to homescreen UiObject2 widget = scrollAndFind(widgetContainer, By.clazz(WidgetCell.class) .hasDescendant(By.text(widgetInfo.getLabel(mTargetContext.getPackageManager())))); - dragToWorkspace(widget, false); + TestViewHelpers.dragToWorkspace(widget, false); assertTrue(mActivityMonitor.itemExists(new ItemOperator() { @Override diff --git a/tests/src/com/android/launcher3/ui/widget/BindWidgetTest.java b/tests/src/com/android/launcher3/ui/widget/BindWidgetTest.java index 17fdd26922..22bc05c942 100644 --- a/tests/src/com/android/launcher3/ui/widget/BindWidgetTest.java +++ b/tests/src/com/android/launcher3/ui/widget/BindWidgetTest.java @@ -30,21 +30,17 @@ import android.content.pm.PackageInstaller.SessionParams; import android.content.pm.PackageManager; import android.database.Cursor; import android.os.Bundle; -import androidx.test.filters.LargeTest; -import androidx.test.runner.AndroidJUnit4; -import androidx.test.uiautomator.UiSelector; import com.android.launcher3.LauncherAppWidgetHost; import com.android.launcher3.LauncherAppWidgetInfo; import com.android.launcher3.LauncherAppWidgetProviderInfo; -import com.android.launcher3.LauncherModel; import com.android.launcher3.LauncherSettings; import com.android.launcher3.Workspace; import com.android.launcher3.compat.AppWidgetManagerCompat; import com.android.launcher3.compat.PackageInstallerCompat; import com.android.launcher3.ui.AbstractLauncherUiTest; +import com.android.launcher3.ui.TestViewHelpers; import com.android.launcher3.util.ContentWriter; -import com.android.launcher3.util.LooperExecutor; import com.android.launcher3.util.rule.ShellCommandRule; import com.android.launcher3.widget.LauncherAppWidgetHostView; import com.android.launcher3.widget.PendingAddWidgetInfo; @@ -59,8 +55,11 @@ import org.junit.Test; import org.junit.runner.RunWith; import java.util.Set; + +import androidx.test.filters.LargeTest; +import androidx.test.runner.AndroidJUnit4; +import androidx.test.uiautomator.UiSelector; import java.util.concurrent.Callable; -import java.util.concurrent.TimeUnit; /** * Tests for bind widget flow. @@ -71,7 +70,8 @@ import java.util.concurrent.TimeUnit; @RunWith(AndroidJUnit4.class) public class BindWidgetTest extends AbstractLauncherUiTest { - @Rule public ShellCommandRule mGrantWidgetRule = ShellCommandRule.grandWidgetBind(); + @Rule + public ShellCommandRule mGrantWidgetRule = ShellCommandRule.grantWidgetBind(); private ContentResolver mResolver; private AppWidgetManagerCompat mWidgetManager; @@ -84,6 +84,11 @@ public class BindWidgetTest extends AbstractLauncherUiTest { @Override @Before public void setUp() throws Exception { + if (com.android.launcher3.Utilities.IS_RUNNING_IN_TEST_HARNESS + && com.android.launcher3.Utilities.IS_DEBUG_DEVICE) { + android.util.Log.d("b/117332845", + android.util.Log.getStackTraceString(new Throwable())); + } super.setUp(); mResolver = mTargetContext.getContentResolver(); @@ -105,34 +110,42 @@ public class BindWidgetTest extends AbstractLauncherUiTest { } super.tearDown(); + if (com.android.launcher3.Utilities.IS_RUNNING_IN_TEST_HARNESS + && com.android.launcher3.Utilities.IS_DEBUG_DEVICE) { + android.util.Log.d("b/117332845", + android.util.Log.getStackTraceString(new Throwable())); + } } @Test public void testBindNormalWidget_withConfig() { - LauncherAppWidgetProviderInfo info = findWidgetProvider(true); + LauncherAppWidgetProviderInfo info = TestViewHelpers.findWidgetProvider(this, true); LauncherAppWidgetInfo item = createWidgetInfo(info, true); - setupAndVerifyContents(item, LauncherAppWidgetHostView.class, info.label); + setupContents(item); + verifyWidgetPresent(info); } @Test public void testBindNormalWidget_withoutConfig() { - LauncherAppWidgetProviderInfo info = findWidgetProvider(false); + LauncherAppWidgetProviderInfo info = TestViewHelpers.findWidgetProvider(this, false); LauncherAppWidgetInfo item = createWidgetInfo(info, true); - setupAndVerifyContents(item, LauncherAppWidgetHostView.class, info.label); + setupContents(item); + verifyWidgetPresent(info); } @Test @Ignore - public void testUnboundWidget_removed() throws Exception { - LauncherAppWidgetProviderInfo info = findWidgetProvider(false); + public void testUnboundWidget_removed() { + LauncherAppWidgetProviderInfo info = TestViewHelpers.findWidgetProvider(this, false); LauncherAppWidgetInfo item = createWidgetInfo(info, false); item.appWidgetId = -33; - // Since there is no widget to verify, just wait until the workspace is ready. - setupAndVerifyContents(item, Workspace.class, null); + setupContents(item); - waitUntilLoaderIdle(); + // Since there is no widget to verify, just wait until the workspace is ready. + // TODO: fix LauncherInstrumentation#LAUNCHER_PKG + mLauncher.getWorkspace(); // Item deleted from db mCursor = mResolver.query(LauncherSettings.Favorites.getContentUri(item.id), null, null, null, null, null); @@ -144,27 +157,44 @@ public class BindWidgetTest extends AbstractLauncherUiTest { @Test public void testPendingWidget_autoRestored() { + if (com.android.launcher3.Utilities.IS_RUNNING_IN_TEST_HARNESS && com.android.launcher3.Utilities.IS_DEBUG_DEVICE) { + android.util.Log.d("b/117332845", + "Test Started @ " + android.util.Log.getStackTraceString(new Throwable())); + } // A non-restored widget with no config screen gets restored automatically. - LauncherAppWidgetProviderInfo info = findWidgetProvider(false); + LauncherAppWidgetProviderInfo info = TestViewHelpers.findWidgetProvider(this, false); // Do not bind the widget LauncherAppWidgetInfo item = createWidgetInfo(info, false); item.restoreStatus = LauncherAppWidgetInfo.FLAG_ID_NOT_VALID; - setupAndVerifyContents(item, LauncherAppWidgetHostView.class, info.label); + setupContents(item); + verifyWidgetPresent(info); + + if (com.android.launcher3.Utilities.IS_RUNNING_IN_TEST_HARNESS + && com.android.launcher3.Utilities.IS_DEBUG_DEVICE) { + android.util.Log.d("b/117332845", + "Test Ended @ " + android.util.Log.getStackTraceString(new Throwable())); + } } @Test - public void testPendingWidget_withConfigScreen() throws Exception { + public void testPendingWidget_withConfigScreen() { + if (com.android.launcher3.Utilities.IS_RUNNING_IN_TEST_HARNESS + && com.android.launcher3.Utilities.IS_DEBUG_DEVICE) { + android.util.Log.d("b/117332845", + "Test Started @ " + android.util.Log.getStackTraceString(new Throwable())); + } // A non-restored widget with config screen get bound and shows a 'Click to setup' UI. - LauncherAppWidgetProviderInfo info = findWidgetProvider(true); + LauncherAppWidgetProviderInfo info = TestViewHelpers.findWidgetProvider(this, true); // Do not bind the widget LauncherAppWidgetInfo item = createWidgetInfo(info, false); item.restoreStatus = LauncherAppWidgetInfo.FLAG_ID_NOT_VALID; - setupAndVerifyContents(item, PendingAppWidgetHostView.class, null); - waitUntilLoaderIdle(); + setupContents(item); + verifyPendingWidgetPresent(); + // Item deleted from db mCursor = mResolver.query(LauncherSettings.Favorites.getContentUri(item.id), null, null, null, null, null); @@ -176,19 +206,27 @@ public class BindWidgetTest extends AbstractLauncherUiTest { assertNotNull(AppWidgetManager.getInstance(mTargetContext) .getAppWidgetInfo(mCursor.getInt(mCursor.getColumnIndex( LauncherSettings.Favorites.APPWIDGET_ID)))); + if (com.android.launcher3.Utilities.IS_RUNNING_IN_TEST_HARNESS + && com.android.launcher3.Utilities.IS_DEBUG_DEVICE) { + android.util.Log.d("b/117332845", + "Test Ended @ " + android.util.Log.getStackTraceString(new Throwable())); + } } @Test @Ignore - public void testPendingWidget_notRestored_removed() throws Exception { + public void testPendingWidget_notRestored_removed() { LauncherAppWidgetInfo item = getInvalidWidgetInfo(); item.restoreStatus = LauncherAppWidgetInfo.FLAG_ID_NOT_VALID | LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY; - setupAndVerifyContents(item, Workspace.class, null); + setupContents(item); + + // Since there is no widget to verify, just wait until the workspace is ready. + // TODO: fix LauncherInstrumentation#LAUNCHER_PKG + mLauncher.getWorkspace(); // The view does not exist assertFalse(mDevice.findObject( new UiSelector().className(PendingAppWidgetHostView.class)).exists()); - waitUntilLoaderIdle(); // Item deleted from db mCursor = mResolver.query(LauncherSettings.Favorites.getContentUri(item.id), null, null, null, null, null); @@ -196,7 +234,7 @@ public class BindWidgetTest extends AbstractLauncherUiTest { } @Test - public void testPendingWidget_notRestored_brokenInstall() throws Exception { + public void testPendingWidget_notRestored_brokenInstall() { // A widget which is was being installed once, even if its not being // installed at the moment is not removed. LauncherAppWidgetInfo item = getInvalidWidgetInfo(); @@ -204,9 +242,10 @@ public class BindWidgetTest extends AbstractLauncherUiTest { | LauncherAppWidgetInfo.FLAG_RESTORE_STARTED | LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY; - setupAndVerifyContents(item, PendingAppWidgetHostView.class, null); + setupContents(item); + verifyPendingWidgetPresent(); + // Verify item still exists in db - waitUntilLoaderIdle(); mCursor = mResolver.query(LauncherSettings.Favorites.getContentUri(item.id), null, null, null, null, null); assertEquals(1, mCursor.getCount()); @@ -231,9 +270,10 @@ public class BindWidgetTest extends AbstractLauncherUiTest { PackageInstaller installer = mTargetContext.getPackageManager().getPackageInstaller(); mSessionId = installer.createSession(params); - setupAndVerifyContents(item, PendingAppWidgetHostView.class, null); + setupContents(item); + verifyPendingWidgetPresent(); + // Verify item still exists in db - waitUntilLoaderIdle(); mCursor = mResolver.query(LauncherSettings.Favorites.getContentUri(item.id), null, null, null, null, null); assertEquals(1, mCursor.getCount()); @@ -248,12 +288,9 @@ public class BindWidgetTest extends AbstractLauncherUiTest { /** * Adds {@param item} on the homescreen on the 0th screen at 0,0, and verifies that the * widget class is displayed on the homescreen. - * @param widgetClass the View class which is displayed on the homescreen - * @param desc the content description of the view or null. */ - private void setupAndVerifyContents( - LauncherAppWidgetInfo item, Class widgetClass, String desc) { - long screenId = Workspace.FIRST_SCREEN_ID; + private void setupContents(LauncherAppWidgetInfo item) { + int screenId = Workspace.FIRST_SCREEN_ID; // Update the screen id counter for the provider. LauncherSettings.Settings.call(mResolver, LauncherSettings.Settings.METHOD_NEW_SCREEN_ID); @@ -269,7 +306,7 @@ public class BindWidgetTest extends AbstractLauncherUiTest { ContentWriter writer = new ContentWriter(mTargetContext); item.id = LauncherSettings.Settings.call( mResolver, LauncherSettings.Settings.METHOD_NEW_ITEM_ID) - .getLong(LauncherSettings.Settings.EXTRA_VALUE); + .getInt(LauncherSettings.Settings.EXTRA_VALUE); item.screenId = screenId; item.onAddToDatabase(writer); writer.put(LauncherSettings.Favorites._ID, item.id); @@ -278,12 +315,18 @@ public class BindWidgetTest extends AbstractLauncherUiTest { // Launch the home activity mActivityMonitor.startLauncher(); - // Verify UI + waitForModelLoaded(); + } + + private void verifyWidgetPresent(LauncherAppWidgetProviderInfo info) { UiSelector selector = new UiSelector().packageName(mTargetContext.getPackageName()) - .className(widgetClass); - if (desc != null) { - selector = selector.description(desc); - } + .className(LauncherAppWidgetHostView.class).description(info.label); + assertTrue(mDevice.findObject(selector).waitForExists(DEFAULT_UI_TIMEOUT)); + } + + private void verifyPendingWidgetPresent() { + UiSelector selector = new UiSelector().packageName(mTargetContext.getPackageName()) + .className(PendingAppWidgetHostView.class); assertTrue(mDevice.findObject(selector).waitForExists(DEFAULT_UI_TIMEOUT)); } @@ -332,13 +375,9 @@ public class BindWidgetTest extends AbstractLauncherUiTest { int count = 0; String pkg = invalidPackage; - Set activePackage = getOnUiThread(new Callable>() { - @Override - public Set call() throws Exception { - return PackageInstallerCompat.getInstance(mTargetContext) - .updateAndGetActiveSessionCache().keySet(); - } - }); + Set activePackage = getOnUiThread(() -> + PackageInstallerCompat.getInstance(mTargetContext) + .updateAndGetActiveSessionCache().keySet()); while(true) { try { mTargetContext.getPackageManager().getPackageInfo( @@ -362,15 +401,4 @@ public class BindWidgetTest extends AbstractLauncherUiTest { item.container = LauncherSettings.Favorites.CONTAINER_DESKTOP; return item; } - - /** - * Blocks the current thread until all the jobs in the main worker thread are complete. - */ - private void waitUntilLoaderIdle() throws Exception { - new LooperExecutor(LauncherModel.getWorkerLooper()) - .submit(new Runnable() { - @Override - public void run() { } - }).get(DEFAULT_WORKER_TIMEOUT_SECS, TimeUnit.SECONDS); - } } diff --git a/tests/src/com/android/launcher3/ui/widget/RequestPinItemTest.java b/tests/src/com/android/launcher3/ui/widget/RequestPinItemTest.java index 74f0978b44..95a128961c 100644 --- a/tests/src/com/android/launcher3/ui/widget/RequestPinItemTest.java +++ b/tests/src/com/android/launcher3/ui/widget/RequestPinItemTest.java @@ -42,6 +42,7 @@ import com.android.launcher3.testcomponent.AppWidgetNoConfig; import com.android.launcher3.testcomponent.AppWidgetWithConfig; import com.android.launcher3.testcomponent.RequestPinItemActivity; import com.android.launcher3.ui.AbstractLauncherUiTest; +import com.android.launcher3.ui.TestViewHelpers; import com.android.launcher3.util.Condition; import com.android.launcher3.util.Wait; import com.android.launcher3.util.rule.ShellCommandRule; @@ -60,10 +61,9 @@ import java.util.UUID; */ @LargeTest @RunWith(AndroidJUnit4.class) -public class RequestPinItemTest extends AbstractLauncherUiTest { +public class RequestPinItemTest extends AbstractLauncherUiTest { - @Rule public ShellCommandRule mGrantWidgetRule = ShellCommandRule.grandWidgetBind(); - @Rule public ShellCommandRule mDefaultLauncherRule = ShellCommandRule.setDefaultLauncher(); + @Rule public ShellCommandRule mGrantWidgetRule = ShellCommandRule.grantWidgetBind(); private String mCallbackAction; private String mShortcutId; @@ -152,8 +152,8 @@ public class RequestPinItemTest extends AbstractLauncherUiTest { mActivityMonitor.startLauncher(); // Open all apps and wait for load complete - final UiObject2 appsContainer = openAllApps(); - assertTrue(Wait.atMost(Condition.minChildCount(appsContainer, 2), DEFAULT_UI_TIMEOUT)); + final UiObject2 appsContainer = TestViewHelpers.openAllApps(); + Wait.atMost(null, Condition.minChildCount(appsContainer, 2), DEFAULT_UI_TIMEOUT); // Open Pin item activity BlockingBroadcastReceiver openMonitor = new BlockingBroadcastReceiver( @@ -192,7 +192,7 @@ public class RequestPinItemTest extends AbstractLauncherUiTest { // Go back to home mActivityMonitor.returnToHome(); - assertTrue(Wait.atMost(new ItemSearchCondition(itemMatcher), DEFAULT_ACTIVITY_TIMEOUT)); + Wait.atMost(null, new ItemSearchCondition(itemMatcher), DEFAULT_ACTIVITY_TIMEOUT); } /** diff --git a/tests/src/com/android/launcher3/util/IntSetTest.java b/tests/src/com/android/launcher3/util/IntSetTest.java new file mode 100644 index 0000000000..934b749f5e --- /dev/null +++ b/tests/src/com/android/launcher3/util/IntSetTest.java @@ -0,0 +1,58 @@ +/* + * 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.util; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +/** + * Unit tests for {@link IntSet} + */ +@SmallTest +@RunWith(AndroidJUnit4.class) +public class IntSetTest { + + @Test + public void testDuplicateEntries() { + IntSet set = new IntSet(); + + set.add(2); + assertEquals(1, set.size()); + + set.add(2); + assertEquals(1, set.size()); + assertTrue(set.contains(2)); + assertFalse(set.contains(1)); + + set.add(1); + assertEquals(2, set.size()); + assertTrue(set.contains(2)); + assertTrue(set.contains(1)); + + + set.add(10); + assertEquals(3, set.size()); + + assertEquals("1, 2, 10", set.mArray.toConcatString()); + } +} diff --git a/tests/src/com/android/launcher3/util/Wait.java b/tests/src/com/android/launcher3/util/Wait.java index f9e53ba8b1..0e41c0206b 100644 --- a/tests/src/com/android/launcher3/util/Wait.java +++ b/tests/src/com/android/launcher3/util/Wait.java @@ -2,6 +2,8 @@ package com.android.launcher3.util; import android.os.SystemClock; +import org.junit.Assert; + /** * A utility class for waiting for a condition to be true. */ @@ -9,16 +11,16 @@ public class Wait { private static final long DEFAULT_SLEEP_MS = 200; - public static boolean atMost(Condition condition, long timeout) { - return atMost(condition, timeout, DEFAULT_SLEEP_MS); + public static void atMost(String message, Condition condition, long timeout) { + atMost(message, condition, timeout, DEFAULT_SLEEP_MS); } - public static boolean atMost(Condition condition, long timeout, long sleepMillis) { + public static void atMost(String message, Condition condition, long timeout, long sleepMillis) { long endTime = SystemClock.uptimeMillis() + timeout; while (SystemClock.uptimeMillis() < endTime) { try { if (condition.isTrue()) { - return true; + return; } } catch (Throwable t) { // Ignore @@ -29,11 +31,11 @@ public class Wait { // Check once more before returning false. try { if (condition.isTrue()) { - return true; + return; } } catch (Throwable t) { // Ignore } - return false; + Assert.fail(message); } } diff --git a/tests/src/com/android/launcher3/util/rule/LauncherActivityRule.java b/tests/src/com/android/launcher3/util/rule/LauncherActivityRule.java index 1c7160fe51..145c3c89e3 100644 --- a/tests/src/com/android/launcher3/util/rule/LauncherActivityRule.java +++ b/tests/src/com/android/launcher3/util/rule/LauncherActivityRule.java @@ -15,10 +15,14 @@ */ package com.android.launcher3.util.rule; +import static com.android.launcher3.tapl.TestHelpers.getHomeIntentInPackage; + +import static androidx.test.InstrumentationRegistry.getInstrumentation; +import static androidx.test.InstrumentationRegistry.getTargetContext; + import android.app.Activity; import android.app.Application; import android.app.Application.ActivityLifecycleCallbacks; -import android.content.Intent; import android.os.Bundle; import androidx.test.InstrumentationRegistry; @@ -65,19 +69,12 @@ public class LauncherActivityRule implements TestRule { * Starts the launcher activity in the target package. */ public void startLauncher() { - InstrumentationRegistry.getInstrumentation().startActivitySync(getHomeIntent()); + getInstrumentation().startActivitySync(getHomeIntentInPackage(getTargetContext())); } public void returnToHome() { - InstrumentationRegistry.getTargetContext().startActivity(getHomeIntent()); - InstrumentationRegistry.getInstrumentation().waitForIdleSync(); - } - - public static Intent getHomeIntent() { - return new Intent(Intent.ACTION_MAIN) - .addCategory(Intent.CATEGORY_HOME) - .setPackage(InstrumentationRegistry.getTargetContext().getPackageName()) - .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + getTargetContext().startActivity(getHomeIntentInPackage(getTargetContext())); + getInstrumentation().waitForIdleSync(); } private class MyStatement extends Statement implements ActivityLifecycleCallbacks { diff --git a/tests/src/com/android/launcher3/util/rule/ShellCommandRule.java b/tests/src/com/android/launcher3/util/rule/ShellCommandRule.java index 9f0db2bf9e..0ec0f026d9 100644 --- a/tests/src/com/android/launcher3/util/rule/ShellCommandRule.java +++ b/tests/src/com/android/launcher3/util/rule/ShellCommandRule.java @@ -15,17 +15,20 @@ */ package com.android.launcher3.util.rule; +import static com.android.launcher3.tapl.TestHelpers.getLauncherInMyProcess; + +import static androidx.test.InstrumentationRegistry.getInstrumentation; + import android.content.ComponentName; import android.content.pm.ActivityInfo; -import android.os.ParcelFileDescriptor; -import androidx.test.InstrumentationRegistry; import org.junit.rules.TestRule; import org.junit.runner.Description; import org.junit.runners.model.Statement; -import java.io.FileInputStream; -import java.io.IOException; +import androidx.annotation.Nullable; +import androidx.test.InstrumentationRegistry; +import androidx.test.uiautomator.UiDevice; /** * Test rule which executes a shell command at the start of the test. @@ -33,58 +36,55 @@ import java.io.IOException; public class ShellCommandRule implements TestRule { private final String mCmd; + private final String mRevertCommand; - public ShellCommandRule(String cmd) { + public ShellCommandRule(String cmd, @Nullable String revertCommand) { mCmd = cmd; + mRevertCommand = revertCommand; } @Override public Statement apply(Statement base, Description description) { - return new MyStatement(base, mCmd); - } - - public static void runShellCommand(String command) throws IOException { - ParcelFileDescriptor pfd = InstrumentationRegistry.getInstrumentation().getUiAutomation() - .executeShellCommand(command); - - // Read the input stream fully. - FileInputStream fis = new ParcelFileDescriptor.AutoCloseInputStream(pfd); - while (fis.read() != -1); - fis.close(); - } - - private static class MyStatement extends Statement { - private final Statement mBase; - private final String mCmd; - - public MyStatement(Statement base, String cmd) { - mBase = base; - mCmd = cmd; - } - - @Override - public void evaluate() throws Throwable { - runShellCommand(mCmd); - mBase.evaluate(); - } + return new Statement() { + @Override + public void evaluate() throws Throwable { + UiDevice.getInstance(getInstrumentation()).executeShellCommand(mCmd); + try { + base.evaluate(); + } finally { + if (mRevertCommand != null) { + UiDevice.getInstance(getInstrumentation()).executeShellCommand(mRevertCommand); + } + } + } + }; } /** * Grants the launcher permission to bind widgets. */ - public static ShellCommandRule grandWidgetBind() { + public static ShellCommandRule grantWidgetBind() { return new ShellCommandRule("appwidget grantbind --package " - + InstrumentationRegistry.getTargetContext().getPackageName()); + + InstrumentationRegistry.getTargetContext().getPackageName(), null); } /** * Sets the target launcher as default launcher. */ public static ShellCommandRule setDefaultLauncher() { - ActivityInfo launcher = InstrumentationRegistry.getTargetContext().getPackageManager() - .queryIntentActivities(LauncherActivityRule.getHomeIntent(), 0).get(0) - .activityInfo; - return new ShellCommandRule("cmd package set-home-activity " + - new ComponentName(launcher.packageName, launcher.name).flattenToString()); + return new ShellCommandRule(getLauncherCommand(getLauncherInMyProcess()), null); + } + + public static String getLauncherCommand(ActivityInfo launcher) { + return "cmd package set-home-activity " + + new ComponentName(launcher.packageName, launcher.name).flattenToString(); + } + + /** + * Disables heads up notification for the duration of the test + */ + public static ShellCommandRule disableHeadsUpNotification() { + return new ShellCommandRule("settings put global heads_up_notifications_enabled 0", + "settings put global heads_up_notifications_enabled 1"); } } diff --git a/tests/tapl/com/android/launcher3/tapl/AppIcon.java b/tests/tapl/com/android/launcher3/tapl/AppIcon.java index a11f6df0a2..b7ae9f1497 100644 --- a/tests/tapl/com/android/launcher3/tapl/AppIcon.java +++ b/tests/tapl/com/android/launcher3/tapl/AppIcon.java @@ -39,23 +39,11 @@ public final class AppIcon { return By.clazz(TextView.class).text(appName).pkg(LauncherInstrumentation.LAUNCHER_PKG); } - /** - * Clicks the icon to launch its app. - */ - @Deprecated - public Background launch() { - LauncherInstrumentation.log("AppIcon.launch before click"); - LauncherInstrumentation.assertTrue( - "Launching an app didn't open a new window: " + mIcon.getText(), - mIcon.clickAndWait(Until.newWindow(), LauncherInstrumentation.WAIT_TIME_MS)); - return new Background(mLauncher); - } - /** * Clicks the icon to launch its app. */ public Background launch(String packageName) { - LauncherInstrumentation.log("AppIcon.launch before click"); + LauncherInstrumentation.log("AppIcon.launch before click " + mIcon.getVisibleCenter()); LauncherInstrumentation.assertTrue( "Launching an app didn't open a new window: " + mIcon.getText(), mIcon.clickAndWait(Until.newWindow(), LauncherInstrumentation.WAIT_TIME_MS)); diff --git a/tests/tapl/com/android/launcher3/tapl/Background.java b/tests/tapl/com/android/launcher3/tapl/Background.java index 1aef9791d8..27e0954241 100644 --- a/tests/tapl/com/android/launcher3/tapl/Background.java +++ b/tests/tapl/com/android/launcher3/tapl/Background.java @@ -16,10 +16,21 @@ package com.android.launcher3.tapl; +import static com.android.launcher3.tapl.LauncherInstrumentation.WAIT_TIME_MS; +import static com.android.launcher3.tapl.TestHelpers.getOverviewPackageName; + +import static org.junit.Assert.assertTrue; + +import androidx.annotation.NonNull; +import androidx.test.uiautomator.By; +import androidx.test.uiautomator.UiObject2; +import androidx.test.uiautomator.Until; + /** - * Operations on a state when Launcher is inactive because some other app is active. + * Indicates the base state with a UI other than Overview running as foreground. It can also + * indicate Launcher as long as Launcher is not in Overview state. */ -public final class Background extends Home { +public class Background extends LauncherInstrumentation.VisibleContainer { Background(LauncherInstrumentation launcher) { super(launcher); @@ -29,4 +40,33 @@ public final class Background extends Home { protected LauncherInstrumentation.ContainerType getContainerType() { return LauncherInstrumentation.ContainerType.BACKGROUND; } + + /** + * Swipes up or presses the square button to switch to Overview. + * Returns the base overview, which can be either in Launcher or the fallback recents. + * + * @return the Overview panel object. + */ + @NonNull + public BaseOverview switchToOverview() { + verifyActiveContainer(); + goToOverviewUnchecked(); + assertTrue("Overview not visible", mLauncher.getDevice().wait( + Until.hasObject(By.pkg(getOverviewPackageName())), WAIT_TIME_MS)); + return new BaseOverview(mLauncher); + } + + + protected void goToOverviewUnchecked() { + if (mLauncher.isSwipeUpEnabled()) { + final int height = mLauncher.getDevice().getDisplayHeight(); + final UiObject2 navBar = mLauncher.getSystemUiObject("navigation_bar_frame"); + + mLauncher.swipe( + navBar.getVisibleBounds().centerX(), navBar.getVisibleBounds().centerY(), + navBar.getVisibleBounds().centerX(), height - 300); + } else { + mLauncher.getSystemUiObject("recent_apps").click(); + } + } } diff --git a/tests/tapl/com/android/launcher3/tapl/BaseOverview.java b/tests/tapl/com/android/launcher3/tapl/BaseOverview.java new file mode 100644 index 0000000000..4fce211261 --- /dev/null +++ b/tests/tapl/com/android/launcher3/tapl/BaseOverview.java @@ -0,0 +1,84 @@ +/* + * 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.tapl; + +import java.util.Collections; +import java.util.List; + +import androidx.annotation.NonNull; +import androidx.test.uiautomator.Direction; +import androidx.test.uiautomator.UiObject2; + +/** + * Common overview pane for both Launcher and fallback recents + */ +public class BaseOverview extends LauncherInstrumentation.VisibleContainer { + private static final int DEFAULT_FLING_SPEED = 15000; + + BaseOverview(LauncherInstrumentation launcher) { + super(launcher); + } + + @Override + protected LauncherInstrumentation.ContainerType getContainerType() { + return LauncherInstrumentation.ContainerType.BASE_OVERVIEW; + } + + /** + * Flings forward (left) and waits the fling's end. + */ + public void flingForward() { + final UiObject2 overview = verifyActiveContainer(); + LauncherInstrumentation.log("Overview.flingForward before fling"); + overview.fling(Direction.LEFT, DEFAULT_FLING_SPEED); + mLauncher.waitForIdle(); + verifyActiveContainer(); + } + + /** + * Flings backward (right) and waits the fling's end. + */ + public void flingBackward() { + final UiObject2 overview = verifyActiveContainer(); + LauncherInstrumentation.log("Overview.flingBackward before fling"); + overview.fling(Direction.RIGHT, DEFAULT_FLING_SPEED); + mLauncher.waitForIdle(); + verifyActiveContainer(); + } + + /** + * Gets the current task in the carousel, or fails if the carousel is empty. + * + * @return the task in the middle of the visible tasks list. + */ + @NonNull + public OverviewTask getCurrentTask() { + verifyActiveContainer(); + final List taskViews = mLauncher.getDevice().findObjects( + LauncherInstrumentation.getLauncherObjectSelector("snapshot")); + LauncherInstrumentation.assertNotEquals("Unable to find a task", 0, taskViews.size()); + + // taskViews contains up to 3 task views: the 'main' (having the widest visible + // part) one in the center, and parts of its right and left siblings. Find the + // main task view by its width. + final UiObject2 widestTask = Collections.max(taskViews, + (t1, t2) -> Integer.compare(t1.getVisibleBounds().width(), + t2.getVisibleBounds().width())); + + return new OverviewTask(mLauncher, widestTask, this); + } +} \ No newline at end of file diff --git a/tests/tapl/com/android/launcher3/tapl/Home.java b/tests/tapl/com/android/launcher3/tapl/Home.java index 815a63e6f1..522ce14908 100644 --- a/tests/tapl/com/android/launcher3/tapl/Home.java +++ b/tests/tapl/com/android/launcher3/tapl/Home.java @@ -16,8 +16,6 @@ package com.android.launcher3.tapl; -import androidx.test.uiautomator.UiObject2; - import androidx.annotation.NonNull; /** @@ -28,33 +26,28 @@ import androidx.annotation.NonNull; * that essentially represents these two activity states. Any gestures (e.g., switchToOverview) that * can be performed in both of these states can be defined here. */ -public abstract class Home extends LauncherInstrumentation.VisibleContainer { +public abstract class Home extends Background { protected Home(LauncherInstrumentation launcher) { super(launcher); verifyActiveContainer(); } + @Override + protected LauncherInstrumentation.ContainerType getContainerType() { + return LauncherInstrumentation.ContainerType.WORKSPACE; + } + /** * Swipes up or presses the square button to switch to Overview. * * @return the Overview panel object. */ @NonNull + @Override public Overview switchToOverview() { verifyActiveContainer(); - if (mLauncher.isSwipeUpEnabled()) { - final int height = mLauncher.getDevice().getDisplayHeight(); - final UiObject2 navBar = mLauncher.getSystemUiObject("navigation_bar_frame"); - - mLauncher.swipe( - navBar.getVisibleBounds().centerX(), navBar.getVisibleBounds().centerY(), - navBar.getVisibleBounds().centerX(), height - 300 - ); - } else { - mLauncher.getSystemUiObject("recent_apps").click(); - } - + goToOverviewUnchecked(); return new Overview(mLauncher); } } \ No newline at end of file diff --git a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java index 7885e3cce4..31abc53c91 100644 --- a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java +++ b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java @@ -28,13 +28,6 @@ import android.util.Log; import android.view.Surface; import android.view.accessibility.AccessibilityEvent; -import androidx.annotation.NonNull; -import androidx.test.uiautomator.By; -import androidx.test.uiautomator.BySelector; -import androidx.test.uiautomator.UiDevice; -import androidx.test.uiautomator.UiObject2; -import androidx.test.uiautomator.Until; - import com.android.launcher3.TestProtocol; import com.android.quickstep.SwipeUpSetting; @@ -43,6 +36,13 @@ import org.junit.Assert; import java.lang.ref.WeakReference; import java.util.concurrent.TimeoutException; +import androidx.annotation.NonNull; +import androidx.test.uiautomator.By; +import androidx.test.uiautomator.BySelector; +import androidx.test.uiautomator.UiDevice; +import androidx.test.uiautomator.UiObject2; +import androidx.test.uiautomator.Until; + /** * The main tapl object. The only object that can be explicitly constructed by the using code. It * produces all other objects. @@ -54,7 +54,7 @@ public final class LauncherInstrumentation { // Types for launcher containers that the user is interacting with. "Background" is a // pseudo-container corresponding to inactive launcher covered by another app. enum ContainerType { - WORKSPACE, ALL_APPS, OVERVIEW, WIDGETS, BACKGROUND + WORKSPACE, ALL_APPS, OVERVIEW, WIDGETS, BACKGROUND, BASE_OVERVIEW } // Base class for launcher containers. @@ -84,7 +84,7 @@ public final class LauncherInstrumentation { private static final String OVERVIEW_RES_ID = "overview_panel"; private static final String WIDGETS_RES_ID = "widgets_list_view"; static final String LAUNCHER_PKG = "com.google.android.apps.nexuslauncher"; - static final int WAIT_TIME_MS = 60000; + public static final int WAIT_TIME_MS = 60000; private static final String SYSTEMUI_PACKAGE = "com.android.systemui"; private static WeakReference sActiveContainer = new WeakReference<>(null); @@ -108,7 +108,12 @@ public final class LauncherInstrumentation { instrumentation.getTargetContext().getContentResolver(), SWIPE_UP_SETTING_NAME, swipeUpEnabledDefault ? 1 : 0) == 1; - assertTrue("Device must run in a test harness", ActivityManager.isRunningInTestHarness()); + + // Launcher should run in test harness so that custom accessibility protocol between + // Launcher and TAPL is enabled. In-process tests enable this protocol with a direct call + // into Launcher. + assertTrue("Device must run in a test harness", + TestHelpers.isInLauncherProcess() || ActivityManager.isRunningInTestHarness()); } // Used only by TaplTests. @@ -195,8 +200,12 @@ public final class LauncherInstrumentation { } else { waitUntilGone(APPS_RES_ID); } + // Fall through + } + case BASE_OVERVIEW: { waitUntilGone(WORKSPACE_RES_ID); waitUntilGone(WIDGETS_RES_ID); + return waitForLauncherObject(OVERVIEW_RES_ID); } case BACKGROUND: { @@ -301,6 +310,17 @@ public final class LauncherInstrumentation { return new Overview(this); } + /** + * Gets the Base overview object if either Launcher is in overview state or the fallback + * overview activity is showing. Fails otherwise. + * + * @return BaseOverview object. + */ + @NonNull + public BaseOverview getBaseOverview() { + return new BaseOverview(this); + } + /** * Gets the All Apps object if the current state is showing the all apps panel opened by swiping * from workspace. Fails if the launcher is not in that state. Please don't call this method if diff --git a/tests/tapl/com/android/launcher3/tapl/Overview.java b/tests/tapl/com/android/launcher3/tapl/Overview.java index 9841274d6f..9e0c07ffe7 100644 --- a/tests/tapl/com/android/launcher3/tapl/Overview.java +++ b/tests/tapl/com/android/launcher3/tapl/Overview.java @@ -18,18 +18,15 @@ package com.android.launcher3.tapl; import android.graphics.Point; -import androidx.annotation.NonNull; -import androidx.test.uiautomator.Direction; -import androidx.test.uiautomator.UiObject2; +import com.android.launcher3.tapl.LauncherInstrumentation.ContainerType; -import java.util.Collections; -import java.util.List; +import androidx.annotation.NonNull; +import androidx.test.uiautomator.UiObject2; /** * Overview pane. */ -public final class Overview extends LauncherInstrumentation.VisibleContainer { - private static final int DEFAULT_FLING_SPEED = 15000; +public final class Overview extends BaseOverview { Overview(LauncherInstrumentation launcher) { super(launcher); @@ -37,54 +34,10 @@ public final class Overview extends LauncherInstrumentation.VisibleContainer { } @Override - protected LauncherInstrumentation.ContainerType getContainerType() { + protected ContainerType getContainerType() { return LauncherInstrumentation.ContainerType.OVERVIEW; } - /** - * Flings forward (left) and waits the fling's end. - */ - public void flingForward() { - final UiObject2 overview = verifyActiveContainer(); - LauncherInstrumentation.log("Overview.flingForward before fling"); - overview.fling(Direction.LEFT, DEFAULT_FLING_SPEED); - mLauncher.waitForIdle(); - verifyActiveContainer(); - } - - /** - * Flings backward (right) and waits the fling's end. - */ - public void flingBackward() { - final UiObject2 overview = verifyActiveContainer(); - LauncherInstrumentation.log("Overview.flingBackward before fling"); - overview.fling(Direction.RIGHT, DEFAULT_FLING_SPEED); - mLauncher.waitForIdle(); - verifyActiveContainer(); - } - - /** - * Gets the current task in the carousel, or fails if the carousel is empty. - * - * @return the task in the middle of the visible tasks list. - */ - @NonNull - public OverviewTask getCurrentTask() { - verifyActiveContainer(); - final List taskViews = mLauncher.getDevice().findObjects( - LauncherInstrumentation.getLauncherObjectSelector("snapshot")); - LauncherInstrumentation.assertNotEquals("Unable to find a task", 0, taskViews.size()); - - // taskViews contains up to 3 task views: the 'main' (having the widest visible - // part) one in the center, and parts of its right and left siblings. Find the - // main task view by its width. - final UiObject2 widestTask = Collections.max(taskViews, - (t1, t2) -> Integer.compare(t1.getVisibleBounds().width(), - t2.getVisibleBounds().width())); - - return new OverviewTask(mLauncher, widestTask, this); - } - /** * Swipes up to All Apps. * diff --git a/tests/tapl/com/android/launcher3/tapl/OverviewTask.java b/tests/tapl/com/android/launcher3/tapl/OverviewTask.java index 2b67cc0233..48686c448a 100644 --- a/tests/tapl/com/android/launcher3/tapl/OverviewTask.java +++ b/tests/tapl/com/android/launcher3/tapl/OverviewTask.java @@ -26,9 +26,9 @@ import androidx.test.uiautomator.Until; public final class OverviewTask { private final LauncherInstrumentation mLauncher; private final UiObject2 mTask; - private final Overview mOverview; + private final BaseOverview mOverview; - OverviewTask(LauncherInstrumentation launcher, UiObject2 task, Overview overview) { + OverviewTask(LauncherInstrumentation launcher, UiObject2 task, BaseOverview overview) { mLauncher = launcher; mTask = task; mOverview = overview; diff --git a/tests/tapl/com/android/launcher3/tapl/TestHelpers.java b/tests/tapl/com/android/launcher3/tapl/TestHelpers.java new file mode 100644 index 0000000000..93554d2452 --- /dev/null +++ b/tests/tapl/com/android/launcher3/tapl/TestHelpers.java @@ -0,0 +1,84 @@ +/* + * 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.tapl; + +import static androidx.test.InstrumentationRegistry.getInstrumentation; +import static androidx.test.InstrumentationRegistry.getTargetContext; + +import android.app.Instrumentation; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.content.pm.ResolveInfo; +import android.content.res.Resources; + +import java.util.List; + +public class TestHelpers { + + private static Boolean sIsInLauncherProcess; + + public static boolean isInLauncherProcess() { + if (sIsInLauncherProcess == null) { + sIsInLauncherProcess = initIsInLauncherProcess(); + } + return sIsInLauncherProcess; + } + + private static boolean initIsInLauncherProcess() { + ActivityInfo info = getLauncherInMyProcess(); + + // If we are in the same process, we can instantiate the class name. + try { + Class launcherClazz = Class.forName("com.android.launcher3.Launcher"); + return launcherClazz.isAssignableFrom(Class.forName(info.name)); + } catch (Exception e) { + return false; + } + } + + public static Intent getHomeIntentInPackage(Context context) { + return new Intent(Intent.ACTION_MAIN) + .addCategory(Intent.CATEGORY_HOME) + .setPackage(context.getPackageName()) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + } + + public static ActivityInfo getLauncherInMyProcess() { + Instrumentation instrumentation = getInstrumentation(); + if (instrumentation.getTargetContext() == null) { + return null; + } + + List launchers = getTargetContext().getPackageManager() + .queryIntentActivities(getHomeIntentInPackage(getTargetContext()), 0); + if (launchers.size() != 1) { + return null; + } + return launchers.get(0).activityInfo; + } + + public static String getOverviewPackageName() { + Resources res = Resources.getSystem(); + int id = res.getIdentifier("config_recentsComponentName", "string", "android"); + if (id != 0) { + return ComponentName.unflattenFromString(res.getString(id)).getPackageName(); + } + return "com.android.systemui"; + } +} diff --git a/tests/tapl/com/android/launcher3/tapl/Workspace.java b/tests/tapl/com/android/launcher3/tapl/Workspace.java index f5b01ee9b2..493f26ae9c 100644 --- a/tests/tapl/com/android/launcher3/tapl/Workspace.java +++ b/tests/tapl/com/android/launcher3/tapl/Workspace.java @@ -38,11 +38,6 @@ public final class Workspace extends Home { mHotseat = launcher.waitForLauncherObject("hotseat"); } - @Override - protected LauncherInstrumentation.ContainerType getContainerType() { - return LauncherInstrumentation.ContainerType.WORKSPACE; - } - /** * Swipes up to All Apps. *