Merge "Remove first icon from notification footer after it animates." into ub-launcher3-dorval
diff --git a/src/com/android/launcher3/AppWidgetsRestoredReceiver.java b/src/com/android/launcher3/AppWidgetsRestoredReceiver.java
index 7bc3692..84a8bce 100644
--- a/src/com/android/launcher3/AppWidgetsRestoredReceiver.java
+++ b/src/com/android/launcher3/AppWidgetsRestoredReceiver.java
@@ -83,7 +83,7 @@
LauncherAppState app = LauncherAppState.getInstanceNoCreate();
if (app != null) {
- app.reloadWorkspace();
+ app.getModel().forceReload();
}
asyncResult.finish();
}
diff --git a/src/com/android/launcher3/LauncherAppState.java b/src/com/android/launcher3/LauncherAppState.java
index e0e53a6..2e75579 100644
--- a/src/com/android/launcher3/LauncherAppState.java
+++ b/src/com/android/launcher3/LauncherAppState.java
@@ -134,15 +134,6 @@
PackageInstallerCompat.getInstance(mContext).onStop();
}
- /**
- * Reloads the workspace items from the DB and re-binds the workspace. This should generally
- * not be called as DB updates are automatically followed by UI update
- */
- public void reloadWorkspace() {
- mModel.resetLoadedState(false, true);
- mModel.startLoaderFromBackground();
- }
-
LauncherModel setLauncher(Launcher launcher) {
getLocalProvider(mContext).setLauncherProviderChangeListener(launcher);
mModel.initialize(launcher);
diff --git a/src/com/android/launcher3/LauncherModel.java b/src/com/android/launcher3/LauncherModel.java
index 5fd5081..590c242 100644
--- a/src/com/android/launcher3/LauncherModel.java
+++ b/src/com/android/launcher3/LauncherModel.java
@@ -71,8 +71,6 @@
import com.android.launcher3.shortcuts.ShortcutInfoCompat;
import com.android.launcher3.shortcuts.ShortcutKey;
import com.android.launcher3.util.ComponentKey;
-import com.android.launcher3.util.ContentWriter;
-import com.android.launcher3.util.ItemInfoMatcher;
import com.android.launcher3.util.ManagedProfileHeuristic;
import com.android.launcher3.util.MultiHashMap;
import com.android.launcher3.util.PackageManagerHelper;
@@ -93,6 +91,7 @@
import java.util.List;
import java.util.Map;
import java.util.Set;
+import java.util.concurrent.CancellationException;
import java.util.concurrent.Executor;
/**
@@ -123,12 +122,11 @@
}
@Thunk static final Handler sWorker = new Handler(sWorkerThread.getLooper());
- // We start off with everything not loaded. After that, we assume that
+ // Indicates whether the current model data is valid or not.
+ // We start off with everything not loaded. After that, we assume that
// our monitoring of the package manager provides all updates and we never
- // need to do a requery. These are only ever touched from the loader thread.
- private boolean mWorkspaceLoaded;
- private boolean mAllAppsLoaded;
- private boolean mDeepShortcutsLoaded;
+ // need to do a requery. This is only ever touched from the loader thread.
+ private boolean mModelLoaded;
/**
* Set of runnables to be called on the background thread after the workspace binding
@@ -148,11 +146,11 @@
private final Runnable mShortcutPermissionCheckRunnable = new Runnable() {
@Override
public void run() {
- if (mDeepShortcutsLoaded) {
+ if (mModelLoaded) {
boolean hasShortcutHostPermission =
DeepShortcutManager.getInstance(mApp.getContext()).hasHostPermission();
if (hasShortcutHostPermission != mHasShortcutHostPermission) {
- mApp.reloadWorkspace();
+ forceReload();
}
}
}
@@ -480,8 +478,16 @@
}
}
- void forceReload() {
- resetLoadedState(true, true);
+ /**
+ * Reloads the workspace items from the DB and re-binds the workspace. This should generally
+ * not be called as DB updates are automatically followed by UI update
+ */
+ public void forceReload() {
+ synchronized (mLock) {
+ // Stop any existing loaders first, so they don't set mModelLoaded to true later
+ stopLoaderLocked();
+ mModelLoaded = false;
+ }
// Do this here because if the launcher activity is running it will be restarted.
// If it's not running startLoaderFromBackground will merely tell it that it needs
@@ -489,19 +495,6 @@
startLoaderFromBackground();
}
- public void resetLoadedState(boolean resetAllAppsLoaded, boolean resetWorkspaceLoaded) {
- synchronized (mLock) {
- // Stop any existing loaders first, so they don't set mAllAppsLoaded or
- // mWorkspaceLoaded to true later
- stopLoaderLocked();
- if (resetAllAppsLoaded) mAllAppsLoaded = false;
- if (resetWorkspaceLoaded) mWorkspaceLoaded = false;
- // Always reset deep shortcuts loaded.
- // TODO: why?
- mDeepShortcutsLoaded = false;
- }
- }
-
/**
* When the launcher is in the background, it's possible for it to miss paired
* configuration changes. So whenever we trigger the loader from the background
@@ -553,9 +546,8 @@
// If there is already one running, tell it to stop.
stopLoaderLocked();
mLoaderTask = new LoaderTask(mApp.getContext(), synchronousBindPage);
- // TODO: mDeepShortcutsLoaded does not need to be true for synchronous bind.
- if (synchronousBindPage != PagedView.INVALID_RESTORE_PAGE && mAllAppsLoaded
- && mWorkspaceLoaded && mDeepShortcutsLoaded && !mIsLoaderTaskRunning) {
+ if (synchronousBindPage != PagedView.INVALID_RESTORE_PAGE
+ && mModelLoaded && !mIsLoaderTaskRunning) {
mLoaderTask.runBindSynchronousPage(synchronousBindPage);
return true;
} else {
@@ -607,28 +599,6 @@
mPageToBindFirst = pageToBindFirst;
}
- private void loadAndBindWorkspace() {
- mIsLoadingAndBindingWorkspace = true;
-
- // Load the workspace
- if (DEBUG_LOADERS) {
- Log.d(TAG, "loadAndBindWorkspace mWorkspaceLoaded=" + mWorkspaceLoaded);
- }
-
- if (!mWorkspaceLoaded) {
- loadWorkspace();
- synchronized (LoaderTask.this) {
- if (mStopped) {
- return;
- }
- mWorkspaceLoaded = true;
- }
- }
-
- // Bind the workspace
- bindWorkspace(mPageToBindFirst);
- }
-
private void waitForIdle() {
// Wait until the either we're stopped or the other threads are done.
// This way we don't start loading all apps until the workspace has settled
@@ -671,7 +641,7 @@
throw new RuntimeException("Should not call runBindSynchronousPage() without " +
"valid page index");
}
- if (!mAllAppsLoaded || !mWorkspaceLoaded) {
+ if (!mModelLoaded) {
// Ensure that we don't try and bind a specified page when the pages have not been
// loaded already (we should load everything asynchronously in that case)
throw new RuntimeException("Expecting AllApps and Workspace to be loaded");
@@ -703,6 +673,14 @@
bindDeepShortcuts();
}
+ private void verifyNotStopped() throws CancellationException {
+ synchronized (LoaderTask.this) {
+ if (mStopped) {
+ throw new CancellationException("Loader stopped");
+ }
+ }
+ }
+
public void run() {
synchronized (mLock) {
if (mStopped) {
@@ -710,41 +688,62 @@
}
mIsLoaderTaskRunning = true;
}
- // Optimize for end-user experience: if the Launcher is up and // running with the
- // All Apps interface in the foreground, load All Apps first. Otherwise, load the
- // workspace first (default).
- keep_running: {
- if (DEBUG_LOADERS) Log.d(TAG, "step 1: loading workspace");
- loadAndBindWorkspace();
- if (mStopped) {
- break keep_running;
- }
+ try {
+ if (DEBUG_LOADERS) Log.d(TAG, "step 1.1: loading workspace");
+ // Set to false in bindWorkspace()
+ mIsLoadingAndBindingWorkspace = true;
+ loadWorkspace();
+ verifyNotStopped();
+ if (DEBUG_LOADERS) Log.d(TAG, "step 1.2: bind workspace workspace");
+ bindWorkspace(mPageToBindFirst);
+
+ // Take a break
+ if (DEBUG_LOADERS) Log.d(TAG, "step 1 completed, wait for idle");
waitForIdle();
+ verifyNotStopped();
// second step
- if (DEBUG_LOADERS) Log.d(TAG, "step 2: loading all apps");
- loadAndBindAllApps();
+ if (DEBUG_LOADERS) Log.d(TAG, "step 2.1: loading all apps");
+ loadAllApps();
+ verifyNotStopped();
+ if (DEBUG_LOADERS) Log.d(TAG, "step 2.2: Update icon cache");
+ updateIconCache();
+
+ // Take a break
+ if (DEBUG_LOADERS) Log.d(TAG, "step 2 completed, wait for idle");
waitForIdle();
+ verifyNotStopped();
// third step
- if (DEBUG_LOADERS) Log.d(TAG, "step 3: loading deep shortcuts");
- loadAndBindDeepShortcuts();
- }
+ if (DEBUG_LOADERS) Log.d(TAG, "step 3.1: loading deep shortcuts");
+ loadDeepShortcuts();
- // Clear out this reference, otherwise we end up holding it until all of the
- // callback runnables are done.
- mContext = null;
+ verifyNotStopped();
+ if (DEBUG_LOADERS) Log.d(TAG, "step 3.2: bind deep shortcuts");
+ bindDeepShortcuts();
- synchronized (mLock) {
- // If we are still the last one to be scheduled, remove ourselves.
- if (mLoaderTask == this) {
- mLoaderTask = null;
+ synchronized (mLock) {
+ // Everything loaded bind the data.
+ mModelLoaded = true;
+ mHasLoaderCompletedOnce = true;
}
- mIsLoaderTaskRunning = false;
- mHasLoaderCompletedOnce = true;
+ } catch (CancellationException e) {
+ // Loader stopped, ignore
+ } finally {
+ // Clear out this reference, otherwise we end up holding it until all of the
+ // callback runnables are done.
+ mContext = null;
+
+ synchronized (mLock) {
+ // If we are still the last one to be scheduled, remove ourselves.
+ if (mLoaderTask == this) {
+ mLoaderTask = null;
+ }
+ mIsLoaderTaskRunning = false;
+ }
}
}
@@ -1605,29 +1604,6 @@
}
}
- private void loadAndBindAllApps() {
- if (DEBUG_LOADERS) {
- Log.d(TAG, "loadAndBindAllApps mAllAppsLoaded=" + mAllAppsLoaded);
- }
- if (!mAllAppsLoaded) {
- loadAllApps();
- synchronized (LoaderTask.this) {
- if (mStopped) {
- return;
- }
- }
- updateIconCache();
- synchronized (LoaderTask.this) {
- if (mStopped) {
- return;
- }
- mAllAppsLoaded = true;
- }
- } else {
- onlyBindAllApps();
- }
- }
-
private void updateIconCache() {
// Ignore packages which have a promise icon.
HashSet<String> packagesToIgnore = new HashSet<>();
@@ -1768,11 +1744,8 @@
}
}
- private void loadAndBindDeepShortcuts() {
- if (DEBUG_LOADERS) {
- Log.d(TAG, "loadAndBindDeepShortcuts mDeepShortcutsLoaded=" + mDeepShortcutsLoaded);
- }
- if (!mDeepShortcutsLoaded) {
+ private void loadDeepShortcuts() {
+ if (!mModelLoaded) {
sBgDataModel.deepShortcutMap.clear();
DeepShortcutManager shortcutManager = DeepShortcutManager.getInstance(mContext);
mHasShortcutHostPermission = shortcutManager.hasHostPermission();
@@ -1785,14 +1758,7 @@
}
}
}
- synchronized (LoaderTask.this) {
- if (mStopped) {
- return;
- }
- mDeepShortcutsLoaded = true;
- }
}
- bindDeepShortcuts();
}
}
@@ -1834,6 +1800,12 @@
}
void enqueueModelUpdateTask(BaseModelUpdateTask task) {
+ if (!mModelLoaded && mLoaderTask == null) {
+ if (DEBUG_LOADERS) {
+ Log.d(TAG, "enqueueModelUpdateTask Ignoring task since loader is pending=" + task);
+ }
+ return;
+ }
task.init(this);
runOnWorkerThread(task);
}
diff --git a/src/com/android/launcher3/LauncherProvider.java b/src/com/android/launcher3/LauncherProvider.java
index e71ef2c..ad9a13e 100644
--- a/src/com/android/launcher3/LauncherProvider.java
+++ b/src/com/android/launcher3/LauncherProvider.java
@@ -174,7 +174,7 @@
if (Utilities.ATLEAST_MARSHMALLOW && Binder.getCallingPid() != Process.myPid()) {
LauncherAppState app = LauncherAppState.getInstanceNoCreate();
if (app != null) {
- app.reloadWorkspace();
+ app.getModel().forceReload();
}
}
}
@@ -205,7 +205,7 @@
// Deprecated behavior to support legacy devices which rely on provider callbacks.
LauncherAppState app = LauncherAppState.getInstanceNoCreate();
if (app != null && "true".equals(uri.getQueryParameter("isExternalAdd"))) {
- app.reloadWorkspace();
+ app.getModel().forceReload();
}
String notify = uri.getQueryParameter("notify");
diff --git a/tests/AndroidManifest-common.xml b/tests/AndroidManifest-common.xml
index 24882aa..0a29147 100644
--- a/tests/AndroidManifest-common.xml
+++ b/tests/AndroidManifest-common.xml
@@ -23,7 +23,9 @@
<application android:debuggable="true">
<uses-library android:name="android.test.runner" />
- <receiver android:name="com.android.launcher3.testcomponent.AppWidgetNoConfig">
+ <receiver
+ android:name="com.android.launcher3.testcomponent.AppWidgetNoConfig"
+ android:label="No Config">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
@@ -31,7 +33,9 @@
android:resource="@xml/appwidget_no_config" />
</receiver>
- <receiver android:name="com.android.launcher3.testcomponent.AppWidgetWithConfig">
+ <receiver
+ android:name="com.android.launcher3.testcomponent.AppWidgetWithConfig"
+ android:label="With Config">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
@@ -45,5 +49,15 @@
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE"/>
</intent-filter>
</activity>
+ <activity
+ android:name="com.android.launcher3.testcomponent.RequestPinItemActivity"
+ android:label="Test Pin Item"
+ android:icon="@drawable/test_drawable_pin_item">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN"/>
+ <category android:name="android.intent.category.LAUNCHER"/>
+ <category android:name="android.intent.category.DEFAULT"/>
+ </intent-filter>
+ </activity>
</application>
</manifest>
diff --git a/tests/res/drawable/test_drawable_pin_item.xml b/tests/res/drawable/test_drawable_pin_item.xml
new file mode 100644
index 0000000..1d07256
--- /dev/null
+++ b/tests/res/drawable/test_drawable_pin_item.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+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.
+-->
+<vector
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="48dp"
+ android:height="48dp"
+ android:viewportWidth="48.0"
+ android:viewportHeight="48.0">
+
+ <path
+ android:fillColor="#66000000"
+ android:pathData="M0,24a24,24 0 1,0 48,0a24,24 0 1,0 -48,0"/>
+ <path
+ android:fillColor="#FF1B5E20"
+ android:pathData="M2,24a22,22 0 1,0 44,0a22,22 0 1,0 -44,0"/>
+
+ <group
+ android:translateX="12"
+ android:translateY="12">
+ <path
+ android:fillColor="#FFFFFFFF"
+ android:pathData="M16,12V4H17V2H7V4H8V12L6,14V16H11.2V22H12.8V16H18V14L16,12Z"/>
+ </group>
+</vector>
\ No newline at end of file
diff --git a/tests/src/com/android/launcher3/testcomponent/BaseTestingActivity.java b/tests/src/com/android/launcher3/testcomponent/BaseTestingActivity.java
new file mode 100644
index 0000000..904590c
--- /dev/null
+++ b/tests/src/com/android/launcher3/testcomponent/BaseTestingActivity.java
@@ -0,0 +1,126 @@
+/*
+ * 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.testcomponent;
+
+import android.app.Activity;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Bundle;
+import android.util.TypedValue;
+import android.view.View;
+import android.widget.Button;
+import android.widget.LinearLayout;
+import android.widget.LinearLayout.LayoutParams;
+
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+
+/**
+ * Base activity with utility methods to help automate testing.
+ */
+public class BaseTestingActivity extends Activity implements View.OnClickListener {
+
+ public static final String SUFFIX_COMMAND = "-command";
+ public static final String EXTRA_METHOD = "method";
+ public static final String EXTRA_PARAM = "param_";
+
+ private static final int MARGIN_DP = 20;
+
+ private final String mAction = this.getClass().getName();
+
+ private LinearLayout mView;
+ private int mMargin;
+
+ private final BroadcastReceiver mCommandReceiver = new BroadcastReceiver() {
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ handleCommand(intent);
+ }
+ };
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ mMargin = Math.round(TypedValue.applyDimension(
+ TypedValue.COMPLEX_UNIT_DIP, MARGIN_DP, getResources().getDisplayMetrics()));
+ mView = new LinearLayout(this);
+ mView.setPadding(mMargin, mMargin, mMargin, mMargin);
+ mView.setOrientation(LinearLayout.VERTICAL);
+ setContentView(mView);
+
+ registerReceiver(mCommandReceiver, new IntentFilter(mAction + SUFFIX_COMMAND));
+ }
+
+ protected void addButton(String title, String method) {
+ Button button = new Button(this);
+ button.setText(title);
+ button.setTag(method);
+ button.setOnClickListener(this);
+
+ LayoutParams lp = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
+ lp.bottomMargin = mMargin;
+ mView.addView(button, lp);
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ sendBroadcast(new Intent(mAction).putExtra(Intent.EXTRA_INTENT, getIntent()));
+ }
+
+ @Override
+ protected void onDestroy() {
+ unregisterReceiver(mCommandReceiver);
+ super.onDestroy();
+ }
+
+ @Override
+ public void onClick(View view) {
+ handleCommand(new Intent().putExtra(EXTRA_METHOD, (String) view.getTag()));
+ }
+
+ private void handleCommand(Intent cmd) {
+ String methodName = cmd.getStringExtra(EXTRA_METHOD);
+ try {
+ Method method = null;
+ for (Method m : this.getClass().getDeclaredMethods()) {
+ if (methodName.equals(m.getName()) &&
+ !Modifier.isStatic(m.getModifiers()) &&
+ Modifier.isPublic(m.getModifiers())) {
+ method = m;
+ break;
+ }
+ }
+ Object[] args = new Object[method.getParameterTypes().length];
+ Bundle extras = cmd.getExtras();
+ for (int i = 0; i < args.length; i++) {
+ args[i] = extras.get(EXTRA_PARAM + i);
+ }
+ method.invoke(this, args);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public static Intent getCommandIntent(Class<?> clazz, String method) {
+ return new Intent(clazz.getName() + SUFFIX_COMMAND)
+ .putExtra(EXTRA_METHOD, method);
+ }
+}
diff --git a/tests/src/com/android/launcher3/testcomponent/RequestPinItemActivity.java b/tests/src/com/android/launcher3/testcomponent/RequestPinItemActivity.java
new file mode 100644
index 0000000..c2dd225
--- /dev/null
+++ b/tests/src/com/android/launcher3/testcomponent/RequestPinItemActivity.java
@@ -0,0 +1,89 @@
+/*
+ * 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.testcomponent;
+
+import android.annotation.TargetApi;
+import android.app.PendingIntent;
+import android.appwidget.AppWidgetManager;
+import android.content.ComponentName;
+import android.content.IntentSender;
+import android.content.pm.ShortcutInfo;
+import android.content.pm.ShortcutManager;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.drawable.Icon;
+import android.os.Bundle;
+
+/**
+ * Sample activity to request pinning an item.
+ */
+@TargetApi(26)
+public class RequestPinItemActivity extends BaseTestingActivity {
+
+ private PendingIntent mCallback = null;
+ private String mShortcutId;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ addButton("Pin Shortcut", "pinShortcut");
+ addButton("Pin Widget without config ", "pinWidgetNoConfig");
+ addButton("Pin Widget with config", "pinWidgetWithConfig");
+ }
+
+ public void setCallback(PendingIntent callback) {
+ mCallback = callback;
+ }
+
+ public void setShortcutId(String id) {
+ mShortcutId = id;
+ }
+
+ public void pinShortcut() {
+ ShortcutManager sm = getSystemService(ShortcutManager.class);
+
+ // Generate icon
+ int r = sm.getIconMaxWidth() / 2;
+ Bitmap icon = Bitmap.createBitmap(r * 2, r * 2, Bitmap.Config.ARGB_8888);
+ Paint p = new Paint();
+ p.setColor(Color.RED);
+ new Canvas(icon).drawCircle(r, r, r, p);
+
+ ShortcutInfo info = new ShortcutInfo.Builder(this, mShortcutId)
+ .setIntent(getPackageManager().getLaunchIntentForPackage(getPackageName()))
+ .setIcon(Icon.createWithBitmap(icon))
+ .setShortLabel("Test shortcut")
+ .build();
+
+ IntentSender callback = mCallback == null ? null : mCallback.getIntentSender();
+ sm.requestPinShortcut(info, callback);
+ }
+
+ public void pinWidgetNoConfig() {
+ requestWidget(new ComponentName(this, AppWidgetNoConfig.class));
+ }
+
+ public void pinWidgetWithConfig() {
+ requestWidget(new ComponentName(this, AppWidgetWithConfig.class));
+ }
+
+ private void requestWidget(ComponentName cn) {
+ AppWidgetManager.getInstance(this).requestPinAppWidget(cn, mCallback);
+ }
+}
diff --git a/tests/src/com/android/launcher3/testcomponent/WidgetConfigActivity.java b/tests/src/com/android/launcher3/testcomponent/WidgetConfigActivity.java
index c0509bc..d76ad04 100644
--- a/tests/src/com/android/launcher3/testcomponent/WidgetConfigActivity.java
+++ b/tests/src/com/android/launcher3/testcomponent/WidgetConfigActivity.java
@@ -15,50 +15,30 @@
*/
package com.android.launcher3.testcomponent;
-import android.app.Activity;
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
import android.os.Bundle;
/**
* Simple activity for widget configuration
*/
-public class WidgetConfigActivity extends Activity {
+public class WidgetConfigActivity extends BaseTestingActivity {
public static final String SUFFIX_FINISH = "-finish";
public static final String EXTRA_CODE = "code";
- public static final String EXTRA_INTENT = "intent";
-
- private final BroadcastReceiver mFinishReceiver = new BroadcastReceiver() {
-
- @Override
- public void onReceive(Context context, Intent intent) {
- WidgetConfigActivity.this.setResult(
- intent.getIntExtra(EXTRA_CODE, RESULT_CANCELED),
- (Intent) intent.getParcelableExtra(EXTRA_INTENT));
- WidgetConfigActivity.this.finish();
- }
- };
-
- private final String mAction = this.getClass().getName();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
- registerReceiver(mFinishReceiver, new IntentFilter(mAction + SUFFIX_FINISH));
+ addButton("Cancel", "clickCancel");
+ addButton("OK", "clickOK");
}
- @Override
- protected void onResume() {
- super.onResume();
- sendBroadcast(new Intent(mAction).putExtra(Intent.EXTRA_INTENT, getIntent()));
+ public void clickCancel() {
+ setResult(RESULT_CANCELED);
+ finish();
}
- @Override
- protected void onDestroy() {
- unregisterReceiver(mFinishReceiver);
- super.onDestroy();
+ public void clickOK() {
+ setResult(RESULT_OK);
+ finish();
}
}
diff --git a/tests/src/com/android/launcher3/ui/AllAppsIconToHomeTest.java b/tests/src/com/android/launcher3/ui/AllAppsIconToHomeTest.java
index 3f77bfd..9361750 100644
--- a/tests/src/com/android/launcher3/ui/AllAppsIconToHomeTest.java
+++ b/tests/src/com/android/launcher3/ui/AllAppsIconToHomeTest.java
@@ -22,6 +22,7 @@
@Override
protected void setUp() throws Exception {
super.setUp();
+ setDefaultLauncher();
mSettingsApp = LauncherAppsCompat.getInstance(mTargetContext)
.getActivityList("com.android.settings", Process.myUserHandle()).get(0);
diff --git a/tests/src/com/android/launcher3/ui/LauncherInstrumentationTestCase.java b/tests/src/com/android/launcher3/ui/LauncherInstrumentationTestCase.java
index 4bc40c6..47b43f5 100644
--- a/tests/src/com/android/launcher3/ui/LauncherInstrumentationTestCase.java
+++ b/tests/src/com/android/launcher3/ui/LauncherInstrumentationTestCase.java
@@ -15,10 +15,14 @@
*/
package com.android.launcher3.ui;
+import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
import android.graphics.Point;
import android.os.ParcelFileDescriptor;
import android.os.Process;
@@ -46,16 +50,24 @@
import com.android.launcher3.testcomponent.AppWidgetWithConfig;
import com.android.launcher3.util.ManagedProfileHeuristic;
+import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.ArrayList;
import java.util.Locale;
import java.util.concurrent.Callable;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
/**
* Base class for all instrumentation tests providing various utility methods.
*/
public class LauncherInstrumentationTestCase extends InstrumentationTestCase {
+ public static final long DEFAULT_ACTIVITY_TIMEOUT = TimeUnit.SECONDS.toMillis(10);
+ public static final long DEFAULT_BROADCAST_TIMEOUT_SECS = 5;
+
public static final long DEFAULT_UI_TIMEOUT = 3000;
public static final long DEFAULT_WORKER_TIMEOUT_SECS = 5;
@@ -89,11 +101,14 @@
* Starts the launcher activity in the target package and returns the Launcher instance.
*/
protected Launcher startLauncher() {
- Intent homeIntent = new Intent(Intent.ACTION_MAIN)
+ return (Launcher) getInstrumentation().startActivitySync(getHomeIntent());
+ }
+
+ protected Intent getHomeIntent() {
+ return new Intent(Intent.ACTION_MAIN)
.addCategory(Intent.CATEGORY_HOME)
.setPackage(mTargetPackage)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- return (Launcher) getInstrumentation().startActivitySync(homeIntent);
}
/**
@@ -104,16 +119,31 @@
if (mTargetContext.getPackageManager().checkPermission(
mTargetPackage, android.Manifest.permission.BIND_APPWIDGET)
!= PackageManager.PERMISSION_GRANTED) {
- ParcelFileDescriptor pfd = getInstrumentation().getUiAutomation().executeShellCommand(
- "appwidget grantbind --package " + mTargetPackage);
- // Read the input stream fully.
- FileInputStream fis = new ParcelFileDescriptor.AutoCloseInputStream(pfd);
- while (fis.read() != -1);
- fis.close();
+ runShellCommand("appwidget grantbind --package " + mTargetPackage);
}
}
/**
+ * Sets the target launcher as default launcher.
+ */
+ protected void setDefaultLauncher() throws IOException {
+ ActivityInfo launcher = mTargetContext.getPackageManager()
+ .queryIntentActivities(getHomeIntent(), 0).get(0).activityInfo;
+ runShellCommand("cmd package set-home-activity " +
+ new ComponentName(launcher.packageName, launcher.name).flattenToString());
+ }
+
+ protected void runShellCommand(String command) throws IOException {
+ ParcelFileDescriptor pfd = getInstrumentation().getUiAutomation()
+ .executeShellCommand(command);
+
+ // Read the input stream fully.
+ FileInputStream fis = new ParcelFileDescriptor.AutoCloseInputStream(pfd);
+ while (fis.read() != -1);
+ fis.close();
+ }
+
+ /**
* Opens all apps and returns the recycler view
*/
protected UiObject2 openAllApps() {
@@ -236,8 +266,7 @@
@Override
public void run() {
ManagedProfileHeuristic.markExistingUsersForNoFolderCreation(mTargetContext);
- LauncherAppState.getInstance(mTargetContext).getModel()
- .resetLoadedState(true, true);
+ LauncherAppState.getInstance(mTargetContext).getModel().forceReload();
}
});
} catch (Throwable t) {
@@ -285,4 +314,35 @@
String name = mTargetContext.getResources().getResourceEntryName(id);
return By.res(mTargetPackage, name);
}
+
+
+ /**
+ * Broadcast receiver which blocks until the result is received.
+ */
+ public class BlockingBroadcastReceiver extends BroadcastReceiver {
+
+ private final CountDownLatch latch = new CountDownLatch(1);
+ private Intent mIntent;
+
+ public BlockingBroadcastReceiver(String action) {
+ mTargetContext.registerReceiver(this, new IntentFilter(action));
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ mIntent = intent;
+ latch.countDown();
+ }
+
+ public Intent blockingGetIntent() throws InterruptedException {
+ latch.await(DEFAULT_BROADCAST_TIMEOUT_SECS, TimeUnit.SECONDS);
+ mTargetContext.unregisterReceiver(this);
+ return mIntent;
+ }
+
+ public Intent blockingGetExtraIntent() throws InterruptedException {
+ Intent intent = blockingGetIntent();
+ return intent == null ? null : (Intent) intent.getParcelableExtra(Intent.EXTRA_INTENT);
+ }
+ }
}
diff --git a/tests/src/com/android/launcher3/ui/ShortcutsLaunchTest.java b/tests/src/com/android/launcher3/ui/ShortcutsLaunchTest.java
index c6828f0..ee3a628 100644
--- a/tests/src/com/android/launcher3/ui/ShortcutsLaunchTest.java
+++ b/tests/src/com/android/launcher3/ui/ShortcutsLaunchTest.java
@@ -25,6 +25,7 @@
@Override
protected void setUp() throws Exception {
super.setUp();
+ setDefaultLauncher();
mSettingsApp = LauncherAppsCompat.getInstance(mTargetContext)
.getActivityList("com.android.settings", Process.myUserHandle()).get(0);
diff --git a/tests/src/com/android/launcher3/ui/ShortcutsToHomeTest.java b/tests/src/com/android/launcher3/ui/ShortcutsToHomeTest.java
index d573eea..061e865 100644
--- a/tests/src/com/android/launcher3/ui/ShortcutsToHomeTest.java
+++ b/tests/src/com/android/launcher3/ui/ShortcutsToHomeTest.java
@@ -25,6 +25,7 @@
@Override
protected void setUp() throws Exception {
super.setUp();
+ setDefaultLauncher();
mSettingsApp = LauncherAppsCompat.getInstance(mTargetContext)
.getActivityList("com.android.settings", Process.myUserHandle()).get(0);
diff --git a/tests/src/com/android/launcher3/ui/widget/AddConfigWidgetTest.java b/tests/src/com/android/launcher3/ui/widget/AddConfigWidgetTest.java
index 7cbd292..0b4e34f 100644
--- a/tests/src/com/android/launcher3/ui/widget/AddConfigWidgetTest.java
+++ b/tests/src/com/android/launcher3/ui/widget/AddConfigWidgetTest.java
@@ -18,10 +18,7 @@
import android.app.Activity;
import android.app.Application;
import android.appwidget.AppWidgetManager;
-import android.content.BroadcastReceiver;
-import android.content.Context;
import android.content.Intent;
-import android.content.IntentFilter;
import android.support.test.uiautomator.By;
import android.support.test.uiautomator.UiObject2;
import android.test.suitebuilder.annotation.LargeTest;
@@ -41,8 +38,6 @@
import com.android.launcher3.widget.WidgetCell;
import java.util.concurrent.Callable;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
/**
* Test to verify widget configuration is properly shown.
@@ -50,9 +45,6 @@
@LargeTest
public class AddConfigWidgetTest extends LauncherInstrumentationTestCase {
- public static final long DEFAULT_ACTIVITY_TIMEOUT = TimeUnit.SECONDS.toMillis(10);
- public static final long DEFAULT_BROADCAST_TIMEOUT_SECS = 5;
-
private LauncherAppWidgetProviderInfo mWidgetInfo;
private SimpleActivityMonitor mActivityMonitor;
private MainThreadExecutor mMainThreadExecutor;
@@ -69,6 +61,8 @@
.registerActivityLifecycleCallbacks(mActivityMonitor);
mMainThreadExecutor = new MainThreadExecutor();
mAppWidgetManager = AppWidgetManager.getInstance(mTargetContext);
+
+ grantWidgetPermission();
}
@Override
@@ -126,12 +120,11 @@
// Verify that the widget id is valid and bound
assertNotNull(mAppWidgetManager.getAppWidgetInfo(mWidgetId));
+ setResult(acceptConfig);
if (acceptConfig) {
- setResult(Activity.RESULT_OK);
assertTrue(Wait.atMost(new WidgetSearchCondition(), DEFAULT_ACTIVITY_TIMEOUT));
assertNotNull(mAppWidgetManager.getAppWidgetInfo(mWidgetId));
} else {
- setResult(Activity.RESULT_CANCELED);
// Verify that the widget id is deleted.
assertTrue(Wait.atMost(new Condition() {
@Override
@@ -142,10 +135,11 @@
}
}
- private void setResult(int resultCode) {
- String action = WidgetConfigActivity.class.getName() + WidgetConfigActivity.SUFFIX_FINISH;
+ private void setResult(boolean success) {
+
getInstrumentation().getTargetContext().sendBroadcast(
- new Intent(action).putExtra(WidgetConfigActivity.EXTRA_CODE, resultCode));
+ WidgetConfigActivity.getCommandIntent(WidgetConfigActivity.class,
+ success ? "clickOK" : "clickCancel"));
}
/**
@@ -185,28 +179,17 @@
/**
* Broadcast receiver for receiving widget config activity status.
*/
- private class WidgetConfigStartupMonitor extends BroadcastReceiver {
+ private class WidgetConfigStartupMonitor extends BlockingBroadcastReceiver {
- private final CountDownLatch latch = new CountDownLatch(1);
- private Intent mIntent;
-
- WidgetConfigStartupMonitor() {
- getInstrumentation().getTargetContext().registerReceiver(this,
- new IntentFilter(WidgetConfigActivity.class.getName()));
- }
-
- @Override
- public void onReceive(Context context, Intent intent) {
- mIntent = intent.getParcelableExtra(Intent.EXTRA_INTENT);
- latch.countDown();
+ public WidgetConfigStartupMonitor() {
+ super(WidgetConfigActivity.class.getName());
}
public int getWidgetId() throws InterruptedException {
- latch.await(DEFAULT_BROADCAST_TIMEOUT_SECS, TimeUnit.SECONDS);
- getInstrumentation().getTargetContext().unregisterReceiver(this);
- assertNotNull(mIntent);
- assertEquals(AppWidgetManager.ACTION_APPWIDGET_CONFIGURE, mIntent.getAction());
- int widgetId = mIntent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID,
+ Intent intent = blockingGetExtraIntent();
+ assertNotNull(intent);
+ assertEquals(AppWidgetManager.ACTION_APPWIDGET_CONFIGURE, intent.getAction());
+ int widgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID,
LauncherAppWidgetInfo.NO_ID);
assertNotSame(widgetId, LauncherAppWidgetInfo.NO_ID);
return widgetId;
diff --git a/tests/src/com/android/launcher3/ui/widget/AddWidgetTest.java b/tests/src/com/android/launcher3/ui/widget/AddWidgetTest.java
index b7e1ca9..3c92c57 100644
--- a/tests/src/com/android/launcher3/ui/widget/AddWidgetTest.java
+++ b/tests/src/com/android/launcher3/ui/widget/AddWidgetTest.java
@@ -41,6 +41,7 @@
@Override
protected void setUp() throws Exception {
super.setUp();
+ grantWidgetPermission();
widgetInfo = findWidgetProvider(false /* hasConfigureScreen */);
}
diff --git a/tests/src/com/android/launcher3/ui/widget/RequestPinItemTest.java b/tests/src/com/android/launcher3/ui/widget/RequestPinItemTest.java
new file mode 100644
index 0000000..5ef5ec1
--- /dev/null
+++ b/tests/src/com/android/launcher3/ui/widget/RequestPinItemTest.java
@@ -0,0 +1,210 @@
+/*
+ * 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.widget;
+
+import android.app.Activity;
+import android.app.Application;
+import android.app.PendingIntent;
+import android.appwidget.AppWidgetManager;
+import android.content.Intent;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.UiObject2;
+import android.support.test.uiautomator.Until;
+import android.test.suitebuilder.annotation.LargeTest;
+import android.view.View;
+
+import com.android.launcher3.ItemInfo;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherAppWidgetInfo;
+import com.android.launcher3.LauncherSettings.Favorites;
+import com.android.launcher3.MainThreadExecutor;
+import com.android.launcher3.R;
+import com.android.launcher3.ShortcutInfo;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.Workspace.ItemOperator;
+import com.android.launcher3.shortcuts.ShortcutKey;
+import com.android.launcher3.testcomponent.AppWidgetNoConfig;
+import com.android.launcher3.testcomponent.AppWidgetWithConfig;
+import com.android.launcher3.testcomponent.RequestPinItemActivity;
+import com.android.launcher3.ui.LauncherInstrumentationTestCase;
+import com.android.launcher3.util.Condition;
+import com.android.launcher3.util.SimpleActivityMonitor;
+import com.android.launcher3.util.Wait;
+import com.android.launcher3.widget.WidgetCell;
+
+import java.util.UUID;
+import java.util.concurrent.Callable;
+
+/**
+ * Test to verify pin item request flow.
+ */
+@LargeTest
+public class RequestPinItemTest extends LauncherInstrumentationTestCase {
+
+ private SimpleActivityMonitor mActivityMonitor;
+ private MainThreadExecutor mMainThreadExecutor;
+
+ private String mCallbackAction;
+ private String mShortcutId;
+ private int mAppWidgetId;
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ grantWidgetPermission();
+ setDefaultLauncher();
+
+ mActivityMonitor = new SimpleActivityMonitor();
+ ((Application) getInstrumentation().getTargetContext().getApplicationContext())
+ .registerActivityLifecycleCallbacks(mActivityMonitor);
+ mMainThreadExecutor = new MainThreadExecutor();
+
+ mCallbackAction = UUID.randomUUID().toString();
+ mShortcutId = UUID.randomUUID().toString();
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ ((Application) getInstrumentation().getTargetContext().getApplicationContext())
+ .unregisterActivityLifecycleCallbacks(mActivityMonitor);
+ super.tearDown();
+ }
+
+ public void testPinWidgetNoConfig() throws Throwable {
+ runTest("pinWidgetNoConfig", true, new ItemOperator() {
+ @Override
+ public boolean evaluate(ItemInfo info, View view) {
+ return info instanceof LauncherAppWidgetInfo &&
+ ((LauncherAppWidgetInfo) info).appWidgetId == mAppWidgetId &&
+ ((LauncherAppWidgetInfo) info).providerName.getClassName()
+ .equals(AppWidgetNoConfig.class.getName());
+ }
+ });
+ }
+
+ public void testPinWidgetWithConfig() throws Throwable {
+ runTest("pinWidgetWithConfig", true, new ItemOperator() {
+ @Override
+ public boolean evaluate(ItemInfo info, View view) {
+ return info instanceof LauncherAppWidgetInfo &&
+ ((LauncherAppWidgetInfo) info).appWidgetId == mAppWidgetId &&
+ ((LauncherAppWidgetInfo) info).providerName.getClassName()
+ .equals(AppWidgetWithConfig.class.getName());
+ }
+ });
+ }
+
+
+ public void testPinWidgetShortcut() throws Throwable {
+ runTest("pinShortcut", false, new ItemOperator() {
+ @Override
+ public boolean evaluate(ItemInfo info, View view) {
+ return info instanceof ShortcutInfo &&
+ info.itemType == Favorites.ITEM_TYPE_DEEP_SHORTCUT &&
+ ShortcutKey.fromItemInfo(info).getId().equals(mShortcutId);
+ }
+ });
+ }
+
+ private void runTest(String activityMethod, boolean isWidget, ItemOperator itemMatcher)
+ throws Throwable {
+ if (!Utilities.isAtLeastO()) {
+ return;
+ }
+ lockRotation(true);
+
+ clearHomescreen();
+ startLauncher();
+
+ // Open all apps and wait for load complete
+ final UiObject2 appsContainer = openAllApps();
+ assertTrue(Wait.atMost(Condition.minChildCount(appsContainer, 2), DEFAULT_UI_TIMEOUT));
+
+ // Open Pin item activity
+ BlockingBroadcastReceiver openMonitor = new BlockingBroadcastReceiver(
+ RequestPinItemActivity.class.getName());
+ scrollAndFind(appsContainer, By.text("Test Pin Item")).click();
+ assertNotNull(openMonitor.blockingGetExtraIntent());
+
+ // Set callback
+ PendingIntent callback = PendingIntent.getBroadcast(mTargetContext, 0,
+ new Intent(mCallbackAction), PendingIntent.FLAG_ONE_SHOT);
+ mTargetContext.sendBroadcast(RequestPinItemActivity.getCommandIntent(
+ RequestPinItemActivity.class, "setCallback").putExtra(
+ RequestPinItemActivity.EXTRA_PARAM + "0", callback));
+
+ if (!isWidget) {
+ // Set shortcut id
+ mTargetContext.sendBroadcast(RequestPinItemActivity.getCommandIntent(
+ RequestPinItemActivity.class, "setShortcutId").putExtra(
+ RequestPinItemActivity.EXTRA_PARAM + "0", mShortcutId));
+ }
+
+ // call the requested method to start the flow
+ mTargetContext.sendBroadcast(RequestPinItemActivity.getCommandIntent(
+ RequestPinItemActivity.class, activityMethod));
+ UiObject2 widgetCell = mDevice.wait(
+ Until.findObject(By.clazz(WidgetCell.class)), DEFAULT_ACTIVITY_TIMEOUT);
+ assertNotNull(widgetCell);
+
+ // Accept confirmation:
+ BlockingBroadcastReceiver resultReceiver = new BlockingBroadcastReceiver(mCallbackAction);
+ mDevice.wait(Until.findObject(By.text(mTargetContext.getString(
+ R.string.place_automatically).toUpperCase())), DEFAULT_UI_TIMEOUT).click();
+ Intent result = resultReceiver.blockingGetIntent();
+ assertNotNull(result);
+ mAppWidgetId = result.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1);
+ if (isWidget) {
+ assertNotSame(-1, mAppWidgetId);
+ }
+
+ // Go back to home
+ mTargetContext.startActivity(getHomeIntent());
+ assertTrue(Wait.atMost(new ItemSearchCondition(itemMatcher), DEFAULT_ACTIVITY_TIMEOUT));
+ }
+
+ /**
+ * Condition for for an item
+ */
+ private class ItemSearchCondition extends Condition implements Callable<Boolean> {
+
+ private final ItemOperator mOp;
+
+ ItemSearchCondition(ItemOperator op) {
+ mOp = op;
+ }
+
+ @Override
+ public boolean isTrue() throws Throwable {
+ return mMainThreadExecutor.submit(this).get();
+ }
+
+ @Override
+ public Boolean call() throws Exception {
+ // Find the resumed launcher
+ Launcher launcher = null;
+ for (Activity a : mActivityMonitor.resumed) {
+ if (a instanceof Launcher) {
+ launcher = (Launcher) a;
+ }
+ }
+ if (launcher == null) {
+ return false;
+ }
+ return launcher.getWorkspace().getFirstMatch(mOp) != null;
+ }
+ }
+}