Implement new thumbnail loading strategy

- By default, we load only the reduced resolution screenshots.
- As soon as the user stops scrolling fast, we also start loading
full resolution screenshots.
- We prefetch reduced resolution screenshots when scrolling from
back to front, as the other direction is automatically prefetched
because the thumbnails aren't immediately visible.

Test: Open many apps, adb restart, scroll fast and slow in recents
Test: runtest systemui -c com.android.systemui.recents.model.HighResThumbnailLoaderTest
Bug: 34829962
Change-Id: I7f7a9842eb28a09a18573426fa9677cee2877124
This commit is contained in:
Jorim Jaggi
2017-03-17 17:22:47 +01:00
parent 35e3f53a30
commit 6f9dbcb742
13 changed files with 429 additions and 34 deletions

View File

@@ -726,6 +726,10 @@
<!-- The alpha to apply to the recents row when it doesn't have focus -->
<item name="recents_recents_row_dim_alpha" format="float" type="dimen">0.5</item>
<!-- The speed in dp/s at which the user needs to be scrolling in recents such that we start
loading full resolution screenshots. -->
<dimen name="recents_fast_fling_velocity">600dp</dimen>
<!-- The size of the PIP drag-to-dismiss target. -->
<dimen name="pip_dismiss_target_size">48dp</dimen>

View File

@@ -33,6 +33,7 @@ import android.hardware.display.DisplayManager;
import android.os.Build;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.RemoteException;
import android.os.SystemProperties;
import android.os.UserHandle;
@@ -59,6 +60,7 @@ import com.android.systemui.recents.events.component.ScreenPinningRequestEvent;
import com.android.systemui.recents.events.component.ShowUserToastEvent;
import com.android.systemui.recents.events.ui.RecentsDrawnEvent;
import com.android.systemui.recents.misc.SystemServicesProxy;
import com.android.systemui.recents.model.HighResThumbnailLoader;
import com.android.systemui.recents.model.RecentsTaskLoader;
import com.android.systemui.stackdivider.Divider;
import com.android.systemui.statusbar.CommandQueue;
@@ -184,6 +186,7 @@ public class Recents extends SystemUI
return sTaskLoader;
}
public static SystemServicesProxy getSystemServices() {
return sSystemServicesProxy;
}

View File

@@ -375,6 +375,8 @@ public class RecentsActivity extends Activity implements ViewTreeObserver.OnPreD
// Notify of the next draw
mRecentsView.getViewTreeObserver().addOnPreDrawListener(mRecentsDrawnEventListener);
Recents.getTaskLoader().getHighResThumbnailLoader().setVisible(true);
}
@Override
@@ -529,6 +531,7 @@ public class RecentsActivity extends Activity implements ViewTreeObserver.OnPreD
mReceivedNewIntent = false;
EventBus.getDefault().send(new RecentsVisibilityChangedEvent(this, false));
MetricsLogger.hidden(this, MetricsEvent.OVERVIEW_ACTIVITY);
Recents.getTaskLoader().getHighResThumbnailLoader().setVisible(false);
if (!isChangingConfigurations()) {
// Workaround for b/22542869, if the RecentsActivity is started again, but without going

View File

@@ -637,7 +637,7 @@ public class SystemServicesProxy {
}
/** Returns the top task thumbnail for the given task id */
public ThumbnailData getTaskThumbnail(int taskId) {
public ThumbnailData getTaskThumbnail(int taskId, boolean reduced) {
if (mAm == null) return null;
// If we are mocking, then just return a dummy thumbnail
@@ -649,7 +649,7 @@ public class SystemServicesProxy {
return thumbnailData;
}
ThumbnailData thumbnailData = getThumbnail(taskId);
ThumbnailData thumbnailData = getThumbnail(taskId, reduced);
if (thumbnailData.thumbnail != null && !ActivityManager.ENABLE_TASK_SNAPSHOTS) {
thumbnailData.thumbnail.setHasAlpha(false);
// We use a dumb heuristic for now, if the thumbnail is purely transparent in the top
@@ -669,7 +669,7 @@ public class SystemServicesProxy {
/**
* Returns a task thumbnail from the activity manager
*/
public @NonNull ThumbnailData getThumbnail(int taskId) {
public @NonNull ThumbnailData getThumbnail(int taskId, boolean reducedResolution) {
if (mAm == null) {
return new ThumbnailData();
}

View File

@@ -0,0 +1,215 @@
/*
* 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.systemui.recents.model;
import static android.os.Process.setThreadPriority;
import android.os.Handler;
import android.os.Looper;
import android.os.SystemClock;
import android.util.ArraySet;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.systemui.recents.misc.SystemServicesProxy;
import com.android.systemui.recents.model.Task.TaskCallbacks;
import java.util.ArrayDeque;
import java.util.ArrayList;
/**
* Loader class that loads full-resolution thumbnails when appropriate.
*/
public class HighResThumbnailLoader implements TaskCallbacks {
@GuardedBy("mLoadQueue")
private final ArrayDeque<Task> mLoadQueue = new ArrayDeque<>();
@GuardedBy("mLoadQueue")
private final ArraySet<Task> mLoadingTasks = new ArraySet<>();
@GuardedBy("mLoadQueue")
private boolean mLoaderIdling;
private final ArrayList<Task> mVisibleTasks = new ArrayList<>();
private final Thread mLoadThread;
private final Handler mMainThreadHandler;
private final SystemServicesProxy mSystemServicesProxy;
private boolean mLoading;
private boolean mVisible;
private boolean mFlingingFast;
public HighResThumbnailLoader(SystemServicesProxy ssp, Looper looper) {
mMainThreadHandler = new Handler(looper);
mLoadThread = new Thread(mLoader, "Recents-HighResThumbnailLoader");
mLoadThread.start();
mSystemServicesProxy = ssp;
}
public void setVisible(boolean visible) {
mVisible = visible;
updateLoading();
}
public void setFlingingFast(boolean flingingFast) {
if (mFlingingFast == flingingFast) {
return;
}
mFlingingFast = flingingFast;
updateLoading();
}
@VisibleForTesting
boolean isLoading() {
return mLoading;
}
private void updateLoading() {
setLoading(mVisible && !mFlingingFast);
}
private void setLoading(boolean loading) {
if (loading == mLoading) {
return;
}
synchronized (mLoadQueue) {
mLoading = loading;
if (!loading) {
stopLoading();
} else {
startLoading();
}
}
}
@GuardedBy("mLoadQueue")
private void startLoading() {
for (int i = mVisibleTasks.size() - 1; i >= 0; i--) {
Task t = mVisibleTasks.get(i);
if ((t.thumbnail == null || t.thumbnail.reducedResolution)
&& !mLoadQueue.contains(t) && !mLoadingTasks.contains(t)) {
mLoadQueue.add(t);
}
}
mLoadQueue.notifyAll();
}
@GuardedBy("mLoadQueue")
private void stopLoading() {
mLoadQueue.clear();
mLoadQueue.notifyAll();
}
/**
* Needs to be called when a task becomes visible. Note that this is different from
* {@link TaskCallbacks#onTaskDataLoaded} as this method should only be called once when it
* becomes visible, whereas onTaskDataLoaded can be called multiple times whenever some data
* has been updated.
*/
public void onTaskVisible(Task t) {
t.addCallback(this);
mVisibleTasks.add(t);
if ((t.thumbnail == null || t.thumbnail.reducedResolution) && mLoading) {
synchronized (mLoadQueue) {
mLoadQueue.add(t);
mLoadQueue.notifyAll();
}
}
}
/**
* Needs to be called when a task becomes visible. See {@link #onTaskVisible} why this is
* different from {@link TaskCallbacks#onTaskDataUnloaded()}
*/
public void onTaskInvisible(Task t) {
t.removeCallback(this);
mVisibleTasks.remove(t);
synchronized (mLoadQueue) {
mLoadQueue.remove(t);
}
}
@VisibleForTesting
void waitForLoaderIdle() {
while (true) {
synchronized (mLoadQueue) {
if (mLoadQueue.isEmpty() && mLoaderIdling) {
return;
}
}
SystemClock.sleep(100);
}
}
@Override
public void onTaskDataLoaded(Task task, ThumbnailData thumbnailData) {
if (thumbnailData != null && !thumbnailData.reducedResolution) {
synchronized (mLoadQueue) {
mLoadQueue.remove(task);
}
}
}
@Override
public void onTaskDataUnloaded() {
}
@Override
public void onTaskStackIdChanged() {
}
private final Runnable mLoader = new Runnable() {
@Override
public void run() {
setThreadPriority(android.os.Process.THREAD_PRIORITY_BACKGROUND + 1);
while (true) {
Task next = null;
synchronized (mLoadQueue) {
if (!mLoading || mLoadQueue.isEmpty()) {
try {
mLoaderIdling = true;
mLoadQueue.wait();
mLoaderIdling = false;
} catch (InterruptedException e) {
// Don't care.
}
} else {
next = mLoadQueue.poll();
if (next != null) {
mLoadingTasks.add(next);
}
}
}
if (next != null) {
loadTask(next);
}
}
}
private void loadTask(Task t) {
ThumbnailData thumbnail = mSystemServicesProxy.getTaskThumbnail(t.key.id,
false /* reducedResolution */);
mMainThreadHandler.post(() -> {
synchronized (mLoadQueue) {
mLoadingTasks.remove(t);
}
if (mVisibleTasks.contains(t)) {
t.notifyTaskDataLoaded(thumbnail, t.icon);
}
});
}
};
}

View File

@@ -188,7 +188,8 @@ public class RecentsTaskLoadPlan {
Drawable icon = isStackTask
? loader.getAndUpdateActivityIcon(taskKey, t.taskDescription, res, false)
: null;
Bitmap thumbnail = loader.getAndUpdateThumbnail(taskKey, false /* loadIfNotCached */);
ThumbnailData thumbnail = loader.getAndUpdateThumbnail(taskKey,
false /* loadIfNotCached */);
int activityColor = loader.getActivityPrimaryColor(t.taskDescription);
int backgroundColor = loader.getActivityBackgroundColor(t.taskDescription);
boolean isSystemApp = (info != null) &&

View File

@@ -27,6 +27,7 @@ import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.util.Log;
import android.util.LruCache;
@@ -37,6 +38,7 @@ import com.android.systemui.recents.RecentsDebugFlags;
import com.android.systemui.recents.events.activity.PackagesChangedEvent;
import com.android.systemui.recents.misc.SystemServicesProxy;
import com.android.systemui.recents.misc.Utilities;
import com.android.systemui.recents.model.Task.TaskKey;
import java.io.PrintWriter;
import java.util.Map;
@@ -156,7 +158,6 @@ class BackgroundTaskLoader implements Runnable {
}
}
} else {
RecentsConfiguration config = Recents.getConfiguration();
SystemServicesProxy ssp = Recents.getSystemServices();
// If we've stopped the loader, then fall through to the above logic to wait on
// the load thread
@@ -190,7 +191,8 @@ class BackgroundTaskLoader implements Runnable {
}
if (DEBUG) Log.d(TAG, "Loading thumbnail: " + t.key);
ThumbnailData cachedThumbnailData = ssp.getTaskThumbnail(t.key.id);
ThumbnailData cachedThumbnailData = ssp.getTaskThumbnail(t.key.id,
true /* reducedResolution */);
if (cachedThumbnailData.thumbnail == null) {
cachedThumbnailData.thumbnail = mDefaultThumbnail;
@@ -242,6 +244,7 @@ public class RecentsTaskLoader {
private final TaskKeyLruCache<String> mContentDescriptionCache;
private final TaskResourceLoadQueue mLoadQueue;
private final BackgroundTaskLoader mLoader;
private final HighResThumbnailLoader mHighResThumbnailLoader;
private final int mMaxThumbnailCacheSize;
private final int mMaxIconCacheSize;
@@ -293,6 +296,8 @@ public class RecentsTaskLoader {
mClearActivityInfoOnEviction);
mActivityInfoCache = new LruCache(numRecentTasks);
mLoader = new BackgroundTaskLoader(mLoadQueue, mIconCache, mDefaultThumbnail, mDefaultIcon);
mHighResThumbnailLoader = new HighResThumbnailLoader(Recents.getSystemServices(),
Looper.getMainLooper());
}
/** Returns the size of the app icon cache. */
@@ -305,6 +310,10 @@ public class RecentsTaskLoader {
return mMaxThumbnailCacheSize;
}
public HighResThumbnailLoader getHighResThumbnailLoader() {
return mHighResThumbnailLoader;
}
/** Creates a new plan for loading the recent tasks. */
public RecentsTaskLoadPlan createLoadPlan(Context context) {
RecentsTaskLoadPlan plan = new RecentsTaskLoadPlan(context);
@@ -346,7 +355,7 @@ public class RecentsTaskLoader {
/** Releases the task resource data back into the pool. */
public void unloadTaskData(Task t) {
mLoadQueue.removeTask(t);
t.notifyTaskDataUnloaded(null, mDefaultIcon);
t.notifyTaskDataUnloaded(mDefaultIcon);
}
/** Completely removes the resource data from the pool. */
@@ -356,7 +365,7 @@ public class RecentsTaskLoader {
mActivityLabelCache.remove(t.key);
mContentDescriptionCache.remove(t.key);
if (notifyTaskDataUnloaded) {
t.notifyTaskDataUnloaded(null, mDefaultIcon);
t.notifyTaskDataUnloaded(mDefaultIcon);
}
}
@@ -491,16 +500,16 @@ public class RecentsTaskLoader {
/**
* Returns the cached thumbnail if the task key is not expired, updating the cache if it is.
*/
Bitmap getAndUpdateThumbnail(Task.TaskKey taskKey, boolean loadIfNotCached) {
ThumbnailData getAndUpdateThumbnail(Task.TaskKey taskKey, boolean loadIfNotCached) {
SystemServicesProxy ssp = Recents.getSystemServices();
if (loadIfNotCached) {
RecentsConfiguration config = Recents.getConfiguration();
if (config.svelteLevel < RecentsConfiguration.SVELTE_DISABLE_LOADING) {
// Load the thumbnail from the system
ThumbnailData thumbnailData = ssp.getTaskThumbnail(taskKey.id);
ThumbnailData thumbnailData = ssp.getTaskThumbnail(taskKey.id, true /* reducedResolution */);
if (thumbnailData.thumbnail != null) {
return thumbnailData.thumbnail;
return thumbnailData;
}
}
}

View File

@@ -17,6 +17,7 @@
package com.android.systemui.recents.model;
import android.app.ActivityManager;
import android.app.ActivityManager.TaskThumbnail;
import android.content.ComponentName;
import android.content.Intent;
import android.content.pm.ActivityInfo;
@@ -141,7 +142,7 @@ public class Task {
* which can then fall back to the application icon.
*/
public Drawable icon;
public Bitmap thumbnail;
public ThumbnailData thumbnail;
@ViewDebug.ExportedProperty(category="recents")
public String title;
@ViewDebug.ExportedProperty(category="recents")
@@ -199,11 +200,11 @@ public class Task {
}
public Task(TaskKey key, int affiliationTaskId, int affiliationColor, Drawable icon,
Bitmap thumbnail, String title, String titleDescription, String dismissDescription,
String appInfoDescription, int colorPrimary, int colorBackground,
boolean isLaunchTarget, boolean isStackTask, boolean isSystemApp,
boolean isDockable, Rect bounds, ActivityManager.TaskDescription taskDescription,
int resizeMode, ComponentName topActivity, boolean isLocked) {
ThumbnailData thumbnail, String title, String titleDescription,
String dismissDescription, String appInfoDescription, int colorPrimary,
int colorBackground, boolean isLaunchTarget, boolean isStackTask, boolean isSystemApp,
boolean isDockable, Rect bounds, ActivityManager.TaskDescription taskDescription,
int resizeMode, ComponentName topActivity, boolean isLocked) {
boolean isInAffiliationGroup = (affiliationTaskId != key.id);
boolean hasAffiliationGroupColor = isInAffiliationGroup && (affiliationColor != 0);
this.key = key;
@@ -301,7 +302,7 @@ public class Task {
/** Notifies the callback listeners that this task has been loaded */
public void notifyTaskDataLoaded(ThumbnailData thumbnailData, Drawable applicationIcon) {
this.icon = applicationIcon;
this.thumbnail = thumbnailData != null ? thumbnailData.thumbnail : null;
this.thumbnail = thumbnailData;
int callbackCount = mCallbacks.size();
for (int i = 0; i < callbackCount; i++) {
mCallbacks.get(i).onTaskDataLoaded(this, thumbnailData);
@@ -309,9 +310,9 @@ public class Task {
}
/** Notifies the callback listeners that this task has been unloaded */
public void notifyTaskDataUnloaded(Bitmap defaultThumbnail, Drawable defaultApplicationIcon) {
public void notifyTaskDataUnloaded(Drawable defaultApplicationIcon) {
icon = defaultApplicationIcon;
thumbnail = defaultThumbnail;
thumbnail = null;
for (int i = mCallbacks.size() - 1; i >= 0; i--) {
mCallbacks.get(i).onTaskDataUnloaded();
}

View File

@@ -29,12 +29,16 @@ public class ThumbnailData {
public Bitmap thumbnail;
public int orientation;
public final Rect insets = new Rect();
public boolean reducedResolution;
public float scale;
public static ThumbnailData createFromTaskSnapshot(TaskSnapshot snapshot) {
ThumbnailData out = new ThumbnailData();
out.thumbnail = Bitmap.createHardwareBitmap(snapshot.getSnapshot());
out.insets.set(snapshot.getContentInsets());
out.orientation = snapshot.getOrientation();
out.reducedResolution = snapshot.isReducedResolution();
out.scale = snapshot.getScale();
return out;
}
}

View File

@@ -83,8 +83,8 @@ import com.android.systemui.recents.events.ui.TaskViewDismissedEvent;
import com.android.systemui.recents.events.ui.UpdateFreeformTaskViewVisibilityEvent;
import com.android.systemui.recents.events.ui.UserInteractionEvent;
import com.android.systemui.recents.events.ui.dragndrop.DragDropTargetChangedEvent;
import com.android.systemui.recents.events.ui.dragndrop.DragEndEvent;
import com.android.systemui.recents.events.ui.dragndrop.DragEndCancelledEvent;
import com.android.systemui.recents.events.ui.dragndrop.DragEndEvent;
import com.android.systemui.recents.events.ui.dragndrop.DragStartEvent;
import com.android.systemui.recents.events.ui.dragndrop.DragStartInitializeDropTargetsEvent;
import com.android.systemui.recents.events.ui.focus.DismissFocusedTaskViewEvent;
@@ -96,10 +96,10 @@ import com.android.systemui.recents.misc.SystemServicesProxy;
import com.android.systemui.recents.misc.Utilities;
import com.android.systemui.recents.model.Task;
import com.android.systemui.recents.model.TaskStack;
import com.android.systemui.recents.views.grid.GridTaskView;
import com.android.systemui.recents.views.grid.TaskGridLayoutAlgorithm;
import com.android.systemui.recents.views.grid.TaskViewFocusFrame;
import java.io.PrintWriter;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -217,6 +217,9 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal
// grid layout.
private TaskViewFocusFrame mTaskViewFocusFrame;
private Task mPrefetchingTask;
private final float mFastFlingVelocity;
// A convenience update listener to request updating clipping of tasks
private ValueAnimator.AnimatorUpdateListener mRequestUpdateClippingListener =
new ValueAnimator.AnimatorUpdateListener() {
@@ -273,6 +276,7 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal
mTaskCornerRadiusPx = Recents.getConfiguration().isGridEnabled ?
res.getDimensionPixelSize(R.dimen.recents_grid_task_view_rounded_corners_radius) :
res.getDimensionPixelSize(R.dimen.recents_task_view_rounded_corners_radius);
mFastFlingVelocity = res.getDimensionPixelSize(R.dimen.recents_fast_fling_velocity);
mDividerSize = ssp.getDockedDividerSize(context);
mDisplayOrientation = Utilities.getAppConfiguration(mContext).orientation;
mDisplayRect = ssp.getDisplayRect();
@@ -663,6 +667,8 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal
}
}
updatePrefetchingTask(tasks, visibleTaskRange[0], visibleTaskRange[1]);
// Update the focus if the previous focused task was returned to the view pool
if (lastFocusedTaskIndex != -1) {
int newFocusedTaskIndex = (lastFocusedTaskIndex < visibleTaskRange[1])
@@ -1200,6 +1206,8 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal
if (mStackScroller.computeScroll()) {
// Notify accessibility
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SCROLLED);
Recents.getTaskLoader().getHighResThumbnailLoader().setFlingingFast(
mStackScroller.getScrollVelocity() > mFastFlingVelocity);
}
if (mDeferredTaskViewLayoutAnimation != null) {
relayoutTaskViews(mDeferredTaskViewLayoutAnimation);
@@ -1657,13 +1665,41 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal
tv.setNoUserInteractionState();
}
// Load the task data
Recents.getTaskLoader().loadTaskData(task);
if (task == mPrefetchingTask) {
task.notifyTaskDataLoaded(task.thumbnail, task.icon);
} else {
// Load the task data
Recents.getTaskLoader().loadTaskData(task);
}
Recents.getTaskLoader().getHighResThumbnailLoader().onTaskVisible(task);
}
private void unbindTaskView(TaskView tv, Task task) {
// Report that this task's data is no longer being used
Recents.getTaskLoader().unloadTaskData(task);
if (task != mPrefetchingTask) {
// Report that this task's data is no longer being used
Recents.getTaskLoader().unloadTaskData(task);
}
Recents.getTaskLoader().getHighResThumbnailLoader().onTaskInvisible(task);
}
private void updatePrefetchingTask(ArrayList<Task> tasks, int frontIndex, int backIndex) {
Task t = null;
boolean somethingVisible = frontIndex != -1 && backIndex != -1;
if (somethingVisible && frontIndex < tasks.size() - 1) {
t = tasks.get(frontIndex + 1);
}
if (mPrefetchingTask != t) {
if (mPrefetchingTask != null) {
int index = tasks.indexOf(mPrefetchingTask);
if (index < backIndex || index > frontIndex) {
Recents.getTaskLoader().unloadTaskData(mPrefetchingTask);
}
}
mPrefetchingTask = t;
if (t != null) {
Recents.getTaskLoader().loadTaskData(t);
}
}
}
/**** TaskViewCallbacks Implementation ****/

View File

@@ -262,6 +262,10 @@ public class TaskStackViewScroller {
return !mScroller.isFinished();
}
float getScrollVelocity() {
return mScroller.getCurrVelocity();
}
/** Stops the scroller and any current fling. */
void stopScroller() {
if (!mScroller.isFinished()) {

View File

@@ -66,7 +66,7 @@ public class TaskViewThumbnail extends View {
protected Rect mThumbnailRect = new Rect();
@ViewDebug.ExportedProperty(category="recents")
protected float mThumbnailScale;
private float mFullscreenThumbnailScale;
private float mFullscreenThumbnailScale = 1f;
/** The height, in pixels, of the task view's title bar. */
private int mTitleBarHeight;
private boolean mSizeToFit = false;
@@ -116,12 +116,6 @@ public class TaskViewThumbnail extends View {
mCornerRadius = res.getDimensionPixelSize(R.dimen.recents_task_view_rounded_corners_radius);
mBgFillPaint.setColor(Color.WHITE);
mLockedPaint.setColor(Color.WHITE);
if (ActivityManager.ENABLE_TASK_SNAPSHOTS) {
mFullscreenThumbnailScale = 1f;
} else {
mFullscreenThumbnailScale = res.getFraction(
com.android.internal.R.fraction.thumbnail_fullscreen_scale, 1, 1);
}
mTitleBarHeight = res.getDimensionPixelSize(R.dimen.recents_grid_task_view_header_height);
}
@@ -190,6 +184,7 @@ public class TaskViewThumbnail extends View {
if (thumbnailData != null && thumbnailData.thumbnail != null) {
Bitmap bm = thumbnailData.thumbnail;
bm.prepareToDraw();
mFullscreenThumbnailScale = thumbnailData.scale;
mBitmapShader = new BitmapShader(bm, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
mDrawPaint.setShader(mBitmapShader);
mThumbnailRect.set(0, 0,
@@ -297,7 +292,8 @@ public class TaskViewThumbnail extends View {
(float) mTaskViewRect.width() / mThumbnailRect.width(),
(float) mTaskViewRect.height() / mThumbnailRect.height());
}
mMatrix.setTranslate(-mThumbnailData.insets.left, -mThumbnailData.insets.top);
mMatrix.setTranslate(-mThumbnailData.insets.left * mFullscreenThumbnailScale,
-mThumbnailData.insets.top * mFullscreenThumbnailScale);
mMatrix.postScale(mThumbnailScale, mThumbnailScale);
mBitmapShader.setLocalMatrix(mMatrix);
}

View File

@@ -0,0 +1,119 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License
*/
package com.android.systemui.recents.model;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.anyBoolean;
import static org.mockito.Mockito.anyInt;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.os.Looper;
import android.support.test.filters.SmallTest;
import android.support.test.runner.AndroidJUnit4;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.recents.misc.SystemServicesProxy;
import com.android.systemui.recents.model.Task.TaskKey;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
/**
* runtest systemui -c com.android.systemui.recents.model.HighResThumbnailLoaderTest
*/
@SmallTest
@RunWith(AndroidJUnit4.class)
public class HighResThumbnailLoaderTest extends SysuiTestCase {
private HighResThumbnailLoader mLoader;
@Mock
private SystemServicesProxy mMockSystemServicesProxy;
@Mock
private Task mTask;
private ThumbnailData mThumbnailData = new ThumbnailData();
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
mLoader = new HighResThumbnailLoader(mMockSystemServicesProxy, Looper.getMainLooper());
mTask.key = new TaskKey(0, 0, null, 0, 0, 0);
when(mMockSystemServicesProxy.getTaskThumbnail(anyInt(), anyBoolean()))
.thenReturn(mThumbnailData);
mLoader.setVisible(true);
}
@Test
public void testLoading() throws Exception {
mLoader.setVisible(true);
assertTrue(mLoader.isLoading());
mLoader.setVisible(false);
assertFalse(mLoader.isLoading());
mLoader.setVisible(true);
mLoader.setFlingingFast(true);
assertFalse(mLoader.isLoading());
mLoader.setFlingingFast(false);
assertTrue(mLoader.isLoading());
}
@Test
public void testLoad() throws Exception {
mLoader.onTaskVisible(mTask);
mLoader.waitForLoaderIdle();
waitForIdleSync();
verify(mTask).notifyTaskDataLoaded(mThumbnailData, null);
}
@Test
public void testFlinging_notLoaded() throws Exception {
mLoader.setFlingingFast(true);
mLoader.onTaskVisible(mTask);
mLoader.waitForLoaderIdle();
waitForIdleSync();
verify(mTask, never()).notifyTaskDataLoaded(mThumbnailData, null);
}
/**
* Tests whether task is loaded after stopping to fling
*/
@Test
public void testAfterFlinging() throws Exception {
mLoader.setFlingingFast(true);
mLoader.onTaskVisible(mTask);
mLoader.setFlingingFast(false);
mLoader.waitForLoaderIdle();
waitForIdleSync();
verify(mTask).notifyTaskDataLoaded(mThumbnailData, null);
}
@Test
public void testAlreadyLoaded() throws Exception {
mTask.thumbnail = new ThumbnailData();
mTask.thumbnail.reducedResolution = false;
mLoader.onTaskVisible(mTask);
mLoader.waitForLoaderIdle();
waitForIdleSync();
verify(mTask, never()).notifyTaskDataLoaded(mThumbnailData, null);
}
}