diff options
Diffstat (limited to 'packages/SystemUI/src')
27 files changed, 4637 insertions, 207 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/ImageUtils.java b/packages/SystemUI/src/com/android/systemui/ImageUtils.java new file mode 100644 index 0000000..540ba20 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/ImageUtils.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2014 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; + +import android.graphics.Bitmap; + +/** + * Utility class for image analysis and processing. + */ +public class ImageUtils { + + // Amount (max is 255) that two channels can differ before the color is no longer "gray". + private static final int TOLERANCE = 20; + + // Alpha amount for which values below are considered transparent. + private static final int ALPHA_TOLERANCE = 50; + + private int[] mTempBuffer; + + /** + * Checks whether a bitmap is grayscale. Grayscale here means "very close to a perfect + * gray". + */ + public boolean isGrayscale(Bitmap bitmap) { + final int height = bitmap.getHeight(); + final int width = bitmap.getWidth(); + int size = height*width; + + ensureBufferSize(size); + bitmap.getPixels(mTempBuffer, 0, width, 0, 0, width, height); + for (int i = 0; i < size; i++) { + if (!isGrayscale(mTempBuffer[i])) { + return false; + } + } + return true; + } + + /** + * Makes sure that {@code mTempBuffer} has at least length {@code size}. + */ + private void ensureBufferSize(int size) { + if (mTempBuffer == null || mTempBuffer.length < size) { + mTempBuffer = new int[size]; + } + } + + /** + * Classifies a color as grayscale or not. Grayscale here means "very close to a perfect + * gray"; if all three channels are approximately equal, this will return true. + * + * Note that really transparent colors are always grayscale. + */ + public boolean isGrayscale(int color) { + int alpha = 0xFF & (color >> 24); + if (alpha < ALPHA_TOLERANCE) { + return true; + } + + int r = 0xFF & (color >> 16); + int g = 0xFF & (color >> 8); + int b = 0xFF & color; + + return Math.abs(r - g) < TOLERANCE + && Math.abs(r - b) < TOLERANCE + && Math.abs(g - b) < TOLERANCE; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/power/PowerDialogWarnings.java b/packages/SystemUI/src/com/android/systemui/power/PowerDialogWarnings.java new file mode 100644 index 0000000..feec87c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/power/PowerDialogWarnings.java @@ -0,0 +1,218 @@ +/* + * Copyright (C) 2014 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.power; + +import android.app.AlertDialog; +import android.content.ContentResolver; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.media.AudioManager; +import android.media.Ringtone; +import android.media.RingtoneManager; +import android.net.Uri; +import android.os.SystemClock; +import android.os.UserHandle; +import android.provider.Settings; +import android.util.Slog; +import android.view.View; +import android.view.WindowManager; +import android.widget.TextView; + +import com.android.systemui.R; + +import java.io.PrintWriter; + +public class PowerDialogWarnings implements PowerUI.WarningsUI { + private static final String TAG = PowerUI.TAG + ".Dialog"; + private static final boolean DEBUG = PowerUI.DEBUG; + + private final Context mContext; + + private int mBatteryLevel; + private int mBucket; + private long mScreenOffTime; + + private AlertDialog mInvalidChargerDialog; + private AlertDialog mLowBatteryDialog; + private TextView mBatteryLevelTextView; + + public PowerDialogWarnings(Context context) { + mContext = context; + } + + @Override + public void dump(PrintWriter pw) { + pw.print("mInvalidChargerDialog="); + pw.println(mInvalidChargerDialog == null ? "null" : mInvalidChargerDialog.toString()); + pw.print("mLowBatteryDialog="); + pw.println(mLowBatteryDialog == null ? "null" : mLowBatteryDialog.toString()); + } + + @Override + public void update(int batteryLevel, int bucket, long screenOffTime) { + mBatteryLevel = batteryLevel; + mBucket = bucket; + mScreenOffTime = screenOffTime; + } + + @Override + public boolean isInvalidChargerWarningShowing() { + return mInvalidChargerDialog != null; + } + + @Override + public void updateLowBatteryWarning() { + if (mBatteryLevelTextView != null) { + showLowBatteryWarning(false /*playSound*/); + } + } + + @Override + public void dismissLowBatteryWarning() { + if (mLowBatteryDialog != null) { + Slog.i(TAG, "closing low battery warning: level=" + mBatteryLevel); + mLowBatteryDialog.dismiss(); + } + } + + @Override + public void showLowBatteryWarning(boolean playSound) { + Slog.i(TAG, + ((mBatteryLevelTextView == null) ? "showing" : "updating") + + " low battery warning: level=" + mBatteryLevel + + " [" + mBucket + "]"); + + CharSequence levelText = mContext.getString( + R.string.battery_low_percent_format, mBatteryLevel); + + if (mBatteryLevelTextView != null) { + mBatteryLevelTextView.setText(levelText); + } else { + View v = View.inflate(mContext, R.layout.battery_low, null); + mBatteryLevelTextView = (TextView)v.findViewById(R.id.level_percent); + + mBatteryLevelTextView.setText(levelText); + + AlertDialog.Builder b = new AlertDialog.Builder(mContext); + b.setCancelable(true); + b.setTitle(R.string.battery_low_title); + b.setView(v); + b.setIconAttribute(android.R.attr.alertDialogIcon); + b.setPositiveButton(android.R.string.ok, null); + + final Intent intent = new Intent(Intent.ACTION_POWER_USAGE_SUMMARY); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK + | Intent.FLAG_ACTIVITY_MULTIPLE_TASK + | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS + | Intent.FLAG_ACTIVITY_NO_HISTORY); + if (intent.resolveActivity(mContext.getPackageManager()) != null) { + b.setNegativeButton(R.string.battery_low_why, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + mContext.startActivityAsUser(intent, UserHandle.CURRENT); + dismissLowBatteryWarning(); + } + }); + } + + AlertDialog d = b.create(); + d.setOnDismissListener(new DialogInterface.OnDismissListener() { + @Override + public void onDismiss(DialogInterface dialog) { + mLowBatteryDialog = null; + mBatteryLevelTextView = null; + } + }); + d.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT); + d.getWindow().getAttributes().privateFlags |= + WindowManager.LayoutParams.PRIVATE_FLAG_SHOW_FOR_ALL_USERS; + d.show(); + mLowBatteryDialog = d; + if (playSound) { + playLowBatterySound(); + } + } + } + + private void playLowBatterySound() { + final ContentResolver cr = mContext.getContentResolver(); + + final int silenceAfter = Settings.Global.getInt(cr, + Settings.Global.LOW_BATTERY_SOUND_TIMEOUT, 0); + final long offTime = SystemClock.elapsedRealtime() - mScreenOffTime; + if (silenceAfter > 0 + && mScreenOffTime > 0 + && offTime > silenceAfter) { + Slog.i(TAG, "screen off too long (" + offTime + "ms, limit " + silenceAfter + + "ms): not waking up the user with low battery sound"); + return; + } + + if (DEBUG) { + Slog.d(TAG, "playing low battery sound. pick-a-doop!"); // WOMP-WOMP is deprecated + } + + if (Settings.Global.getInt(cr, Settings.Global.POWER_SOUNDS_ENABLED, 1) == 1) { + final String soundPath = Settings.Global.getString(cr, + Settings.Global.LOW_BATTERY_SOUND); + if (soundPath != null) { + final Uri soundUri = Uri.parse("file://" + soundPath); + if (soundUri != null) { + final Ringtone sfx = RingtoneManager.getRingtone(mContext, soundUri); + if (sfx != null) { + sfx.setStreamType(AudioManager.STREAM_SYSTEM); + sfx.play(); + } + } + } + } + } + + @Override + public void dismissInvalidChargerWarning() { + if (mInvalidChargerDialog != null) { + mInvalidChargerDialog.dismiss(); + } + } + + @Override + public void showInvalidChargerWarning() { + Slog.d(TAG, "showing invalid charger dialog"); + + dismissLowBatteryWarning(); + + AlertDialog.Builder b = new AlertDialog.Builder(mContext); + b.setCancelable(true); + b.setMessage(R.string.invalid_charger); + b.setIconAttribute(android.R.attr.alertDialogIcon); + b.setPositiveButton(android.R.string.ok, null); + + AlertDialog d = b.create(); + d.setOnDismissListener(new DialogInterface.OnDismissListener() { + public void onDismiss(DialogInterface dialog) { + mInvalidChargerDialog = null; + mBatteryLevelTextView = null; + } + }); + + d.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT); + d.show(); + mInvalidChargerDialog = d; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/power/PowerUI.java b/packages/SystemUI/src/com/android/systemui/power/PowerUI.java index 28c2772..0fb0f8b 100644 --- a/packages/SystemUI/src/com/android/systemui/power/PowerUI.java +++ b/packages/SystemUI/src/com/android/systemui/power/PowerUI.java @@ -16,29 +16,17 @@ package com.android.systemui.power; -import android.app.AlertDialog; import android.content.BroadcastReceiver; -import android.content.ContentResolver; import android.content.Context; -import android.content.DialogInterface; import android.content.Intent; import android.content.IntentFilter; -import android.media.AudioManager; -import android.media.Ringtone; -import android.media.RingtoneManager; -import android.net.Uri; import android.os.BatteryManager; import android.os.Handler; import android.os.PowerManager; import android.os.SystemClock; -import android.os.UserHandle; import android.provider.Settings; import android.util.Slog; -import android.view.View; -import android.view.WindowManager; -import android.widget.TextView; -import com.android.systemui.R; import com.android.systemui.SystemUI; import java.io.FileDescriptor; @@ -50,19 +38,17 @@ public class PowerUI extends SystemUI { static final boolean DEBUG = false; - Handler mHandler = new Handler(); + private WarningsUI mWarnings; - int mBatteryLevel = 100; - int mBatteryStatus = BatteryManager.BATTERY_STATUS_UNKNOWN; - int mPlugType = 0; - int mInvalidCharger = 0; + private final Handler mHandler = new Handler(); - int mLowBatteryAlertCloseLevel; - int[] mLowBatteryReminderLevels = new int[2]; + private int mBatteryLevel = 100; + private int mBatteryStatus = BatteryManager.BATTERY_STATUS_UNKNOWN; + private int mPlugType = 0; + private int mInvalidCharger = 0; - AlertDialog mInvalidChargerDialog; - AlertDialog mLowBatteryDialog; - TextView mBatteryLevelTextView; + private int mLowBatteryAlertCloseLevel; + private final int[] mLowBatteryReminderLevels = new int[2]; private long mScreenOffTime = -1; @@ -77,6 +63,7 @@ public class PowerUI extends SystemUI { final PowerManager pm = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE); mScreenOffTime = pm.isScreenOn() ? -1 : SystemClock.elapsedRealtime(); + mWarnings = new PowerDialogWarnings(mContext); // Register for Intent broadcasts for... IntentFilter filter = new IntentFilter(); @@ -145,13 +132,14 @@ public class PowerUI extends SystemUI { Slog.d(TAG, "plugged " + oldPlugged + " --> " + plugged); } + mWarnings.update(mBatteryLevel, bucket, mScreenOffTime); if (oldInvalidCharger == 0 && mInvalidCharger != 0) { Slog.d(TAG, "showing invalid charger warning"); - showInvalidChargerDialog(); + mWarnings.showInvalidChargerWarning(); return; } else if (oldInvalidCharger != 0 && mInvalidCharger == 0) { - dismissInvalidChargerDialog(); - } else if (mInvalidChargerDialog != null) { + mWarnings.dismissInvalidChargerWarning(); + } else if (mWarnings.isInvalidChargerWarningShowing()) { // if invalid charger is showing, don't show low battery return; } @@ -160,16 +148,13 @@ public class PowerUI extends SystemUI { && (bucket < oldBucket || oldPlugged) && mBatteryStatus != BatteryManager.BATTERY_STATUS_UNKNOWN && bucket < 0) { - showLowBatteryWarning(); - // only play SFX when the dialog comes up or the bucket changes - if (bucket != oldBucket || oldPlugged) { - playLowBatterySound(); - } + final boolean playSound = bucket != oldBucket || oldPlugged; + mWarnings.showLowBatteryWarning(playSound); } else if (plugged || (bucket > oldBucket && bucket > 0)) { - dismissLowBatteryWarning(); - } else if (mBatteryLevelTextView != null) { - showLowBatteryWarning(); + mWarnings.dismissLowBatteryWarning(); + } else { + mWarnings.updateLowBatteryWarning(); } } else if (Intent.ACTION_SCREEN_OFF.equals(action)) { mScreenOffTime = SystemClock.elapsedRealtime(); @@ -181,142 +166,11 @@ public class PowerUI extends SystemUI { } }; - void dismissLowBatteryWarning() { - if (mLowBatteryDialog != null) { - Slog.i(TAG, "closing low battery warning: level=" + mBatteryLevel); - mLowBatteryDialog.dismiss(); - } - } - - void showLowBatteryWarning() { - Slog.i(TAG, - ((mBatteryLevelTextView == null) ? "showing" : "updating") - + " low battery warning: level=" + mBatteryLevel - + " [" + findBatteryLevelBucket(mBatteryLevel) + "]"); - - CharSequence levelText = mContext.getString( - R.string.battery_low_percent_format, mBatteryLevel); - - if (mBatteryLevelTextView != null) { - mBatteryLevelTextView.setText(levelText); - } else { - View v = View.inflate(mContext, R.layout.battery_low, null); - mBatteryLevelTextView = (TextView)v.findViewById(R.id.level_percent); - - mBatteryLevelTextView.setText(levelText); - - AlertDialog.Builder b = new AlertDialog.Builder(mContext); - b.setCancelable(true); - b.setTitle(R.string.battery_low_title); - b.setView(v); - b.setIconAttribute(android.R.attr.alertDialogIcon); - b.setPositiveButton(android.R.string.ok, null); - - final Intent intent = new Intent(Intent.ACTION_POWER_USAGE_SUMMARY); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK - | Intent.FLAG_ACTIVITY_MULTIPLE_TASK - | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS - | Intent.FLAG_ACTIVITY_NO_HISTORY); - if (intent.resolveActivity(mContext.getPackageManager()) != null) { - b.setNegativeButton(R.string.battery_low_why, - new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - mContext.startActivityAsUser(intent, UserHandle.CURRENT); - dismissLowBatteryWarning(); - } - }); - } - - AlertDialog d = b.create(); - d.setOnDismissListener(new DialogInterface.OnDismissListener() { - @Override - public void onDismiss(DialogInterface dialog) { - mLowBatteryDialog = null; - mBatteryLevelTextView = null; - } - }); - d.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT); - d.getWindow().getAttributes().privateFlags |= - WindowManager.LayoutParams.PRIVATE_FLAG_SHOW_FOR_ALL_USERS; - d.show(); - mLowBatteryDialog = d; - } - } - - void playLowBatterySound() { - final ContentResolver cr = mContext.getContentResolver(); - - final int silenceAfter = Settings.Global.getInt(cr, - Settings.Global.LOW_BATTERY_SOUND_TIMEOUT, 0); - final long offTime = SystemClock.elapsedRealtime() - mScreenOffTime; - if (silenceAfter > 0 - && mScreenOffTime > 0 - && offTime > silenceAfter) { - Slog.i(TAG, "screen off too long (" + offTime + "ms, limit " + silenceAfter - + "ms): not waking up the user with low battery sound"); - return; - } - - if (DEBUG) { - Slog.d(TAG, "playing low battery sound. pick-a-doop!"); // WOMP-WOMP is deprecated - } - - if (Settings.Global.getInt(cr, Settings.Global.POWER_SOUNDS_ENABLED, 1) == 1) { - final String soundPath = Settings.Global.getString(cr, - Settings.Global.LOW_BATTERY_SOUND); - if (soundPath != null) { - final Uri soundUri = Uri.parse("file://" + soundPath); - if (soundUri != null) { - final Ringtone sfx = RingtoneManager.getRingtone(mContext, soundUri); - if (sfx != null) { - sfx.setStreamType(AudioManager.STREAM_SYSTEM); - sfx.play(); - } - } - } - } - } - - void dismissInvalidChargerDialog() { - if (mInvalidChargerDialog != null) { - mInvalidChargerDialog.dismiss(); - } - } - - void showInvalidChargerDialog() { - Slog.d(TAG, "showing invalid charger dialog"); - - dismissLowBatteryWarning(); - - AlertDialog.Builder b = new AlertDialog.Builder(mContext); - b.setCancelable(true); - b.setMessage(R.string.invalid_charger); - b.setIconAttribute(android.R.attr.alertDialogIcon); - b.setPositiveButton(android.R.string.ok, null); - - AlertDialog d = b.create(); - d.setOnDismissListener(new DialogInterface.OnDismissListener() { - public void onDismiss(DialogInterface dialog) { - mInvalidChargerDialog = null; - mBatteryLevelTextView = null; - } - }); - - d.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT); - d.show(); - mInvalidChargerDialog = d; - } - public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { pw.print("mLowBatteryAlertCloseLevel="); pw.println(mLowBatteryAlertCloseLevel); pw.print("mLowBatteryReminderLevels="); pw.println(Arrays.toString(mLowBatteryReminderLevels)); - pw.print("mInvalidChargerDialog="); - pw.println(mInvalidChargerDialog == null ? "null" : mInvalidChargerDialog.toString()); - pw.print("mLowBatteryDialog="); - pw.println(mLowBatteryDialog == null ? "null" : mLowBatteryDialog.toString()); pw.print("mBatteryLevel="); pw.println(Integer.toString(mBatteryLevel)); pw.print("mBatteryStatus="); @@ -338,6 +192,18 @@ public class PowerUI extends SystemUI { Settings.Global.LOW_BATTERY_SOUND_TIMEOUT, 0)); pw.print("bucket: "); pw.println(Integer.toString(findBatteryLevelBucket(mBatteryLevel))); + mWarnings.dump(pw); + } + + public interface WarningsUI { + void update(int batteryLevel, int bucket, long screenOffTime); + void dismissLowBatteryWarning(); + void showLowBatteryWarning(boolean playSound); + void dismissInvalidChargerWarning(); + void showInvalidChargerWarning(); + void updateLowBatteryWarning(); + boolean isInvalidChargerWarningShowing(); + void dump(PrintWriter pw); } } diff --git a/packages/SystemUI/src/com/android/systemui/recent/Recents.java b/packages/SystemUI/src/com/android/systemui/recent/Recents.java index f5670e1..07c0c78 100644 --- a/packages/SystemUI/src/com/android/systemui/recent/Recents.java +++ b/packages/SystemUI/src/com/android/systemui/recent/Recents.java @@ -16,37 +16,143 @@ package com.android.systemui.recent; +import android.app.ActivityManager; import android.app.ActivityOptions; import android.content.ActivityNotFoundException; +import android.content.ComponentName; +import android.content.Context; import android.content.Intent; +import android.content.ServiceConnection; +import android.content.pm.ActivityInfo; +import android.content.pm.PackageManager; import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.Canvas; +import android.graphics.Matrix; import android.graphics.Paint; +import android.graphics.Rect; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.os.Handler; +import android.os.IBinder; +import android.os.Message; +import android.os.Messenger; +import android.os.RemoteException; +import android.os.SystemProperties; import android.os.UserHandle; import android.util.DisplayMetrics; import android.util.Log; import android.view.Display; +import android.view.Surface; +import android.view.SurfaceControl; import android.view.View; - +import android.view.WindowManager; import com.android.systemui.R; import com.android.systemui.RecentsComponent; import com.android.systemui.SystemUI; +import java.util.List; + + public class Recents extends SystemUI implements RecentsComponent { + /** A handler for messages from the recents implementation */ + class RecentsMessageHandler extends Handler { + @Override + public void handleMessage(Message msg) { + if (!mUseAlternateRecents) return; + if (msg.what == MSG_UPDATE_FOR_CONFIGURATION) { + Resources res = mContext.getResources(); + float statusBarHeight = res.getDimensionPixelSize( + com.android.internal.R.dimen.status_bar_height); + mFirstTaskRect = (Rect) msg.getData().getParcelable("taskRect"); + mFirstTaskRect.offset(0, (int) statusBarHeight); + } + } + } + + /** A service connection to the recents implementation */ + class RecentsServiceConnection implements ServiceConnection { + @Override + public void onServiceConnected(ComponentName className, IBinder service) { + if (!mUseAlternateRecents) return; + + Log.d(TAG, "[RecentsComponent|ServiceConnection|onServiceConnected] toggleRecents: " + + mToggleRecentsUponServiceBound); + mService = new Messenger(service); + mServiceIsBound = true; + + // Toggle recents if this service connection was triggered by hitting the recents button + if (mToggleRecentsUponServiceBound) { + startAlternateRecentsActivity(); + } + mToggleRecentsUponServiceBound = false; + } + + @Override + public void onServiceDisconnected(ComponentName className) { + if (!mUseAlternateRecents) return; + + Log.d(TAG, "[RecentsComponent|ServiceConnection|onServiceDisconnected]"); + mService = null; + mServiceIsBound = false; + } + } + private static final String TAG = "Recents"; - private static final boolean DEBUG = false; + private static final boolean DEBUG = true; + + final static int MSG_UPDATE_FOR_CONFIGURATION = 0; + final static int MSG_UPDATE_TASK_THUMBNAIL = 1; + final static int MSG_PRELOAD_TASKS = 2; + final static int MSG_CANCEL_PRELOAD_TASKS = 3; + + final static String sToggleRecentsAction = "com.android.systemui.recents.TOGGLE_RECENTS"; + final static String sRecentsPackage = "com.android.systemui"; + final static String sRecentsActivity = "com.android.systemui.recents.RecentsActivity"; + final static String sRecentsService = "com.android.systemui.recents.RecentsService"; + + // Which recents to use + boolean mUseAlternateRecents; + + // Recents service binding + Messenger mService = null; + Messenger mMessenger; + boolean mServiceIsBound = false; + boolean mToggleRecentsUponServiceBound; + RecentsServiceConnection mConnection = new RecentsServiceConnection(); + + View mStatusBarView; + Rect mFirstTaskRect = new Rect(); + + public Recents() { + mMessenger = new Messenger(new RecentsMessageHandler()); + } @Override public void start() { + mUseAlternateRecents = + SystemProperties.getBoolean("persist.recents.use_alternate", false); + putComponent(RecentsComponent.class, this); + + if (mUseAlternateRecents) { + Log.d(TAG, "[RecentsComponent|start]"); + + // Try to create a long-running connection to the recents service + bindToRecentsService(false); + } } @Override public void toggleRecents(Display display, int layoutDirection, View statusBarView) { + if (mUseAlternateRecents) { + // Launch the alternate recents if required + toggleAlternateRecents(display, layoutDirection, statusBarView); + return; + } + if (DEBUG) Log.d(TAG, "toggle recents panel"); try { TaskDescription firstTask = RecentTasksLoader.getInstance(mContext).getFirstTask(); @@ -190,33 +296,227 @@ public class Recents extends SystemUI implements RecentsComponent { } } + /** Toggles the alternate recents activity */ + public void toggleAlternateRecents(Display display, int layoutDirection, View statusBarView) { + if (!mUseAlternateRecents) return; + + Log.d(TAG, "[RecentsComponent|toggleRecents] serviceIsBound: " + mServiceIsBound); + mStatusBarView = statusBarView; + if (!mServiceIsBound) { + // Try to create a long-running connection to the recents service before toggling + // recents + bindToRecentsService(true); + return; + } + + try { + startAlternateRecentsActivity(); + } catch (ActivityNotFoundException e) { + Log.e(TAG, "Failed to launch RecentAppsIntent", e); + } + } + + @Override + protected void onConfigurationChanged(Configuration newConfig) { + if (mServiceIsBound) { + Resources res = mContext.getResources(); + int statusBarHeight = res.getDimensionPixelSize( + com.android.internal.R.dimen.status_bar_height); + int navBarHeight = res.getDimensionPixelSize( + com.android.internal.R.dimen.navigation_bar_height); + Rect rect = new Rect(); + WindowManager wm = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE); + wm.getDefaultDisplay().getRectSize(rect); + + // Try and update the recents configuration + try { + Bundle data = new Bundle(); + data.putParcelable("windowRect", rect); + data.putParcelable("systemInsets", new Rect(0, statusBarHeight, 0, 0)); + Message msg = Message.obtain(null, MSG_UPDATE_FOR_CONFIGURATION, 0, 0); + msg.setData(data); + msg.replyTo = mMessenger; + mService.send(msg); + } catch (RemoteException re) { + re.printStackTrace(); + } + } + } + + /** Binds to the recents implementation */ + private void bindToRecentsService(boolean toggleRecentsUponConnection) { + if (!mUseAlternateRecents) return; + + mToggleRecentsUponServiceBound = toggleRecentsUponConnection; + Intent intent = new Intent(); + intent.setClassName(sRecentsPackage, sRecentsService); + mContext.bindService(intent, mConnection, Context.BIND_AUTO_CREATE); + } + + /** Loads the first task thumbnail */ + Bitmap loadFirstTaskThumbnail() { + ActivityManager am = (ActivityManager) mContext.getSystemService(Context.ACTIVITY_SERVICE); + List<ActivityManager.RecentTaskInfo> tasks = am.getRecentTasksForUser(1, + ActivityManager.RECENT_IGNORE_UNAVAILABLE, UserHandle.CURRENT.getIdentifier()); + for (ActivityManager.RecentTaskInfo t : tasks) { + // Skip tasks in the home stack + if (am.isInHomeStack(t.persistentId)) { + return null; + } + + Bitmap thumbnail = am.getTaskTopThumbnail(t.persistentId); + return thumbnail; + } + return null; + } + + /** Returns whether there is a first task */ + boolean hasFirstTask() { + ActivityManager am = (ActivityManager) mContext.getSystemService(Context.ACTIVITY_SERVICE); + List<ActivityManager.RecentTaskInfo> tasks = am.getRecentTasksForUser(1, + ActivityManager.RECENT_IGNORE_UNAVAILABLE, UserHandle.CURRENT.getIdentifier()); + for (ActivityManager.RecentTaskInfo t : tasks) { + // Skip tasks in the home stack + if (am.isInHomeStack(t.persistentId)) { + continue; + } + + return true; + } + return false; + } + + /** Converts from the device rotation to the degree */ + float getDegreesForRotation(int value) { + switch (value) { + case Surface.ROTATION_90: + return 360f - 90f; + case Surface.ROTATION_180: + return 360f - 180f; + case Surface.ROTATION_270: + return 360f - 270f; + } + return 0f; + } + + /** Takes a screenshot of the surface */ + Bitmap takeScreenshot(Display display) { + DisplayMetrics dm = new DisplayMetrics(); + display.getRealMetrics(dm); + float[] dims = {dm.widthPixels, dm.heightPixels}; + float degrees = getDegreesForRotation(display.getRotation()); + boolean requiresRotation = (degrees > 0); + if (requiresRotation) { + // Get the dimensions of the device in its native orientation + Matrix m = new Matrix(); + m.preRotate(-degrees); + m.mapPoints(dims); + dims[0] = Math.abs(dims[0]); + dims[1] = Math.abs(dims[1]); + } + return SurfaceControl.screenshot((int) dims[0], (int) dims[1]); + } + + /** Starts the recents activity */ + void startAlternateRecentsActivity() { + Rect taskRect = mFirstTaskRect; + if (taskRect != null && taskRect.width() > 0 && taskRect.height() > 0 && hasFirstTask()) { + // Loading from thumbnail + Bitmap thumbnail; + Bitmap firstThumbnail = loadFirstTaskThumbnail(); + if (firstThumbnail == null) { + // Load the thumbnail from the screenshot + WindowManager wm = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE); + Display display = wm.getDefaultDisplay(); + Bitmap screenshot = takeScreenshot(display); + Resources res = mContext.getResources(); + int size = Math.min(screenshot.getWidth(), screenshot.getHeight()); + int statusBarHeight = res.getDimensionPixelSize( + com.android.internal.R.dimen.status_bar_height); + thumbnail = Bitmap.createBitmap(mFirstTaskRect.width(), mFirstTaskRect.height(), + Bitmap.Config.ARGB_8888); + Canvas c = new Canvas(thumbnail); + c.drawBitmap(screenshot, new Rect(0, statusBarHeight, size, statusBarHeight + size), + new Rect(0, 0, mFirstTaskRect.width(), mFirstTaskRect.height()), null); + c.setBitmap(null); + // Recycle the old screenshot + screenshot.recycle(); + } else { + // Create the thumbnail + thumbnail = Bitmap.createBitmap(mFirstTaskRect.width(), mFirstTaskRect.height(), + Bitmap.Config.ARGB_8888); + int size = Math.min(firstThumbnail.getWidth(), firstThumbnail.getHeight()); + Canvas c = new Canvas(thumbnail); + c.drawBitmap(firstThumbnail, new Rect(0, 0, size, size), + new Rect(0, 0, mFirstTaskRect.width(), mFirstTaskRect.height()), null); + c.setBitmap(null); + // Recycle the old thumbnail + firstThumbnail.recycle(); + } + + ActivityOptions opts = ActivityOptions.makeThumbnailScaleDownAnimation(mStatusBarView, + thumbnail, mFirstTaskRect.left, mFirstTaskRect.top, null); + startAlternateRecentsActivity(opts); + } else { + ActivityOptions opts = ActivityOptions.makeCustomAnimation(mContext, + R.anim.recents_from_launcher_enter, + R.anim.recents_from_launcher_exit); + startAlternateRecentsActivity(opts); + } + } + + /** Starts the recents activity */ + void startAlternateRecentsActivity(ActivityOptions opts) { + Intent intent = new Intent(sToggleRecentsAction); + intent.setClassName(sRecentsPackage, sRecentsActivity); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK + | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); + if (opts != null) { + mContext.startActivityAsUser(intent, opts.toBundle(), new UserHandle( + UserHandle.USER_CURRENT)); + } else { + mContext.startActivityAsUser(intent, new UserHandle(UserHandle.USER_CURRENT)); + } + } + @Override public void preloadRecentTasksList() { - if (DEBUG) Log.d(TAG, "preloading recents"); - Intent intent = new Intent(RecentsActivity.PRELOAD_INTENT); - intent.setClassName("com.android.systemui", - "com.android.systemui.recent.RecentsPreloadReceiver"); - mContext.sendBroadcastAsUser(intent, new UserHandle(UserHandle.USER_CURRENT)); + if (mUseAlternateRecents) { + Log.d(TAG, "[RecentsComponent|preloadRecents]"); + } else { + Intent intent = new Intent(RecentsActivity.PRELOAD_INTENT); + intent.setClassName("com.android.systemui", + "com.android.systemui.recent.RecentsPreloadReceiver"); + mContext.sendBroadcastAsUser(intent, new UserHandle(UserHandle.USER_CURRENT)); - RecentTasksLoader.getInstance(mContext).preloadFirstTask(); + RecentTasksLoader.getInstance(mContext).preloadFirstTask(); + } } @Override public void cancelPreloadingRecentTasksList() { - if (DEBUG) Log.d(TAG, "cancel preloading recents"); - Intent intent = new Intent(RecentsActivity.CANCEL_PRELOAD_INTENT); - intent.setClassName("com.android.systemui", - "com.android.systemui.recent.RecentsPreloadReceiver"); - mContext.sendBroadcastAsUser(intent, new UserHandle(UserHandle.USER_CURRENT)); + if (mUseAlternateRecents) { + Log.d(TAG, "[RecentsComponent|cancelPreload]"); + } else { + Intent intent = new Intent(RecentsActivity.CANCEL_PRELOAD_INTENT); + intent.setClassName("com.android.systemui", + "com.android.systemui.recent.RecentsPreloadReceiver"); + mContext.sendBroadcastAsUser(intent, new UserHandle(UserHandle.USER_CURRENT)); - RecentTasksLoader.getInstance(mContext).cancelPreloadingFirstTask(); + RecentTasksLoader.getInstance(mContext).cancelPreloadingFirstTask(); + } } @Override public void closeRecents() { - if (DEBUG) Log.d(TAG, "closing recents panel"); - Intent intent = new Intent(RecentsActivity.CLOSE_RECENTS_INTENT); - intent.setPackage("com.android.systemui"); - mContext.sendBroadcastAsUser(intent, new UserHandle(UserHandle.USER_CURRENT)); + if (mUseAlternateRecents) { + Log.d(TAG, "[RecentsComponent|closeRecents]"); + } else { + Intent intent = new Intent(RecentsActivity.CLOSE_RECENTS_INTENT); + intent.setPackage("com.android.systemui"); + mContext.sendBroadcastAsUser(intent, new UserHandle(UserHandle.USER_CURRENT)); + + RecentTasksLoader.getInstance(mContext).cancelPreloadingFirstTask(); + } } } diff --git a/packages/SystemUI/src/com/android/systemui/recents/Console.java b/packages/SystemUI/src/com/android/systemui/recents/Console.java new file mode 100644 index 0000000..b3d9ccf --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/recents/Console.java @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2014 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; + + +import android.content.Context; +import android.util.Log; +import android.view.MotionEvent; +import android.widget.Toast; + +public class Console { + // Colors + public static final String AnsiReset = "\u001B[0m"; + public static final String AnsiBlack = "\u001B[30m"; + public static final String AnsiRed = "\u001B[31m"; // SystemUIHandshake + public static final String AnsiGreen = "\u001B[32m"; // MeasureAndLayout + public static final String AnsiYellow = "\u001B[33m"; // SynchronizeViewsWithModel + public static final String AnsiBlue = "\u001B[34m"; // TouchEvents + public static final String AnsiPurple = "\u001B[35m"; // Draw + public static final String AnsiCyan = "\u001B[36m"; // ClickEvents + public static final String AnsiWhite = "\u001B[37m"; + + /** Logs a key */ + public static void log(String key) { + Console.log(true, key, "", AnsiReset); + } + + /** Logs a conditioned key */ + public static void log(boolean condition, String key) { + if (condition) { + Console.log(condition, key, "", AnsiReset); + } + } + + /** Logs a key in a specific color */ + public static void log(boolean condition, String key, Object data) { + if (condition) { + Console.log(condition, key, data, AnsiReset); + } + } + + /** Logs a key with data in a specific color */ + public static void log(boolean condition, String key, Object data, String color) { + if (condition) { + System.out.println(color + key + AnsiReset + " " + data.toString()); + } + } + + /** Logs an error */ + public static void logError(Context context, String msg) { + Toast.makeText(context, msg, Toast.LENGTH_SHORT).show(); + Log.e("Recents", msg); + } + + /** Logs a divider bar */ + public static void logDivider(boolean condition) { + if (condition) { + System.out.println("==== [" + System.currentTimeMillis() + + "] ============================================================"); + } + } + + /** Returns the stringified MotionEvent action */ + public static String motionEventActionToString(int action) { + switch (action) { + case MotionEvent.ACTION_DOWN: + return "Down"; + case MotionEvent.ACTION_UP: + return "Up"; + case MotionEvent.ACTION_MOVE: + return "Move"; + case MotionEvent.ACTION_CANCEL: + return "Cancel"; + case MotionEvent.ACTION_POINTER_DOWN: + return "Pointer Down"; + case MotionEvent.ACTION_POINTER_UP: + return "Pointer Up"; + default: + return "" + action; + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/recents/Constants.java b/packages/SystemUI/src/com/android/systemui/recents/Constants.java new file mode 100644 index 0000000..aeae4ab --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/recents/Constants.java @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2014 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; + +/** + * Constants + * XXX: We are going to move almost all of these into a resource. + */ +public class Constants { + public static class DebugFlags { + // Enable this with any other debug flag to see more info + public static final boolean Verbose = false; + + public static class App { + public static final boolean EnableTaskFiltering = false; + public static final boolean EnableTaskStackClipping = false; + public static final boolean EnableBackgroundTaskLoading = true; + public static final boolean ForceDisableBackgroundCache = false; + public static final boolean TaskDataLoader = false; + public static final boolean SystemUIHandshake = false; + public static final boolean TimeSystemCalls = false; + } + + public static class UI { + public static final boolean Draw = false; + public static final boolean ClickEvents = false; + public static final boolean TouchEvents = false; + public static final boolean MeasureAndLayout = false; + public static final boolean Clipping = false; + public static final boolean HwLayers = true; + } + + public static class TaskStack { + public static final boolean SynchronizeViewsWithModel = false; + } + + public static class ViewPool { + public static final boolean PoolCallbacks = false; + } + } + + public static class Values { + public static class Window { + public static final float DarkBackgroundDim = 0.5f; + public static final float BackgroundDim = 0.35f; + } + + public static class RecentsTaskLoader { + // XXX: This should be calculated on the first load + public static final int PreloadFirstTasksCount = 5; + public static final int TaskEntryMultiplier = 1; + } + + public static class TaskStackView { + public static class Animation { + public static final int TaskRemovedReshuffleDuration = 200; + public static final int SnapScrollBackDuration = 650; + public static final int SwipeDismissDuration = 350; + public static final int SwipeSnapBackDuration = 350; + } + + // The padding will be applied to the smallest dimension, and then applied to all sides + public static final float StackPaddingPct = 0.15f; + // The overlap height relative to the task height + public static final float StackOverlapPct = 0.65f; + // The height of the peek space relative to the stack height + public static final float StackPeekHeightPct = 0.1f; + // The min scale of the last card in the peek area + public static final float StackPeekMinScale = 0.9f; + // The number of cards we see in the peek space + public static final int StackPeekNumCards = 3; + } + + public static class TaskView { + public static class Animation { + public static final int TaskDataUpdatedFadeDuration = 250; + public static final int TaskIconCircularClipInDuration = 225; + public static final int TaskIconCircularClipOutDuration = 85; + } + + public static final boolean AnimateFrontTaskIconOnEnterRecents = true; + public static final boolean AnimateFrontTaskIconOnLeavingRecents = true; + public static final boolean AnimateFrontTaskIconOnLeavingUseClip = false; + public static final boolean DrawColoredTaskBars = false; + public static final boolean UseRoundedCorners = true; + public static final float RoundedCornerRadiusDps = 3; + + public static final float TaskBarHeightDps = 54; + public static final float TaskIconSizeDps = 60; + } + } + + // UNMIGRATED CONSTANTS: + + /** Determines whether to layout the stack vertically in landscape mode */ + public static final boolean LANDSCAPE_LAYOUT_VERTICAL_STACK = true; +}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/recents/RecentsActivity.java b/packages/SystemUI/src/com/android/systemui/recents/RecentsActivity.java new file mode 100644 index 0000000..d050847 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/recents/RecentsActivity.java @@ -0,0 +1,176 @@ +/* + * Copyright (C) 2014 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; + +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.WindowManager; +import android.widget.FrameLayout; +import com.android.systemui.recents.model.SpaceNode; +import com.android.systemui.recents.model.TaskStack; +import com.android.systemui.recents.views.RecentsView; +import com.android.systemui.R; + +import java.util.ArrayList; + + +/* Activity */ +public class RecentsActivity extends Activity { + FrameLayout mContainerView; + RecentsView mRecentsView; + View mEmptyView; + boolean mVisible; + + /** Updates the set of recent tasks */ + void updateRecentsTasks() { + RecentsTaskLoader loader = RecentsTaskLoader.getInstance(); + SpaceNode root = loader.reload(this, Constants.Values.RecentsTaskLoader.PreloadFirstTasksCount); + ArrayList<TaskStack> stacks = root.getStacks(); + if (!stacks.isEmpty()) { + // XXX: We just replace the root every time for now, we will change this in the future + mRecentsView.setBSP(root); + } + + // Add the default no-recents layout + if (stacks.size() == 1 && stacks.get(0).getTaskCount() == 0) { + mEmptyView.setVisibility(View.VISIBLE); + + // Dim the background even more + WindowManager.LayoutParams wlp = getWindow().getAttributes(); + wlp.dimAmount = Constants.Values.Window.DarkBackgroundDim; + getWindow().setAttributes(wlp); + getWindow().addFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND); + } else { + mEmptyView.setVisibility(View.GONE); + } + } + + /** Dismisses recents if we are already visible and the intent is to toggle the recents view */ + boolean dismissRecentsIfVisible(Intent intent) { + if ("com.android.systemui.recents.TOGGLE_RECENTS".equals(intent.getAction())) { + if (mVisible) { + if (!mRecentsView.launchFirstTask()) { + finish(); + } + return true; + } + } + return false; + } + + /** Called with the activity is first created. */ + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + Console.logDivider(Constants.DebugFlags.App.SystemUIHandshake); + Console.log(Constants.DebugFlags.App.SystemUIHandshake, "[RecentsActivity|onCreate]", + getIntent().getAction() + " visible: " + mVisible, Console.AnsiRed); + + // Initialize the loader and the configuration + RecentsTaskLoader.initialize(this); + RecentsConfiguration.reinitialize(this); + + // Dismiss recents if it is visible and we are toggling + if (dismissRecentsIfVisible(getIntent())) return; + + // Set the background dim + WindowManager.LayoutParams wlp = getWindow().getAttributes(); + wlp.dimAmount = Constants.Values.Window.BackgroundDim; + getWindow().setAttributes(wlp); + getWindow().addFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND); + + // Create the view hierarchy + mRecentsView = new RecentsView(this); + mRecentsView.setLayoutParams(new FrameLayout.LayoutParams( + FrameLayout.LayoutParams.MATCH_PARENT, + FrameLayout.LayoutParams.MATCH_PARENT)); + + // Create the empty view + LayoutInflater inflater = LayoutInflater.from(this); + mEmptyView = inflater.inflate(R.layout.recents_empty, mContainerView, false); + + mContainerView = new FrameLayout(this); + mContainerView.addView(mRecentsView); + mContainerView.addView(mEmptyView); + setContentView(mContainerView); + + // Update the recent tasks + updateRecentsTasks(); + } + + @Override + protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + Console.logDivider(Constants.DebugFlags.App.SystemUIHandshake); + Console.log(Constants.DebugFlags.App.SystemUIHandshake, "[RecentsActivity|onNewIntent]", + intent.getAction() + " visible: " + mVisible, Console.AnsiRed); + + // Dismiss recents if it is visible and we are toggling + if (dismissRecentsIfVisible(intent)) return; + + // Initialize the loader and the configuration + RecentsTaskLoader.initialize(this); + RecentsConfiguration.reinitialize(this); + + // Update the recent tasks + updateRecentsTasks(); + } + + @Override + protected void onStart() { + Console.log(Constants.DebugFlags.App.SystemUIHandshake, "[RecentsActivity|onStart]", "", + Console.AnsiRed); + super.onStart(); + mVisible = true; + } + + @Override + protected void onResume() { + Console.log(Constants.DebugFlags.App.SystemUIHandshake, "[RecentsActivity|onResume]", "", + Console.AnsiRed); + super.onResume(); + } + + @Override + protected void onPause() { + Console.log(Constants.DebugFlags.App.SystemUIHandshake, "[RecentsActivity|onPause]", "", + Console.AnsiRed); + super.onPause(); + + // Stop the loader when we leave Recents + RecentsTaskLoader loader = RecentsTaskLoader.getInstance(); + loader.stopLoader(); + } + + @Override + protected void onStop() { + Console.log(Constants.DebugFlags.App.SystemUIHandshake, "[RecentsActivity|onStop]", "", + Console.AnsiRed); + super.onStop(); + mVisible = false; + } + + @Override + public void onBackPressed() { + if (!mRecentsView.unfilterFilteredStacks()) { + super.onBackPressed(); + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/recents/RecentsConfiguration.java b/packages/SystemUI/src/com/android/systemui/recents/RecentsConfiguration.java new file mode 100644 index 0000000..f3881ae --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/recents/RecentsConfiguration.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2014 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; + +import android.content.Context; +import android.content.res.Configuration; +import android.graphics.Rect; +import android.util.DisplayMetrics; +import android.util.TypedValue; + + +/** A static Recents configuration for the current context + * NOTE: We should not hold any references to a Context from a static instance */ +public class RecentsConfiguration { + static RecentsConfiguration sInstance; + + DisplayMetrics mDisplayMetrics; + + public boolean layoutVerticalStack; + public Rect systemInsets = new Rect(); + + /** Private constructor */ + private RecentsConfiguration() {} + + /** Updates the configuration to the current context */ + public static RecentsConfiguration reinitialize(Context context) { + if (sInstance == null) { + sInstance = new RecentsConfiguration(); + } + sInstance.update(context); + return sInstance; + } + + /** Returns the current recents configuration */ + public static RecentsConfiguration getInstance() { + return sInstance; + } + + /** Updates the state, given the specified context */ + void update(Context context) { + mDisplayMetrics = context.getResources().getDisplayMetrics(); + + boolean isPortrait = context.getResources().getConfiguration().orientation == + Configuration.ORIENTATION_PORTRAIT; + layoutVerticalStack = isPortrait || Constants.LANDSCAPE_LAYOUT_VERTICAL_STACK; + } + + public void updateSystemInsets(Rect insets) { + systemInsets.set(insets); + } + + /** Converts from DPs to PXs */ + public int pxFromDp(float size) { + return Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, + size, mDisplayMetrics)); + } + /** Converts from SPs to PXs */ + public int pxFromSp(float size) { + return Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, + size, mDisplayMetrics)); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/recents/RecentsService.java b/packages/SystemUI/src/com/android/systemui/recents/RecentsService.java new file mode 100644 index 0000000..522ab0f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/recents/RecentsService.java @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2014 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; + +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.graphics.Rect; +import android.os.Bundle; +import android.os.Handler; +import android.os.IBinder; +import android.os.Message; +import android.os.Messenger; +import android.os.RemoteException; +import com.android.systemui.recents.model.TaskStack; +import com.android.systemui.recents.views.TaskStackView; +import com.android.systemui.recents.views.TaskViewTransform; + + +/* Service */ +public class RecentsService extends Service { + // XXX: This should be getting the message from recents definition + final static int MSG_UPDATE_RECENTS_FOR_CONFIGURATION = 0; + + class MessageHandler extends Handler { + @Override + public void handleMessage(Message msg) { + Console.log(Constants.DebugFlags.App.SystemUIHandshake, "[RecentsService|handleMessage]", msg); + if (msg.what == MSG_UPDATE_RECENTS_FOR_CONFIGURATION) { + Context context = RecentsService.this; + RecentsTaskLoader.initialize(context); + RecentsConfiguration.reinitialize(context); + + try { + Bundle data = msg.getData(); + Rect windowRect = (Rect) data.getParcelable("windowRect"); + Rect systemInsets = (Rect) data.getParcelable("systemInsets"); + RecentsConfiguration.getInstance().updateSystemInsets(systemInsets); + + // Create a dummy task stack & compute the rect for the thumbnail to animate to + TaskStack stack = new TaskStack(context); + TaskStackView tsv = new TaskStackView(context, stack); + tsv.computeRects(windowRect.width(), windowRect.height() - systemInsets.top); + tsv.boundScroll(); + TaskViewTransform transform = tsv.getStackTransform(0); + + data.putParcelable("taskRect", transform.rect); + Message reply = Message.obtain(null, MSG_UPDATE_RECENTS_FOR_CONFIGURATION, 0, 0); + reply.setData(data); + msg.replyTo.send(reply); + } catch (RemoteException re) { + re.printStackTrace(); + } + } + } + } + + Messenger mMessenger = new Messenger(new MessageHandler()); + + @Override + public void onCreate() { + Console.log(Constants.DebugFlags.App.SystemUIHandshake, "[RecentsService|onCreate]"); + super.onCreate(); + } + + @Override + public IBinder onBind(Intent intent) { + Console.log(Constants.DebugFlags.App.SystemUIHandshake, "[RecentsService|onBind]"); + return mMessenger.getBinder(); + } + + @Override + public boolean onUnbind(Intent intent) { + Console.log(Constants.DebugFlags.App.SystemUIHandshake, "[RecentsService|onUnbind]"); + return super.onUnbind(intent); + } + + @Override + public void onRebind(Intent intent) { + Console.log(Constants.DebugFlags.App.SystemUIHandshake, "[RecentsService|onRebind]"); + super.onRebind(intent); + } + + @Override + public void onDestroy() { + Console.log(Constants.DebugFlags.App.SystemUIHandshake, "[RecentsService|onDestroy]"); + super.onDestroy(); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/recents/RecentsTaskLoader.java b/packages/SystemUI/src/com/android/systemui/recents/RecentsTaskLoader.java new file mode 100644 index 0000000..c303ca7 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/recents/RecentsTaskLoader.java @@ -0,0 +1,463 @@ +/* + * Copyright (C) 2014 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; + +import android.app.ActivityManager; +import android.content.Context; +import android.content.pm.ActivityInfo; +import android.content.pm.PackageManager; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.UserHandle; +import android.util.LruCache; +import com.android.systemui.recents.model.SpaceNode; +import com.android.systemui.recents.model.Task; +import com.android.systemui.recents.model.TaskStack; + +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.ConcurrentLinkedQueue; + + +/** A bitmap load queue */ +class TaskResourceLoadQueue { + ConcurrentLinkedQueue<Task> mQueue = new ConcurrentLinkedQueue<Task>(); + + Task nextTask() { + Console.log(Constants.DebugFlags.App.TaskDataLoader, " [TaskResourceLoadQueue|nextTask]"); + return mQueue.poll(); + } + + void addTask(Task t) { + Console.log(Constants.DebugFlags.App.TaskDataLoader, " [TaskResourceLoadQueue|addTask]"); + if (!mQueue.contains(t)) { + mQueue.add(t); + } + synchronized(this) { + notifyAll(); + } + } + + void removeTask(Task t) { + Console.log(Constants.DebugFlags.App.TaskDataLoader, " [TaskResourceLoadQueue|removeTask]"); + mQueue.remove(t); + } + + void clearTasks() { + Console.log(Constants.DebugFlags.App.TaskDataLoader, " [TaskResourceLoadQueue|clearTasks]"); + mQueue.clear(); + } + + boolean isEmpty() { + return mQueue.isEmpty(); + } +} + +/* Task resource loader */ +class TaskResourceLoader implements Runnable { + Context mContext; + HandlerThread mLoadThread; + Handler mLoadThreadHandler; + Handler mMainThreadHandler; + + TaskResourceLoadQueue mLoadQueue; + DrawableLruCache mIconCache; + BitmapLruCache mThumbnailCache; + boolean mCancelled; + + /** Constructor, creates a new loading thread that loads task resources in the background */ + public TaskResourceLoader(TaskResourceLoadQueue loadQueue, DrawableLruCache iconCache, + BitmapLruCache thumbnailCache) { + mLoadQueue = loadQueue; + mIconCache = iconCache; + mThumbnailCache = thumbnailCache; + mMainThreadHandler = new Handler(); + mLoadThread = new HandlerThread("Recents-TaskResourceLoader"); + mLoadThread.setPriority(Thread.NORM_PRIORITY - 1); + mLoadThread.start(); + mLoadThreadHandler = new Handler(mLoadThread.getLooper()); + mLoadThreadHandler.post(this); + } + + /** Restarts the loader thread */ + void start(Context context) { + Console.log(Constants.DebugFlags.App.TaskDataLoader, "[TaskResourceLoader|start]"); + mContext = context; + mCancelled = false; + // Notify the load thread to start loading + synchronized(mLoadThread) { + mLoadThread.notifyAll(); + } + } + + /** Requests the loader thread to stop after the current iteration */ + void stop() { + Console.log(Constants.DebugFlags.App.TaskDataLoader, "[TaskResourceLoader|stop]"); + // Mark as cancelled for the thread to pick up + mCancelled = true; + } + + @Override + public void run() { + while (true) { + Console.log(Constants.DebugFlags.App.TaskDataLoader, + "[TaskResourceLoader|run|" + Thread.currentThread().getId() + "]"); + if (mCancelled) { + // We have to unset the context here, since the background thread may be using it + // when we call stop() + mContext = null; + // If we are cancelled, then wait until we are started again + synchronized(mLoadThread) { + try { + Console.log(Constants.DebugFlags.App.TaskDataLoader, + "[TaskResourceLoader|waitOnLoadThreadCancelled]"); + mLoadThread.wait(); + } catch (InterruptedException ie) { + ie.printStackTrace(); + } + } + } else { + // Load the next item from the queue + final Task t = mLoadQueue.nextTask(); + if (t != null) { + try { + Drawable cachedIcon = mIconCache.get(t); + Bitmap cachedThumbnail = mThumbnailCache.get(t); + Console.log(Constants.DebugFlags.App.TaskDataLoader, + " [TaskResourceLoader|load]", + t + " icon: " + cachedIcon + " thumbnail: " + cachedThumbnail); + // Load the icon + if (cachedIcon == null) { + PackageManager pm = mContext.getPackageManager(); + ActivityInfo info = pm.getActivityInfo(t.intent.getComponent(), + PackageManager.GET_META_DATA); + Drawable icon = info.loadIcon(pm); + if (!mCancelled) { + Console.log(Constants.DebugFlags.App.TaskDataLoader, + " [TaskResourceLoader|loadIcon]", + icon); + t.icon = icon; + mIconCache.put(t, icon); + } + } + // Load the thumbnail + if (cachedThumbnail == null) { + ActivityManager am = (ActivityManager) + mContext.getSystemService(Context.ACTIVITY_SERVICE); + Bitmap thumbnail = am.getTaskTopThumbnail(t.id); + if (!mCancelled) { + if (thumbnail != null) { + Console.log(Constants.DebugFlags.App.TaskDataLoader, + " [TaskResourceLoader|loadThumbnail]", + thumbnail); + t.thumbnail = thumbnail; + mThumbnailCache.put(t, thumbnail); + } else { + Console.logError(mContext, + "Failed to load task top thumbnail for: " + + t.intent.getComponent().getPackageName()); + } + } + } + if (!mCancelled) { + // Notify that the task data has changed + mMainThreadHandler.post(new Runnable() { + @Override + public void run() { + t.notifyTaskDataChanged(); + } + }); + } + } catch (PackageManager.NameNotFoundException ne) { + ne.printStackTrace(); + } + } + + // If there are no other items in the list, then just wait until something is added + if (!mCancelled && mLoadQueue.isEmpty()) { + synchronized(mLoadQueue) { + try { + Console.log(Constants.DebugFlags.App.TaskDataLoader, + "[TaskResourceLoader|waitOnLoadQueue]"); + mLoadQueue.wait(); + } catch (InterruptedException ie) { + ie.printStackTrace(); + } + } + } + } + } + } +} + +/** The drawable cache */ +class DrawableLruCache extends LruCache<Task, Drawable> { + public DrawableLruCache(int cacheSize) { + super(cacheSize); + } + + @Override + protected int sizeOf(Task t, Drawable d) { + // The cache size will be measured in kilobytes rather than number of items + // NOTE: this isn't actually correct, as the icon may be smaller + int maxBytes = (d.getIntrinsicWidth() * d.getIntrinsicHeight() * 4); + return maxBytes / 1024; + } +} + +/** The bitmap cache */ +class BitmapLruCache extends LruCache<Task, Bitmap> { + public BitmapLruCache(int cacheSize) { + super(cacheSize); + } + + @Override + protected int sizeOf(Task t, Bitmap bitmap) { + // The cache size will be measured in kilobytes rather than number of items + return bitmap.getByteCount() / 1024; + } +} + +/* Recents task loader + * NOTE: We should not hold any references to a Context from a static instance */ +public class RecentsTaskLoader { + static RecentsTaskLoader sInstance; + + DrawableLruCache mIconCache; + BitmapLruCache mThumbnailCache; + TaskResourceLoadQueue mLoadQueue; + TaskResourceLoader mLoader; + + BitmapDrawable mDefaultIcon; + Bitmap mDefaultThumbnail; + + /** Private Constructor */ + private RecentsTaskLoader(Context context) { + int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024); + int iconCacheSize = Constants.DebugFlags.App.ForceDisableBackgroundCache ? 1 : maxMemory / 16; + int thumbnailCacheSize = Constants.DebugFlags.App.ForceDisableBackgroundCache ? 1 : maxMemory / 8; + Console.log(Constants.DebugFlags.App.SystemUIHandshake, + "[RecentsTaskLoader|init]", "thumbnailCache: " + thumbnailCacheSize + + " iconCache: " + iconCacheSize); + mLoadQueue = new TaskResourceLoadQueue(); + mIconCache = new DrawableLruCache(iconCacheSize); + mThumbnailCache = new BitmapLruCache(thumbnailCacheSize); + mLoader = new TaskResourceLoader(mLoadQueue, mIconCache, mThumbnailCache); + + // Create the default assets + Bitmap icon = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888); + mDefaultThumbnail = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888); + Canvas c = new Canvas(); + c.setBitmap(icon); + c.drawColor(0x00000000); + c.setBitmap(mDefaultThumbnail); + c.drawColor(0x00000000); + c.setBitmap(null); + mDefaultIcon = new BitmapDrawable(context.getResources(), icon); + Console.log(Constants.DebugFlags.App.TaskDataLoader, + "[RecentsTaskLoader|defaultBitmaps]", + "icon: " + mDefaultIcon + " thumbnail: " + mDefaultThumbnail, Console.AnsiRed); + } + + /** Initializes the recents task loader */ + public static RecentsTaskLoader initialize(Context context) { + if (sInstance == null) { + sInstance = new RecentsTaskLoader(context); + } + return sInstance; + } + + /** Returns the current recents task loader */ + public static RecentsTaskLoader getInstance() { + return sInstance; + } + + /** Reload the set of recent tasks */ + SpaceNode reload(Context context, int preloadCount) { + Console.log(Constants.DebugFlags.App.SystemUIHandshake, "[RecentsTaskLoader|reload]"); + TaskStack stack = new TaskStack(context); + SpaceNode root = new SpaceNode(context); + root.setStack(stack); + try { + long t1 = System.currentTimeMillis(); + + PackageManager pm = context.getPackageManager(); + ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); + + // Get the recent tasks + List<ActivityManager.RecentTaskInfo> tasks = am.getRecentTasksForUser(25, + ActivityManager.RECENT_IGNORE_UNAVAILABLE, UserHandle.CURRENT.getIdentifier()); + Collections.reverse(tasks); + Console.log(Constants.DebugFlags.App.TimeSystemCalls, + "[RecentsTaskLoader|getRecentTasks]", + "" + (System.currentTimeMillis() - t1) + "ms"); + Console.log(Constants.DebugFlags.App.SystemUIHandshake, + "[RecentsTaskLoader|tasks]", "" + tasks.size()); + + // Remove home/recents tasks + Iterator<ActivityManager.RecentTaskInfo> iter = tasks.iterator(); + while (iter.hasNext()) { + ActivityManager.RecentTaskInfo t = iter.next(); + + // Skip tasks in the home stack + if (am.isInHomeStack(t.persistentId)) { + iter.remove(); + continue; + } + // Skip tasks from this Recents package + if (t.baseIntent.getComponent().getPackageName().equals(context.getPackageName())) { + iter.remove(); + continue; + } + } + + // Add each task to the task stack + t1 = System.currentTimeMillis(); + int taskCount = tasks.size(); + for (int i = 0; i < taskCount; i++) { + ActivityManager.RecentTaskInfo t = tasks.get(i); + + // Load the label, icon and thumbnail + ActivityInfo info = pm.getActivityInfo(t.baseIntent.getComponent(), + PackageManager.GET_META_DATA); + String title = info.loadLabel(pm).toString(); + Drawable icon = null; + Bitmap thumbnail = null; + Task task; + if (i >= (taskCount - preloadCount) || !Constants.DebugFlags.App.EnableBackgroundTaskLoading) { + Console.log(Constants.DebugFlags.App.SystemUIHandshake, + "[RecentsTaskLoader|preloadTask]", + "i: " + i + " task: " + t.baseIntent.getComponent().getPackageName()); + icon = info.loadIcon(pm); + thumbnail = am.getTaskTopThumbnail(t.id); + for (int j = 0; j < Constants.Values.RecentsTaskLoader.TaskEntryMultiplier; j++) { + Console.log(Constants.DebugFlags.App.SystemUIHandshake, + " [RecentsTaskLoader|task]", t.baseIntent.getComponent().getPackageName()); + task = new Task(t.persistentId, t.baseIntent, title, icon, thumbnail); + if (Constants.DebugFlags.App.EnableBackgroundTaskLoading) { + if (thumbnail != null) mThumbnailCache.put(task, thumbnail); + if (icon != null) { + mIconCache.put(task, icon); + } + } + stack.addTask(task); + } + } else { + for (int j = 0; j < Constants.Values.RecentsTaskLoader.TaskEntryMultiplier; j++) { + Console.log(Constants.DebugFlags.App.SystemUIHandshake, + " [RecentsTaskLoader|task]", t.baseIntent.getComponent().getPackageName()); + task = new Task(t.persistentId, t.baseIntent, title, null, null); + stack.addTask(task); + } + } + + /* + if (stacks.containsKey(t.stackId)) { + builder = stacks.get(t.stackId); + } else { + builder = new TaskStackBuilder(); + stacks.put(t.stackId, builder); + } + */ + } + Console.log(Constants.DebugFlags.App.TimeSystemCalls, + "[RecentsTaskLoader|getAllTaskTopThumbnail]", + "" + (System.currentTimeMillis() - t1) + "ms"); + + /* + // Get all the stacks + t1 = System.currentTimeMillis(); + List<ActivityManager.StackInfo> stackInfos = ams.getAllStackInfos(); + Console.log(Constants.DebugFlags.App.TimeSystemCalls, "[RecentsTaskLoader|getAllStackInfos]", "" + (System.currentTimeMillis() - t1) + "ms"); + Console.log(Constants.DebugFlags.App.SystemUIHandshake, "[RecentsTaskLoader|stacks]", "" + tasks.size()); + for (ActivityManager.StackInfo s : stackInfos) { + Console.log(Constants.DebugFlags.App.SystemUIHandshake, " [RecentsTaskLoader|stack]", s.toString()); + if (stacks.containsKey(s.stackId)) { + stacks.get(s.stackId).setRect(s.bounds); + } + } + */ + } catch (Exception e) { + e.printStackTrace(); + } + mLoader.start(context); + return root; + } + + /** Acquires the task resource data from the pool. + * XXX: Move this into Task? */ + public void loadTaskData(Task t) { + if (Constants.DebugFlags.App.EnableBackgroundTaskLoading) { + t.icon = mIconCache.get(t); + t.thumbnail = mThumbnailCache.get(t); + + Console.log(Constants.DebugFlags.App.TaskDataLoader, "[RecentsTaskLoader|loadTask]", + t + " icon: " + t.icon + " thumbnail: " + t.thumbnail); + + boolean requiresLoad = false; + if (t.icon == null) { + t.icon = mDefaultIcon; + requiresLoad = true; + } + if (t.thumbnail == null) { + t.thumbnail = mDefaultThumbnail; + requiresLoad = true; + } + if (requiresLoad) { + mLoadQueue.addTask(t); + } + } + } + + /** Releases the task resource data back into the pool. + * XXX: Move this into Task? */ + public void unloadTaskData(Task t) { + if (Constants.DebugFlags.App.EnableBackgroundTaskLoading) { + Console.log(Constants.DebugFlags.App.TaskDataLoader, + "[RecentsTaskLoader|unloadTask]", t); + mLoadQueue.removeTask(t); + t.icon = mDefaultIcon; + t.thumbnail = mDefaultThumbnail; + } + } + + /** Completely removes the resource data from the pool. + * XXX: Move this into Task? */ + public void deleteTaskData(Task t) { + if (Constants.DebugFlags.App.EnableBackgroundTaskLoading) { + Console.log(Constants.DebugFlags.App.TaskDataLoader, + "[RecentsTaskLoader|deleteTask]", t); + mLoadQueue.removeTask(t); + mThumbnailCache.remove(t); + mIconCache.remove(t); + } + t.icon = mDefaultIcon; + t.thumbnail = mDefaultThumbnail; + } + + /** Stops the task loader */ + void stopLoader() { + Console.log(Constants.DebugFlags.App.TaskDataLoader, "[RecentsTaskLoader|stopLoader]"); + mLoader.stop(); + mLoadQueue.clearTasks(); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/recents/Utilities.java b/packages/SystemUI/src/com/android/systemui/recents/Utilities.java new file mode 100644 index 0000000..33e4246 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/recents/Utilities.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2014 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; + +import android.graphics.Rect; + +/* Common code */ +public class Utilities { + public static final Rect tmpRect = new Rect(); + public static final Rect tmpRect2 = new Rect(); + + /** Scales a rect about its centroid */ + public static void scaleRectAboutCenter(Rect r, float scale) { + if (scale != 1.0f) { + int cx = r.centerX(); + int cy = r.centerY(); + r.offset(-cx, -cy); + r.left = (int) (r.left * scale + 0.5f); + r.top = (int) (r.top * scale + 0.5f); + r.right = (int) (r.right * scale + 0.5f); + r.bottom = (int) (r.bottom * scale + 0.5f); + r.offset(cx, cy); + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/recents/model/SpaceNode.java b/packages/SystemUI/src/com/android/systemui/recents/model/SpaceNode.java new file mode 100644 index 0000000..5893abc --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/recents/model/SpaceNode.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2014 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 android.content.Context; + +import java.util.ArrayList; + + +/** + * The full recents space is partitioned using a BSP into various nodes that define where task + * stacks should be placed. + */ +public class SpaceNode { + Context mContext; + + SpaceNode mStartNode; + SpaceNode mEndNode; + + TaskStack mStack; + + public SpaceNode(Context context) { + mContext = context; + } + + /** Sets the current stack for this space node */ + public void setStack(TaskStack stack) { + mStack = stack; + } + + /** Returns the task stack (not null if this is a leaf) */ + TaskStack getStack() { + return mStack; + } + + /** Returns whether this is a leaf node */ + boolean isLeafNode() { + return (mStartNode == null) && (mEndNode == null); + } + + /** Returns all the descendent task stacks */ + private void getStacksRec(ArrayList<TaskStack> stacks) { + if (isLeafNode()) { + stacks.add(mStack); + } else { + mStartNode.getStacksRec(stacks); + mEndNode.getStacksRec(stacks); + } + } + public ArrayList<TaskStack> getStacks() { + ArrayList<TaskStack> stacks = new ArrayList<TaskStack>(); + getStacksRec(stacks); + return stacks; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/recents/model/SpaceNodeCallbacks.java b/packages/SystemUI/src/com/android/systemui/recents/model/SpaceNodeCallbacks.java new file mode 100644 index 0000000..31b02e7 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/recents/model/SpaceNodeCallbacks.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2014 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 android.graphics.Rect; + + +/* BSP node callbacks */ +public interface SpaceNodeCallbacks { + /** Notifies when a node is added */ + public void onSpaceNodeAdded(SpaceNode node); + /** Notifies when a node is measured */ + public void onSpaceNodeMeasured(SpaceNode node, Rect rect); +}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/recents/model/Task.java b/packages/SystemUI/src/com/android/systemui/recents/model/Task.java new file mode 100644 index 0000000..9b03c5d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/recents/model/Task.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2014 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 android.content.Intent; +import android.graphics.Bitmap; +import android.graphics.drawable.Drawable; +import com.android.systemui.recents.Constants; + + +/** + * A task represents the top most task in the system's task stack. + */ +public class Task { + public final int id; + public final Intent intent; + public String title; + public Drawable icon; + public Bitmap thumbnail; + + TaskCallbacks mCb; + + public Task(int id, Intent intent, String activityTitle, Drawable icon, Bitmap thumbnail) { + this.id = id; + this.intent = intent; + this.title = activityTitle; + this.icon = icon; + this.thumbnail = thumbnail; + } + + /** Set the callbacks */ + public void setCallbacks(TaskCallbacks cb) { + mCb = cb; + } + + /** Notifies the callback listeners that this task's data has changed */ + public void notifyTaskDataChanged() { + if (mCb != null) { + mCb.onTaskDataChanged(this); + } + } + + @Override + public boolean equals(Object o) { + // If we have multiple task entries for the same task, then we do the simple object + // equality check + if (Constants.Values.RecentsTaskLoader.TaskEntryMultiplier > 1) { + return super.equals(o); + } + + // Otherwise, check that the id and intent match (the other fields can be asynchronously + // loaded and is unsuitable to testing the identity of this Task) + Task t = (Task) o; + return (id == t.id) && + (intent.equals(t.intent)); + } + + @Override + public String toString() { + return "Task: " + intent.getComponent().getPackageName() + " [" + super.toString() + "]"; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/recents/model/TaskCallbacks.java b/packages/SystemUI/src/com/android/systemui/recents/model/TaskCallbacks.java new file mode 100644 index 0000000..169f56c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/recents/model/TaskCallbacks.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2014 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; + +/* Task callbacks */ +public interface TaskCallbacks { + /* Notifies when a task's data has been updated */ + public void onTaskDataChanged(Task task); +}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/recents/model/TaskStack.java b/packages/SystemUI/src/com/android/systemui/recents/model/TaskStack.java new file mode 100644 index 0000000..a5aa387 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/recents/model/TaskStack.java @@ -0,0 +1,231 @@ +/* + * Copyright (C) 2014 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 android.content.Context; + +import java.util.ArrayList; +import java.util.List; + + +/** + * An interface for a task filter to query whether a particular task should show in a stack. + */ +interface TaskFilter { + /** Returns whether the filter accepts the specified task */ + public boolean acceptTask(Task t, int index); +} + +/** + * A list of filtered tasks. + */ +class FilteredTaskList { + ArrayList<Task> mTasks = new ArrayList<Task>(); + ArrayList<Task> mFilteredTasks = new ArrayList<Task>(); + TaskFilter mFilter; + + /** Sets the task filter, saving the current touch state */ + void setFilter(TaskFilter filter) { + mFilter = filter; + updateFilteredTasks(); + } + + /** Removes the task filter and returns the previous touch state */ + void removeFilter() { + mFilter = null; + updateFilteredTasks(); + } + + /** Adds a new task to the task list */ + void add(Task t) { + mTasks.add(t); + updateFilteredTasks(); + } + + /** Sets the list of tasks */ + void set(List<Task> tasks) { + mTasks.clear(); + mTasks.addAll(tasks); + updateFilteredTasks(); + } + + /** Removes a task from the base list only if it is in the filtered list */ + boolean remove(Task t) { + if (mFilteredTasks.contains(t)) { + boolean removed = mTasks.remove(t); + updateFilteredTasks(); + return removed; + } + return false; + } + + /** Returns the index of this task in the list of filtered tasks */ + int indexOf(Task t) { + return mFilteredTasks.indexOf(t); + } + + /** Returns the size of the list of filtered tasks */ + int size() { + return mFilteredTasks.size(); + } + + /** Returns whether the filtered list contains this task */ + boolean contains(Task t) { + return mFilteredTasks.contains(t); + } + + /** Updates the list of filtered tasks whenever the base task list changes */ + private void updateFilteredTasks() { + mFilteredTasks.clear(); + if (mFilter != null) { + int taskCount = mTasks.size(); + for (int i = 0; i < taskCount; i++) { + Task t = mTasks.get(i); + if (mFilter.acceptTask(t, i)) { + mFilteredTasks.add(t); + } + } + } else { + mFilteredTasks.addAll(mTasks); + } + } + + /** Returns whether this task list is filtered */ + boolean hasFilter() { + return (mFilter != null); + } + + /** Returns the list of filtered tasks */ + ArrayList<Task> getTasks() { + return mFilteredTasks; + } +} + +/** + * The task stack contains a list of multiple tasks. + */ +public class TaskStack { + Context mContext; + + FilteredTaskList mTaskList = new FilteredTaskList(); + TaskStackCallbacks mCb; + + public TaskStack(Context context) { + mContext = context; + } + + /** Sets the callbacks for this task stack */ + public void setCallbacks(TaskStackCallbacks cb) { + mCb = cb; + } + + /** Adds a new task */ + public void addTask(Task t) { + mTaskList.add(t); + if (mCb != null) { + mCb.onStackTaskAdded(this, t); + } + } + + /** Removes a task */ + public void removeTask(Task t) { + if (mTaskList.contains(t)) { + mTaskList.remove(t); + if (mCb != null) { + mCb.onStackTaskRemoved(this, t); + } + } + } + + /** Sets a few tasks in one go */ + public void setTasks(List<Task> tasks) { + int taskCount = mTaskList.getTasks().size(); + for (int i = 0; i < taskCount; i++) { + Task t = mTaskList.getTasks().get(i); + if (mCb != null) { + mCb.onStackTaskRemoved(this, t); + } + } + mTaskList.set(tasks); + for (Task t : tasks) { + if (mCb != null) { + mCb.onStackTaskAdded(this, t); + } + } + } + + /** Gets the tasks */ + public ArrayList<Task> getTasks() { + return mTaskList.getTasks(); + } + + /** Gets the number of tasks */ + public int getTaskCount() { + return mTaskList.size(); + } + + /** Returns the index of this task in this current task stack */ + public int indexOfTask(Task t) { + return mTaskList.indexOf(t); + } + + /** Tests whether a task is in this current task stack */ + public boolean containsTask(Task t) { + return mTaskList.contains(t); + } + + /** Filters the stack into tasks similar to the one specified */ + public void filterTasks(Task t) { + // Set the task list filter + // XXX: This is a dummy filter that currently just accepts every other task. + mTaskList.setFilter(new TaskFilter() { + @Override + public boolean acceptTask(Task t, int i) { + if (i % 2 == 0) { + return true; + } + return false; + } + }); + if (mCb != null) { + mCb.onStackFiltered(this); + } + } + + /** Unfilters the current stack */ + public void unfilterTasks() { + // Unset the filter, then update the virtual scroll + mTaskList.removeFilter(); + if (mCb != null) { + mCb.onStackUnfiltered(this); + } + } + + /** Returns whether tasks are currently filtered */ + public boolean hasFilteredTasks() { + return mTaskList.hasFilter(); + } + + @Override + public String toString() { + String str = "Tasks:\n"; + for (Task t : mTaskList.getTasks()) { + str += " " + t.toString() + "\n"; + } + return str; + } +}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/recents/model/TaskStackCallbacks.java b/packages/SystemUI/src/com/android/systemui/recents/model/TaskStackCallbacks.java new file mode 100644 index 0000000..4bec655 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/recents/model/TaskStackCallbacks.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2014 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; + +/* Task stack callbacks */ +public interface TaskStackCallbacks { + /* Notifies when a task has been added to the stack */ + public void onStackTaskAdded(TaskStack stack, Task t); + /* Notifies when a task has been removed from the stack */ + public void onStackTaskRemoved(TaskStack stack, Task t); + /** Notifies when the stack was filtered */ + public void onStackFiltered(TaskStack stack); + /** Notifies when the stack was un-filtered */ + public void onStackUnfiltered(TaskStack stack); +}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/RecentsView.java b/packages/SystemUI/src/com/android/systemui/recents/views/RecentsView.java new file mode 100644 index 0000000..c92041c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/recents/views/RecentsView.java @@ -0,0 +1,228 @@ +/* + * Copyright (C) 2014 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.views; + +import android.app.ActivityOptions; +import android.content.Context; +import android.content.Intent; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.os.UserHandle; +import android.view.View; +import android.widget.FrameLayout; +import com.android.systemui.recents.Console; +import com.android.systemui.recents.Constants; +import com.android.systemui.recents.RecentsConfiguration; +import com.android.systemui.recents.model.SpaceNode; +import com.android.systemui.recents.model.Task; +import com.android.systemui.recents.model.TaskStack; + +import java.util.ArrayList; + + +/** + * This view is the the top level layout that contains TaskStacks (which are laid out according + * to their SpaceNode bounds. + */ +public class RecentsView extends FrameLayout implements TaskStackViewCallbacks { + // The space partitioning root of this container + SpaceNode mBSP; + + public RecentsView(Context context) { + super(context); + setWillNotDraw(false); + } + + /** Set/get the bsp root node */ + public void setBSP(SpaceNode n) { + mBSP = n; + + // XXX: We shouldn't be recereating new stacks every time, but for now, that is OK + // Add all the stacks for this partition + removeAllViews(); + ArrayList<TaskStack> stacks = mBSP.getStacks(); + for (TaskStack stack : stacks) { + TaskStackView stackView = new TaskStackView(getContext(), stack); + stackView.setCallbacks(this); + addView(stackView); + } + } + + /** Launches the first task from the first stack if possible */ + public boolean launchFirstTask() { + int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + TaskStackView stackView = (TaskStackView) getChildAt(i); + TaskStack stack = stackView.mStack; + ArrayList<Task> tasks = stack.getTasks(); + if (!tasks.isEmpty()) { + Task task = tasks.get(tasks.size() - 1); + TaskView tv = null; + if (stackView.getChildCount() > 0) { + TaskView stv = (TaskView) stackView.getChildAt(stackView.getChildCount() - 1); + if (stv.getTask() == task) { + tv = stv; + } + } + onTaskLaunched(stackView, tv, stack, task); + return true; + } + } + return false; + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int width = MeasureSpec.getSize(widthMeasureSpec); + int height = MeasureSpec.getSize(heightMeasureSpec); + int heightMode = MeasureSpec.getMode(heightMeasureSpec); + Console.log(Constants.DebugFlags.UI.MeasureAndLayout, "[RecentsView|measure]", "width: " + width + " height: " + height, Console.AnsiGreen); + + // We measure our stack views sans the status bar. It will handle the nav bar itself. + RecentsConfiguration config = RecentsConfiguration.getInstance(); + int childHeight = height - config.systemInsets.top; + + // Measure each child + int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + if (child.getVisibility() != GONE) { + child.measure(widthMeasureSpec, + MeasureSpec.makeMeasureSpec(childHeight, heightMode)); + } + } + + setMeasuredDimension(width, height); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + Console.log(Constants.DebugFlags.UI.MeasureAndLayout, "[RecentsView|layout]", new Rect(left, top, right, bottom) + " changed: " + changed, Console.AnsiGreen); + // We offset our stack views by the status bar height. It will handle the nav bar itself. + RecentsConfiguration config = RecentsConfiguration.getInstance(); + top += config.systemInsets.top; + + // Layout each child + // XXX: Based on the space node for that task view + int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + if (child.getVisibility() != GONE) { + final int width = child.getMeasuredWidth(); + final int height = child.getMeasuredHeight(); + child.layout(left, top, left + width, top + height); + } + } + } + + @Override + protected void dispatchDraw(Canvas canvas) { + Console.log(Constants.DebugFlags.UI.Draw, "[RecentsView|dispatchDraw]", "", Console.AnsiPurple); + super.dispatchDraw(canvas); + } + + @Override + protected boolean fitSystemWindows(Rect insets) { + Console.log(Constants.DebugFlags.UI.MeasureAndLayout, "[RecentsView|fitSystemWindows]", "insets: " + insets, Console.AnsiGreen); + + // Update the configuration with the latest system insets and trigger a relayout + RecentsConfiguration config = RecentsConfiguration.getInstance(); + config.updateSystemInsets(insets); + requestLayout(); + + return true; + } + + /** Unfilters any filtered stacks */ + public boolean unfilterFilteredStacks() { + if (mBSP != null) { + // Check if there are any filtered stacks and unfilter them before we back out of Recents + boolean stacksUnfiltered = false; + ArrayList<TaskStack> stacks = mBSP.getStacks(); + for (TaskStack stack : stacks) { + if (stack.hasFilteredTasks()) { + stack.unfilterTasks(); + stacksUnfiltered = true; + } + } + return stacksUnfiltered; + } + return false; + } + + /**** View.OnClickListener Implementation ****/ + + @Override + public void onTaskLaunched(final TaskStackView stackView, final TaskView tv, + final TaskStack stack, final Task task) { + final Runnable launchRunnable = new Runnable() { + @Override + public void run() { + TaskViewTransform transform; + View sourceView = tv; + int offsetX = 0; + int offsetY = 0; + if (tv == null) { + // Launch the activity + sourceView = stackView; + transform = stackView.getStackTransform(stack.indexOfTask(task)); + offsetX = transform.rect.left; + offsetY = transform.rect.top; + } else { + transform = stackView.getStackTransform(stack.indexOfTask(task)); + } + + // Compute the thumbnail to scale up from + ActivityOptions opts = null; + int thumbnailWidth = transform.rect.width(); + int thumbnailHeight = transform.rect.height(); + if (task.thumbnail != null && thumbnailWidth > 0 && thumbnailHeight > 0 && + task.thumbnail.getWidth() > 0 && task.thumbnail.getHeight() > 0) { + // Resize the thumbnail to the size of the view that we are animating from + Bitmap b = Bitmap.createBitmap(thumbnailWidth, thumbnailHeight, + Bitmap.Config.ARGB_8888); + Canvas c = new Canvas(b); + c.drawBitmap(task.thumbnail, + new Rect(0, 0, task.thumbnail.getWidth(), task.thumbnail.getHeight()), + new Rect(0, 0, thumbnailWidth, thumbnailHeight), null); + c.setBitmap(null); + opts = ActivityOptions.makeThumbnailScaleUpAnimation(sourceView, + b, offsetX, offsetY); + } + + // Launch the activity with the desired animation + Intent i = new Intent(task.intent); + i.setFlags(Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY + | Intent.FLAG_ACTIVITY_TASK_ON_HOME + | Intent.FLAG_ACTIVITY_NEW_TASK); + if (opts != null) { + getContext().startActivityAsUser(i, opts.toBundle(), UserHandle.CURRENT); + } else { + getContext().startActivityAsUser(i, UserHandle.CURRENT); + } + } + }; + + // Launch the app right away if there is no task view, otherwise, animate the icon out first + if (tv == null || !Constants.Values.TaskView.AnimateFrontTaskIconOnLeavingRecents) { + launchRunnable.run(); + } else { + tv.animateOnLeavingRecents(launchRunnable); + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/SwipeHelper.java b/packages/SystemUI/src/com/android/systemui/recents/views/SwipeHelper.java new file mode 100644 index 0000000..fe661bc --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/recents/views/SwipeHelper.java @@ -0,0 +1,389 @@ +/* + * Copyright (C) 2014 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.views; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ObjectAnimator; +import android.animation.ValueAnimator; +import android.animation.ValueAnimator.AnimatorUpdateListener; +import android.annotation.TargetApi; +import android.os.Build; +import android.util.DisplayMetrics; +import android.view.MotionEvent; +import android.view.VelocityTracker; +import android.view.View; +import android.view.animation.LinearInterpolator; + +/** + * This class facilitates swipe to dismiss. It defines an interface to be implemented by the + * by the class hosting the views that need to swiped, and, using this interface, handles touch + * events and translates / fades / animates the view as it is dismissed. + */ +public class SwipeHelper { + static final String TAG = "SwipeHelper"; + private static final boolean SLOW_ANIMATIONS = false; // DEBUG; + private static final boolean CONSTRAIN_SWIPE = true; + private static final boolean FADE_OUT_DURING_SWIPE = true; + private static final boolean DISMISS_IF_SWIPED_FAR_ENOUGH = true; + + public static final int X = 0; + public static final int Y = 1; + + private static LinearInterpolator sLinearInterpolator = new LinearInterpolator(); + + private float SWIPE_ESCAPE_VELOCITY = 100f; // dp/sec + private int DEFAULT_ESCAPE_ANIMATION_DURATION = 75; // ms + private int MAX_ESCAPE_ANIMATION_DURATION = 150; // ms + private int MAX_DISMISS_VELOCITY = 2000; // dp/sec + private static final int SNAP_ANIM_LEN = SLOW_ANIMATIONS ? 1000 : 150; // ms + + public static float ALPHA_FADE_START = 0.15f; // fraction of thumbnail width + // where fade starts + static final float ALPHA_FADE_END = 0.65f; // fraction of thumbnail width + // beyond which alpha->0 + private float mMinAlpha = 0f; + + private float mPagingTouchSlop; + Callback mCallback; + private int mSwipeDirection; + private VelocityTracker mVelocityTracker; + + private float mInitialTouchPos; + private boolean mDragging; + + private View mCurrView; + private boolean mCanCurrViewBeDimissed; + private float mDensityScale; + + public boolean mAllowSwipeTowardsStart = true; + public boolean mAllowSwipeTowardsEnd = true; + private boolean mRtl; + + public SwipeHelper(int swipeDirection, Callback callback, float densityScale, + float pagingTouchSlop) { + mCallback = callback; + mSwipeDirection = swipeDirection; + mVelocityTracker = VelocityTracker.obtain(); + mDensityScale = densityScale; + mPagingTouchSlop = pagingTouchSlop; + } + + public void setDensityScale(float densityScale) { + mDensityScale = densityScale; + } + + public void setPagingTouchSlop(float pagingTouchSlop) { + mPagingTouchSlop = pagingTouchSlop; + } + + public void cancelOngoingDrag() { + if (mDragging) { + if (mCurrView != null) { + mCallback.onDragCancelled(mCurrView); + setTranslation(mCurrView, 0); + mCallback.onSnapBackCompleted(mCurrView); + mCurrView = null; + } + mDragging = false; + } + } + + public void resetTranslation(View v) { + setTranslation(v, 0); + } + + private float getPos(MotionEvent ev) { + return mSwipeDirection == X ? ev.getX() : ev.getY(); + } + + private float getTranslation(View v) { + return mSwipeDirection == X ? v.getTranslationX() : v.getTranslationY(); + } + + private float getVelocity(VelocityTracker vt) { + return mSwipeDirection == X ? vt.getXVelocity() : + vt.getYVelocity(); + } + + private ObjectAnimator createTranslationAnimation(View v, float newPos) { + ObjectAnimator anim = ObjectAnimator.ofFloat(v, + mSwipeDirection == X ? View.TRANSLATION_X : View.TRANSLATION_Y, newPos); + return anim; + } + + private float getPerpendicularVelocity(VelocityTracker vt) { + return mSwipeDirection == X ? vt.getYVelocity() : + vt.getXVelocity(); + } + + private void setTranslation(View v, float translate) { + if (mSwipeDirection == X) { + v.setTranslationX(translate); + } else { + v.setTranslationY(translate); + } + } + + private float getSize(View v) { + final DisplayMetrics dm = v.getContext().getResources().getDisplayMetrics(); + return mSwipeDirection == X ? dm.widthPixels : dm.heightPixels; + } + + public void setMinAlpha(float minAlpha) { + mMinAlpha = minAlpha; + } + + float getAlphaForOffset(View view) { + float viewSize = getSize(view); + final float fadeSize = ALPHA_FADE_END * viewSize; + float result = 1.0f; + float pos = getTranslation(view); + if (pos >= viewSize * ALPHA_FADE_START) { + result = 1.0f - (pos - viewSize * ALPHA_FADE_START) / fadeSize; + } else if (pos < viewSize * (1.0f - ALPHA_FADE_START)) { + result = 1.0f + (viewSize * ALPHA_FADE_START + pos) / fadeSize; + } + result = Math.min(result, 1.0f); + result = Math.max(result, 0f); + return Math.max(mMinAlpha, result); + } + + /** + * Determines whether the given view has RTL layout. + */ + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) + public static boolean isLayoutRtl(View view) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + return View.LAYOUT_DIRECTION_RTL == view.getLayoutDirection(); + } else { + return false; + } + } + + public boolean onInterceptTouchEvent(MotionEvent ev) { + final int action = ev.getAction(); + + switch (action) { + case MotionEvent.ACTION_DOWN: + mDragging = false; + mCurrView = mCallback.getChildAtPosition(ev); + mVelocityTracker.clear(); + if (mCurrView != null) { + mRtl = isLayoutRtl(mCurrView); + mCanCurrViewBeDimissed = mCallback.canChildBeDismissed(mCurrView); + mVelocityTracker.addMovement(ev); + mInitialTouchPos = getPos(ev); + } else { + mCanCurrViewBeDimissed = false; + } + break; + case MotionEvent.ACTION_MOVE: + if (mCurrView != null) { + mVelocityTracker.addMovement(ev); + float pos = getPos(ev); + float delta = pos - mInitialTouchPos; + if (Math.abs(delta) > mPagingTouchSlop) { + mCallback.onBeginDrag(mCurrView); + mDragging = true; + mInitialTouchPos = getPos(ev) - getTranslation(mCurrView); + } + } + break; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + mDragging = false; + mCurrView = null; + break; + } + return mDragging; + } + + /** + * @param view The view to be dismissed + * @param velocity The desired pixels/second speed at which the view should move + */ + private void dismissChild(final View view, float velocity) { + final boolean canAnimViewBeDismissed = mCallback.canChildBeDismissed(view); + float newPos; + if (velocity < 0 + || (velocity == 0 && getTranslation(view) < 0) + // if we use the Menu to dismiss an item in landscape, animate up + || (velocity == 0 && getTranslation(view) == 0 && mSwipeDirection == Y)) { + newPos = -getSize(view); + } else { + newPos = getSize(view); + } + int duration = MAX_ESCAPE_ANIMATION_DURATION; + if (velocity != 0) { + duration = Math.min(duration, + (int) (Math.abs(newPos - getTranslation(view)) * + 1000f / Math.abs(velocity))); + } else { + duration = DEFAULT_ESCAPE_ANIMATION_DURATION; + } + + ValueAnimator anim = createTranslationAnimation(view, newPos); + anim.setInterpolator(sLinearInterpolator); + anim.setDuration(duration); + anim.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mCallback.onChildDismissed(view); + if (FADE_OUT_DURING_SWIPE && canAnimViewBeDismissed) { + view.setAlpha(1.f); + } + } + }); + anim.addUpdateListener(new AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + if (FADE_OUT_DURING_SWIPE && canAnimViewBeDismissed) { + view.setAlpha(getAlphaForOffset(view)); + } + } + }); + anim.start(); + } + + private void snapChild(final View view, float velocity) { + final boolean canAnimViewBeDismissed = mCallback.canChildBeDismissed(view); + ValueAnimator anim = createTranslationAnimation(view, 0); + int duration = SNAP_ANIM_LEN; + anim.setDuration(duration); + anim.addUpdateListener(new AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + if (FADE_OUT_DURING_SWIPE && canAnimViewBeDismissed) { + view.setAlpha(getAlphaForOffset(view)); + } + } + }); + anim.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + if (FADE_OUT_DURING_SWIPE && canAnimViewBeDismissed) { + view.setAlpha(1.0f); + } + mCallback.onSnapBackCompleted(view); + } + }); + anim.start(); + } + + public boolean onTouchEvent(MotionEvent ev) { + if (!mDragging) { + if (!onInterceptTouchEvent(ev)) { + return mCanCurrViewBeDimissed; + } + } + + mVelocityTracker.addMovement(ev); + final int action = ev.getAction(); + switch (action) { + case MotionEvent.ACTION_OUTSIDE: + case MotionEvent.ACTION_MOVE: + if (mCurrView != null) { + float delta = getPos(ev) - mInitialTouchPos; + setSwipeAmount(delta); + } + break; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + if (mCurrView != null) { + endSwipe(mVelocityTracker); + } + break; + } + return true; + } + + private void setSwipeAmount(float amount) { + // don't let items that can't be dismissed be dragged more than + // maxScrollDistance + if (CONSTRAIN_SWIPE + && (!isValidSwipeDirection(amount) || !mCallback.canChildBeDismissed(mCurrView))) { + float size = getSize(mCurrView); + float maxScrollDistance = 0.15f * size; + if (Math.abs(amount) >= size) { + amount = amount > 0 ? maxScrollDistance : -maxScrollDistance; + } else { + amount = maxScrollDistance * (float) Math.sin((amount/size)*(Math.PI/2)); + } + } + setTranslation(mCurrView, amount); + if (FADE_OUT_DURING_SWIPE && mCanCurrViewBeDimissed) { + float alpha = getAlphaForOffset(mCurrView); + mCurrView.setAlpha(alpha); + } + } + + private boolean isValidSwipeDirection(float amount) { + if (mSwipeDirection == X) { + if (mRtl) { + return (amount <= 0) ? mAllowSwipeTowardsEnd : mAllowSwipeTowardsStart; + } else { + return (amount <= 0) ? mAllowSwipeTowardsStart : mAllowSwipeTowardsEnd; + } + } + + // Vertical swipes are always valid. + return true; + } + + private void endSwipe(VelocityTracker velocityTracker) { + float maxVelocity = MAX_DISMISS_VELOCITY * mDensityScale; + velocityTracker.computeCurrentVelocity(1000 /* px/sec */, maxVelocity); + float velocity = getVelocity(velocityTracker); + float perpendicularVelocity = getPerpendicularVelocity(velocityTracker); + float escapeVelocity = SWIPE_ESCAPE_VELOCITY * mDensityScale; + float translation = getTranslation(mCurrView); + // Decide whether to dismiss the current view + boolean childSwipedFarEnough = DISMISS_IF_SWIPED_FAR_ENOUGH && + Math.abs(translation) > 0.6 * getSize(mCurrView); + boolean childSwipedFastEnough = (Math.abs(velocity) > escapeVelocity) && + (Math.abs(velocity) > Math.abs(perpendicularVelocity)) && + (velocity > 0) == (translation > 0); + + boolean dismissChild = mCallback.canChildBeDismissed(mCurrView) + && isValidSwipeDirection(translation) + && (childSwipedFastEnough || childSwipedFarEnough); + + if (dismissChild) { + // flingadingy + dismissChild(mCurrView, childSwipedFastEnough ? velocity : 0f); + } else { + // snappity + mCallback.onDragCancelled(mCurrView); + snapChild(mCurrView, velocity); + } + } + + public interface Callback { + View getChildAtPosition(MotionEvent ev); + + boolean canChildBeDismissed(View v); + + void onBeginDrag(View v); + + void onChildDismissed(View v); + + void onSnapBackCompleted(View v); + + void onDragCancelled(View v); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackView.java b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackView.java new file mode 100644 index 0000000..9dd6c0b --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackView.java @@ -0,0 +1,1075 @@ +/* + * Copyright (C) 2014 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.views; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ObjectAnimator; +import android.animation.ValueAnimator; +import android.app.Activity; +import android.app.ActivityManager; +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.graphics.Region; +import android.view.MotionEvent; +import android.view.VelocityTracker; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.ViewParent; +import android.widget.FrameLayout; +import android.widget.OverScroller; +import android.widget.Toast; +import com.android.systemui.recents.Console; +import com.android.systemui.recents.Constants; +import com.android.systemui.recents.RecentsConfiguration; +import com.android.systemui.recents.RecentsTaskLoader; +import com.android.systemui.recents.Utilities; +import com.android.systemui.recents.model.Task; +import com.android.systemui.recents.model.TaskStack; +import com.android.systemui.recents.model.TaskStackCallbacks; + +import java.util.ArrayList; + +/** The TaskView callbacks */ +interface TaskStackViewCallbacks { + public void onTaskLaunched(TaskStackView stackView, TaskView tv, TaskStack stack, Task t); +} + +/* The visual representation of a task stack view */ +public class TaskStackView extends FrameLayout implements TaskStackCallbacks, TaskViewCallbacks, + ViewPoolConsumer<TaskView, Task>, View.OnClickListener { + TaskStack mStack; + TaskStackViewTouchHandler mTouchHandler; + TaskStackViewCallbacks mCb; + ViewPool<TaskView, Task> mViewPool; + + // The various rects that define the stack view + Rect mRect = new Rect(); + Rect mStackRect = new Rect(); + Rect mStackRectSansPeek = new Rect(); + Rect mTaskRect = new Rect(); + + // The virtual stack scroll that we use for the card layout + int mStackScroll; + int mMinScroll; + int mMaxScroll; + OverScroller mScroller; + ObjectAnimator mScrollAnimator; + + // Optimizations + int mHwLayersRefCount; + int mStackViewsAnimationDuration; + boolean mStackViewsDirty = true; + boolean mAwaitingFirstLayout = true; + + public TaskStackView(Context context, TaskStack stack) { + super(context); + mStack = stack; + mStack.setCallbacks(this); + mScroller = new OverScroller(context); + mTouchHandler = new TaskStackViewTouchHandler(context, this); + mViewPool = new ViewPool<TaskView, Task>(context, this); + } + + /** Sets the callbacks */ + void setCallbacks(TaskStackViewCallbacks cb) { + mCb = cb; + } + + /** Requests that the views be synchronized with the model */ + void requestSynchronizeStackViewsWithModel() { + requestSynchronizeStackViewsWithModel(0); + } + void requestSynchronizeStackViewsWithModel(int duration) { + Console.log(Constants.DebugFlags.TaskStack.SynchronizeViewsWithModel, + "[TaskStackView|requestSynchronize]", "", Console.AnsiYellow); + if (!mStackViewsDirty) { + invalidate(); + } + if (mAwaitingFirstLayout) { + // Skip the animation if we are awaiting first layout + mStackViewsAnimationDuration = 0; + } else { + mStackViewsAnimationDuration = Math.max(mStackViewsAnimationDuration, duration); + } + mStackViewsDirty = true; + } + + // XXX: Optimization: Use a mapping of Task -> View + private TaskView getChildViewForTask(Task t) { + int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + TaskView tv = (TaskView) getChildAt(i); + if (tv.getTask() == t) { + return tv; + } + } + return null; + } + + /** Update/get the transform */ + public TaskViewTransform getStackTransform(int indexInStack) { + TaskViewTransform transform = new TaskViewTransform(); + + // Map the items to an continuous position relative to the current scroll + int numPeekCards = Constants.Values.TaskStackView.StackPeekNumCards; + float overlapHeight = Constants.Values.TaskStackView.StackOverlapPct * mTaskRect.height(); + float peekHeight = Constants.Values.TaskStackView.StackPeekHeightPct * mStackRect.height(); + float t = ((indexInStack * overlapHeight) - getStackScroll()) / overlapHeight; + float boundedT = Math.max(t, -(numPeekCards + 1)); + + // Set the scale relative to its position + float minScale = Constants.Values.TaskStackView.StackPeekMinScale; + float scaleRange = 1f - minScale; + float scaleInc = scaleRange / numPeekCards; + float scale = Math.max(minScale, Math.min(1f, 1f + (boundedT * scaleInc))); + float scaleYOffset = ((1f - scale) * mTaskRect.height()) / 2; + transform.scale = scale; + + // Set the translation + if (boundedT < 0f) { + transform.translationY = (int) ((Math.max(-numPeekCards, boundedT) / + numPeekCards) * peekHeight - scaleYOffset); + } else { + transform.translationY = (int) (boundedT * overlapHeight - scaleYOffset); + } + + // Update the rect and visibility + transform.rect.set(mTaskRect); + if (t < -(numPeekCards + 1)) { + transform.visible = false; + } else { + transform.rect.offset(0, transform.translationY); + Utilities.scaleRectAboutCenter(transform.rect, scale); + transform.visible = Rect.intersects(mRect, transform.rect); + } + transform.t = t; + return transform; + } + + /** Synchronizes the views with the model */ + void synchronizeStackViewsWithModel() { + Console.log(Constants.DebugFlags.TaskStack.SynchronizeViewsWithModel, + "[TaskStackView|synchronizeViewsWithModel]", + "mStackViewsDirty: " + mStackViewsDirty, Console.AnsiYellow); + if (mStackViewsDirty) { + + // XXX: Optimization: Use binary search to find the visible range + // XXX: Optimize to not call getStackTransform() so many times + // XXX: Consider using TaskViewTransform pool to prevent allocations + // XXX: Iterate children views, update transforms and remove all that are not visible + // For all remaining tasks, update transforms and if visible add the view + + // Update the visible state of all the tasks + ArrayList<Task> tasks = mStack.getTasks(); + int taskCount = tasks.size(); + for (int i = 0; i < taskCount; i++) { + Task task = tasks.get(i); + TaskViewTransform transform = getStackTransform(i); + TaskView tv = getChildViewForTask(task); + + if (transform.visible) { + if (tv == null) { + tv = mViewPool.pickUpViewFromPool(task, task); + // When we are picking up a new view from the view pool, prepare it for any + // following animation by putting it in a reasonable place + if (mStackViewsAnimationDuration > 0 && i != 0) { + // XXX: We have to animate when filtering, etc. Maybe we should have a + // runnable that ensures that tasks are animated in a special way + // when they are entering the scene? + int fromIndex = (transform.t < 0) ? (i - 1) : (i + 1); + tv.updateViewPropertiesFromTask(null, getStackTransform(fromIndex), 0); + } + } + } else { + if (tv != null) { + mViewPool.returnViewToPool(tv); + } + } + } + + // Update all the current view children + // NOTE: We have to iterate in reverse where because we are removing views directly + int childCount = getChildCount(); + for (int i = childCount - 1; i >= 0; i--) { + TaskView tv = (TaskView) getChildAt(i); + Task task = tv.getTask(); + TaskViewTransform transform = getStackTransform(mStack.indexOfTask(task)); + if (!transform.visible) { + mViewPool.returnViewToPool(tv); + } else { + tv.updateViewPropertiesFromTask(null, transform, mStackViewsAnimationDuration); + } + } + + Console.log(Constants.DebugFlags.TaskStack.SynchronizeViewsWithModel, + " [TaskStackView|viewChildren]", "" + getChildCount()); + + mStackViewsAnimationDuration = 0; + mStackViewsDirty = false; + } + } + + /** Sets the current stack scroll */ + public void setStackScroll(int value) { + mStackScroll = value; + requestSynchronizeStackViewsWithModel(); + } + + /** Gets the current stack scroll */ + public int getStackScroll() { + return mStackScroll; + } + + /** Animates the stack scroll into bounds */ + ObjectAnimator animateBoundScroll(int duration) { + int curScroll = getStackScroll(); + int newScroll = Math.max(mMinScroll, Math.min(mMaxScroll, curScroll)); + if (newScroll != curScroll) { + // Enable hw layers on the stack + addHwLayersRefCount(); + + // Abort any current animations + mScroller.abortAnimation(); + if (mScrollAnimator != null) { + mScrollAnimator.cancel(); + mScrollAnimator.removeAllListeners(); + } + + // Start a new scroll animation + mScrollAnimator = ObjectAnimator.ofInt(this, "stackScroll", curScroll, newScroll); + mScrollAnimator.setDuration(duration); + mScrollAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + setStackScroll((Integer) animation.getAnimatedValue()); + } + }); + mScrollAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + // Disable hw layers on the stack + decHwLayersRefCount(); + } + }); + mScrollAnimator.start(); + } + return mScrollAnimator; + } + + /** Aborts any current stack scrolls */ + void abortBoundScrollAnimation() { + if (mScrollAnimator != null) { + mScrollAnimator.cancel(); + } + } + + /** Bounds the current scroll if necessary */ + public boolean boundScroll() { + int curScroll = getStackScroll(); + int newScroll = Math.max(mMinScroll, Math.min(mMaxScroll, curScroll)); + if (newScroll != curScroll) { + setStackScroll(newScroll); + return true; + } + return false; + } + + /** Returns whether the current scroll is out of bounds */ + boolean isScrollOutOfBounds() { + return (getStackScroll() < 0) || (getStackScroll() > mMaxScroll); + } + + /** Updates the min and max virtual scroll bounds */ + void updateMinMaxScroll(boolean boundScrollToNewMinMax) { + // Compute the min and max scroll values + int numTasks = Math.max(1, mStack.getTaskCount()); + int taskHeight = mTaskRect.height(); + int stackHeight = mStackRectSansPeek.height(); + int maxScrollHeight = taskHeight + (int) ((numTasks - 1) * + Constants.Values.TaskStackView.StackOverlapPct * taskHeight); + mMinScroll = Math.min(stackHeight, maxScrollHeight) - stackHeight; + mMaxScroll = maxScrollHeight - stackHeight; + + // Debug logging + if (Constants.DebugFlags.UI.MeasureAndLayout) { + Console.log(" [TaskStack|minScroll] " + mMinScroll); + Console.log(" [TaskStack|maxScroll] " + mMaxScroll); + } + + if (boundScrollToNewMinMax) { + boundScroll(); + } + } + + /** Enables the hw layers and increments the hw layer requirement ref count */ + void addHwLayersRefCount() { + Console.log(Constants.DebugFlags.UI.HwLayers, + "[TaskStackView|addHwLayersRefCount] refCount: " + + mHwLayersRefCount + "->" + (mHwLayersRefCount + 1)); + if (mHwLayersRefCount == 0) { + // Enable hw layers on each of the children + int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + TaskView tv = (TaskView) getChildAt(i); + tv.enableHwLayers(); + } + } + mHwLayersRefCount++; + } + + /** Decrements the hw layer requirement ref count and disables the hw layers when we don't + need them anymore. */ + void decHwLayersRefCount() { + Console.log(Constants.DebugFlags.UI.HwLayers, + "[TaskStackView|decHwLayersRefCount] refCount: " + + mHwLayersRefCount + "->" + (mHwLayersRefCount - 1)); + mHwLayersRefCount--; + if (mHwLayersRefCount == 0) { + // Disable hw layers on each of the children + int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + TaskView tv = (TaskView) getChildAt(i); + tv.disableHwLayers(); + } + } else if (mHwLayersRefCount < 0) { + throw new RuntimeException("Invalid hw layers ref count"); + } + } + + @Override + public void computeScroll() { + if (mScroller.computeScrollOffset()) { + setStackScroll(mScroller.getCurrY()); + invalidate(); + + // If we just finished scrolling, then disable the hw layers + if (mScroller.isFinished()) { + decHwLayersRefCount(); + } + } + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + return mTouchHandler.onInterceptTouchEvent(ev); + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + return mTouchHandler.onTouchEvent(ev); + } + + @Override + public void dispatchDraw(Canvas canvas) { + Console.log(Constants.DebugFlags.UI.Draw, "[TaskStackView|dispatchDraw]", "", + Console.AnsiPurple); + synchronizeStackViewsWithModel(); + super.dispatchDraw(canvas); + } + + @Override + protected boolean drawChild(Canvas canvas, View child, long drawingTime) { + if (Constants.DebugFlags.App.EnableTaskStackClipping) { + TaskView tv = (TaskView) child; + TaskView nextTv = null; + int curIndex = indexOfChild(tv); + if (curIndex < (getChildCount() - 1)) { + // Clip against the next view (if we aren't animating its alpha) + nextTv = (TaskView) getChildAt(curIndex + 1); + if (nextTv.getAlpha() == 1f) { + Rect curRect = tv.getClippingRect(Utilities.tmpRect, false); + Rect nextRect = nextTv.getClippingRect(Utilities.tmpRect2, true); + RecentsConfiguration config = RecentsConfiguration.getInstance(); + // The hit rects are relative to the task view, which needs to be offset by the + // system bar height + curRect.offset(0, config.systemInsets.top); + nextRect.offset(0, config.systemInsets.top); + // Compute the clip region + Region clipRegion = new Region(); + clipRegion.op(curRect, Region.Op.UNION); + clipRegion.op(nextRect, Region.Op.DIFFERENCE); + // Clip the canvas + int saveCount = canvas.save(Canvas.CLIP_SAVE_FLAG); + canvas.clipRegion(clipRegion); + boolean invalidate = super.drawChild(canvas, child, drawingTime); + canvas.restoreToCount(saveCount); + return invalidate; + } + } + } + return super.drawChild(canvas, child, drawingTime); + } + + /** Computes the stack and task rects */ + public void computeRects(int width, int height) { + // Note: We let the stack view be the full height because we want the cards to go under the + // navigation bar if possible. However, the stack rects which we use to calculate + // max scroll, etc. need to take the nav bar into account + + // Compute the stack rects + RecentsConfiguration config = RecentsConfiguration.getInstance(); + mRect.set(0, 0, width, height); + mStackRect.set(mRect); + mStackRect.bottom -= config.systemInsets.bottom; + + int smallestDimension = Math.min(width, height); + int padding = (int) (Constants.Values.TaskStackView.StackPaddingPct * smallestDimension / 2f); + mStackRect.inset(padding, padding); + mStackRectSansPeek.set(mStackRect); + mStackRectSansPeek.top += Constants.Values.TaskStackView.StackPeekHeightPct * mStackRect.height(); + + // Compute the task rect + if (RecentsConfiguration.getInstance().layoutVerticalStack) { + int minHeight = (int) (mStackRect.height() - + (Constants.Values.TaskStackView.StackPeekHeightPct * mStackRect.height())); + int size = Math.min(minHeight, Math.min(mStackRect.width(), mStackRect.height())); + int centerX = mStackRect.centerX(); + mTaskRect.set(centerX - size / 2, mStackRectSansPeek.top, + centerX + size / 2, mStackRectSansPeek.top + size); + } else { + int size = Math.min(mStackRect.width(), mStackRect.height()); + int centerY = mStackRect.centerY(); + mTaskRect.set(mStackRectSansPeek.top, centerY - size / 2, + mStackRectSansPeek.top + size, centerY + size / 2); + } + + // Update the scroll bounds + updateMinMaxScroll(false); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int width = MeasureSpec.getSize(widthMeasureSpec); + int height = MeasureSpec.getSize(heightMeasureSpec); + Console.log(Constants.DebugFlags.UI.MeasureAndLayout, "[TaskStackView|measure]", + "width: " + width + " height: " + height + + " awaitingFirstLayout: " + mAwaitingFirstLayout, Console.AnsiGreen); + + // Compute our stack/task rects + computeRects(width, height); + + // Debug logging + if (Constants.DebugFlags.UI.MeasureAndLayout) { + Console.log(" [TaskStack|fullRect] " + mRect); + Console.log(" [TaskStack|stackRect] " + mStackRect); + Console.log(" [TaskStack|stackRectSansPeek] " + mStackRectSansPeek); + Console.log(" [TaskStack|taskRect] " + mTaskRect); + } + + // If this is the first layout, then scroll to the front of the stack and synchronize the + // stack views immediately + if (mAwaitingFirstLayout) { + setStackScroll(mMaxScroll); + requestSynchronizeStackViewsWithModel(); + synchronizeStackViewsWithModel(); + + // Animate the icon of the first task view + if (Constants.Values.TaskView.AnimateFrontTaskIconOnEnterRecents) { + TaskView tv = (TaskView) getChildAt(getChildCount() - 1); + if (tv != null) { + tv.animateOnEnterRecents(); + } + } + } + + // Measure each of the children + int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + TaskView t = (TaskView) getChildAt(i); + t.measure(MeasureSpec.makeMeasureSpec(mTaskRect.width(), MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(mTaskRect.height(), MeasureSpec.EXACTLY)); + } + + setMeasuredDimension(width, height); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + Console.log(Constants.DebugFlags.UI.MeasureAndLayout, "[TaskStackView|layout]", + "" + new Rect(left, top, right, bottom), Console.AnsiGreen); + + // Debug logging + if (Constants.DebugFlags.UI.MeasureAndLayout) { + Console.log(" [TaskStack|fullRect] " + mRect); + Console.log(" [TaskStack|stackRect] " + mStackRect); + Console.log(" [TaskStack|stackRectSansPeek] " + mStackRectSansPeek); + Console.log(" [TaskStack|taskRect] " + mTaskRect); + } + + // Layout each of the children + int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + TaskView t = (TaskView) getChildAt(i); + t.layout(mTaskRect.left, mStackRectSansPeek.top, + mTaskRect.right, mStackRectSansPeek.top + mTaskRect.height()); + } + + if (!mAwaitingFirstLayout) { + requestSynchronizeStackViewsWithModel(); + } else { + mAwaitingFirstLayout = false; + } + } + + @Override + protected void onScrollChanged(int l, int t, int oldl, int oldt) { + super.onScrollChanged(l, t, oldl, oldt); + requestSynchronizeStackViewsWithModel(); + } + + public boolean isTransformedTouchPointInView(float x, float y, View child) { + return isTransformedTouchPointInView(x, y, child, null); + } + + /**** TaskStackCallbacks Implementation ****/ + + @Override + public void onStackTaskAdded(TaskStack stack, Task t) { + requestSynchronizeStackViewsWithModel(); + } + + @Override + public void onStackTaskRemoved(TaskStack stack, Task t) { + // Remove the view associated with this task, we can't rely on updateTransforms + // to work here because the task is no longer in the list + int childCount = getChildCount(); + for (int i = childCount - 1; i >= 0; i--) { + TaskView tv = (TaskView) getChildAt(i); + if (tv.getTask() == t) { + mViewPool.returnViewToPool(tv); + break; + } + } + + updateMinMaxScroll(true); + requestSynchronizeStackViewsWithModel(Constants.Values.TaskStackView.Animation.TaskRemovedReshuffleDuration); + } + + @Override + public void onStackFiltered(TaskStack stack) { + requestSynchronizeStackViewsWithModel(); + } + + @Override + public void onStackUnfiltered(TaskStack stack) { + requestSynchronizeStackViewsWithModel(); + } + + /**** ViewPoolConsumer Implementation ****/ + + @Override + public TaskView createView(Context context) { + Console.log(Constants.DebugFlags.ViewPool.PoolCallbacks, "[TaskStackView|createPoolView]"); + return new TaskView(context); + } + + @Override + public void prepareViewToEnterPool(TaskView tv) { + Task task = tv.getTask(); + tv.resetViewProperties(); + Console.log(Constants.DebugFlags.ViewPool.PoolCallbacks, "[TaskStackView|returnToPool]", + tv.getTask() + " tv: " + tv); + + // Report that this tasks's data is no longer being used + RecentsTaskLoader loader = RecentsTaskLoader.getInstance(); + loader.unloadTaskData(task); + tv.unbindFromTask(); + + // Detach the view from the hierarchy + detachViewFromParent(tv); + + // Disable hw layers on this view + tv.disableHwLayers(); + } + + @Override + public void prepareViewToLeavePool(TaskView tv, Task prepareData, boolean isNewView) { + Console.log(Constants.DebugFlags.ViewPool.PoolCallbacks, "[TaskStackView|leavePool]", + "isNewView: " + isNewView); + + // Setup and attach the view to the window + Task task = prepareData; + // We try and rebind the task (this MUST be done before the task filled) + tv.bindToTask(task, this); + // Request that this tasks's data be filled + RecentsTaskLoader loader = RecentsTaskLoader.getInstance(); + loader.loadTaskData(task); + tv.syncToTask(); + + // Find the index where this task should be placed in the children + int insertIndex = -1; + int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + Task tvTask = ((TaskView) getChildAt(i)).getTask(); + if (mStack.containsTask(task) && (mStack.indexOfTask(task) < mStack.indexOfTask(tvTask))) { + insertIndex = i; + break; + } + } + + // Add/attach the view to the hierarchy + Console.log(Constants.DebugFlags.ViewPool.PoolCallbacks, " [TaskStackView|insertIndex]", + "" + insertIndex); + if (isNewView) { + addView(tv, insertIndex); + tv.setOnClickListener(this); + } else { + attachViewToParent(tv, insertIndex, tv.getLayoutParams()); + } + + // Enable hw layers on this view if hw layers are enabled on the stack + if (mHwLayersRefCount > 0) { + tv.enableHwLayers(); + } + } + + @Override + public boolean hasPreferredData(TaskView tv, Task preferredData) { + return (tv.getTask() == preferredData); + } + + /**** TaskViewCallbacks Implementation ****/ + + @Override + public void onTaskIconClicked(TaskView tv) { + Console.log(Constants.DebugFlags.UI.ClickEvents, "[TaskStack|Clicked|Icon]", + tv.getTask() + " is currently filtered: " + mStack.hasFilteredTasks(), + Console.AnsiCyan); + if (Constants.DebugFlags.App.EnableTaskFiltering) { + if (mStack.hasFilteredTasks()) { + mStack.unfilterTasks(); + } else { + mStack.filterTasks(tv.getTask()); + } + } else { + Toast.makeText(getContext(), "Task Filtering TBD", Toast.LENGTH_SHORT).show(); + } + } + + /**** View.OnClickListener Implementation ****/ + + @Override + public void onClick(View v) { + TaskView tv = (TaskView) v; + Task task = tv.getTask(); + Console.log(Constants.DebugFlags.UI.ClickEvents, "[TaskStack|Clicked|Thumbnail]", + task + " cb: " + mCb); + + if (mCb != null) { + mCb.onTaskLaunched(this, tv, mStack, task); + } + } +} + +/* Handles touch events */ +class TaskStackViewTouchHandler { + static int INACTIVE_POINTER_ID = -1; + + TaskStackView mSv; + VelocityTracker mVelocityTracker; + + boolean mIsScrolling; + boolean mIsSwiping; + + int mInitialMotionX, mInitialMotionY; + int mLastMotionX, mLastMotionY; + int mActivePointerId = INACTIVE_POINTER_ID; + TaskView mActiveTaskView = null; + + int mTotalScrollMotion; + int mMinimumVelocity; + int mMaximumVelocity; + // The scroll touch slop is used to calculate when we start scrolling + int mScrollTouchSlop; + // The swipe touch slop is used to calculate when we start swiping left/right, this takes + // precendence over the scroll touch slop in case the user makes a gesture that starts scrolling + // but is intended to be a swipe + int mSwipeTouchSlop; + // After a certain amount of scrolling, we should start ignoring checks for swiping + int mMaxScrollMotionToRejectSwipe; + + public TaskStackViewTouchHandler(Context context, TaskStackView sv) { + ViewConfiguration configuration = ViewConfiguration.get(context); + mMinimumVelocity = configuration.getScaledMinimumFlingVelocity(); + mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); + mScrollTouchSlop = configuration.getScaledTouchSlop(); + mSwipeTouchSlop = 2 * mScrollTouchSlop; + mMaxScrollMotionToRejectSwipe = 4 * mScrollTouchSlop; + mSv = sv; + } + + /** Velocity tracker helpers */ + void initOrResetVelocityTracker() { + if (mVelocityTracker == null) { + mVelocityTracker = VelocityTracker.obtain(); + } else { + mVelocityTracker.clear(); + } + } + void initVelocityTrackerIfNotExists() { + if (mVelocityTracker == null) { + mVelocityTracker = VelocityTracker.obtain(); + } + } + void recycleVelocityTracker() { + if (mVelocityTracker != null) { + mVelocityTracker.recycle(); + mVelocityTracker = null; + } + } + + /** Returns the view at the specified coordinates */ + TaskView findViewAtPoint(int x, int y) { + int childCount = mSv.getChildCount(); + for (int i = childCount - 1; i >= 0; i--) { + TaskView tv = (TaskView) mSv.getChildAt(i); + if (tv.getVisibility() == View.VISIBLE) { + if (mSv.isTransformedTouchPointInView(x, y, tv)) { + return tv; + } + } + } + return null; + } + + /** Touch preprocessing for handling below */ + public boolean onInterceptTouchEvent(MotionEvent ev) { + Console.log(Constants.DebugFlags.UI.TouchEvents, + "[TaskStackViewTouchHandler|interceptTouchEvent]", + Console.motionEventActionToString(ev.getAction()), Console.AnsiBlue); + + boolean hasChildren = (mSv.getChildCount() > 0); + if (!hasChildren) { + return false; + } + + boolean wasScrolling = !mSv.mScroller.isFinished() || + (mSv.mScrollAnimator != null && mSv.mScrollAnimator.isRunning()); + int action = ev.getAction(); + switch (action & MotionEvent.ACTION_MASK) { + case MotionEvent.ACTION_DOWN: { + // Save the touch down info + mInitialMotionX = mLastMotionX = (int) ev.getX(); + mInitialMotionY = mLastMotionY = (int) ev.getY(); + mActivePointerId = ev.getPointerId(0); + mActiveTaskView = findViewAtPoint(mLastMotionX, mLastMotionY); + // Stop the current scroll if it is still flinging + mSv.mScroller.abortAnimation(); + mSv.abortBoundScrollAnimation(); + // Initialize the velocity tracker + initOrResetVelocityTracker(); + mVelocityTracker.addMovement(ev); + // Check if the scroller is finished yet + mIsScrolling = !mSv.mScroller.isFinished(); + mIsSwiping = false; + break; + } + case MotionEvent.ACTION_MOVE: { + if (mActivePointerId == INACTIVE_POINTER_ID) break; + + int activePointerIndex = ev.findPointerIndex(mActivePointerId); + int y = (int) ev.getY(activePointerIndex); + int x = (int) ev.getX(activePointerIndex); + if (mActiveTaskView != null && + mTotalScrollMotion < mMaxScrollMotionToRejectSwipe && + Math.abs(x - mInitialMotionX) > Math.abs(y - mInitialMotionY) && + Math.abs(x - mInitialMotionX) > mSwipeTouchSlop) { + // Start swiping and stop scrolling + mIsScrolling = false; + mIsSwiping = true; + System.out.println("SWIPING: " + mActiveTaskView); + // Initialize the velocity tracker if necessary + initOrResetVelocityTracker(); + mVelocityTracker.addMovement(ev); + // Disallow parents from intercepting touch events + final ViewParent parent = mSv.getParent(); + if (parent != null) { + parent.requestDisallowInterceptTouchEvent(true); + } + // Enable HW layers + mSv.addHwLayersRefCount(); + } else if (Math.abs(y - mInitialMotionY) > mScrollTouchSlop) { + // Save the touch move info + mIsScrolling = true; + // Initialize the velocity tracker if necessary + initVelocityTrackerIfNotExists(); + mVelocityTracker.addMovement(ev); + // Disallow parents from intercepting touch events + final ViewParent parent = mSv.getParent(); + if (parent != null) { + parent.requestDisallowInterceptTouchEvent(true); + } + // Enable HW layers + mSv.addHwLayersRefCount(); + } + + mLastMotionX = x; + mLastMotionY = y; + break; + } + case MotionEvent.ACTION_CANCEL: + case MotionEvent.ACTION_UP: { + // Animate the scroll back if we've cancelled + mSv.animateBoundScroll(Constants.Values.TaskStackView.Animation.SnapScrollBackDuration); + // Reset the drag state and the velocity tracker + mIsScrolling = false; + mIsSwiping = false; + mActivePointerId = INACTIVE_POINTER_ID; + mActiveTaskView = null; + mTotalScrollMotion = 0; + recycleVelocityTracker(); + break; + } + } + + return wasScrolling || mIsScrolling || mIsSwiping; + } + + /** Handles touch events once we have intercepted them */ + public boolean onTouchEvent(MotionEvent ev) { + Console.log(Constants.DebugFlags.TaskStack.SynchronizeViewsWithModel, + "[TaskStackViewTouchHandler|touchEvent]", + Console.motionEventActionToString(ev.getAction()), Console.AnsiBlue); + + // Short circuit if we have no children + boolean hasChildren = (mSv.getChildCount() > 0); + if (!hasChildren) { + return false; + } + + // Update the velocity tracker + initVelocityTrackerIfNotExists(); + mVelocityTracker.addMovement(ev); + + int action = ev.getAction(); + switch (action & MotionEvent.ACTION_MASK) { + case MotionEvent.ACTION_DOWN: { + // Save the touch down info + mInitialMotionX = mLastMotionX = (int) ev.getX(); + mInitialMotionY = mLastMotionY = (int) ev.getY(); + mActivePointerId = ev.getPointerId(0); + mActiveTaskView = findViewAtPoint(mLastMotionX, mLastMotionY); + // Stop the current scroll if it is still flinging + mSv.mScroller.abortAnimation(); + mSv.abortBoundScrollAnimation(); + // Initialize the velocity tracker + initOrResetVelocityTracker(); + mVelocityTracker.addMovement(ev); + // XXX: Set mIsScrolling or mIsSwiping? + // Disallow parents from intercepting touch events + final ViewParent parent = mSv.getParent(); + if (parent != null) { + parent.requestDisallowInterceptTouchEvent(true); + } + break; + } + case MotionEvent.ACTION_MOVE: { + if (mActivePointerId == INACTIVE_POINTER_ID) break; + + int activePointerIndex = ev.findPointerIndex(mActivePointerId); + int x = (int) ev.getX(activePointerIndex); + int y = (int) ev.getY(activePointerIndex); + int deltaY = mLastMotionY - y; + int deltaX = x - mLastMotionX; + if (!mIsSwiping) { + if (mActiveTaskView != null && + mTotalScrollMotion < mMaxScrollMotionToRejectSwipe && + Math.abs(x - mInitialMotionX) > Math.abs(y - mInitialMotionY) && + Math.abs(x - mInitialMotionX) > mSwipeTouchSlop) { + mIsScrolling = false; + mIsSwiping = true; + System.out.println("SWIPING: " + mActiveTaskView); + // Initialize the velocity tracker if necessary + initOrResetVelocityTracker(); + mVelocityTracker.addMovement(ev); + // Disallow parents from intercepting touch events + final ViewParent parent = mSv.getParent(); + if (parent != null) { + parent.requestDisallowInterceptTouchEvent(true); + } + // Enable HW layers + mSv.addHwLayersRefCount(); + } + } + if (!mIsSwiping && !mIsScrolling) { + if (Math.abs(y - mInitialMotionY) > mScrollTouchSlop) { + mIsScrolling = true; + // Initialize the velocity tracker + initOrResetVelocityTracker(); + mVelocityTracker.addMovement(ev); + // Disallow parents from intercepting touch events + final ViewParent parent = mSv.getParent(); + if (parent != null) { + parent.requestDisallowInterceptTouchEvent(true); + } + // Enable HW layers + mSv.addHwLayersRefCount(); + } + } + if (mIsScrolling) { + mSv.setStackScroll(mSv.getStackScroll() + deltaY); + if (mSv.isScrollOutOfBounds()) { + mVelocityTracker.clear(); + } + } else if (mIsSwiping) { + mActiveTaskView.setTranslationX(mActiveTaskView.getTranslationX() + deltaX); + } + mLastMotionX = x; + mLastMotionY = y; + mTotalScrollMotion += Math.abs(deltaY); + break; + } + case MotionEvent.ACTION_UP: { + if (mIsScrolling || mIsSwiping) { + final TaskView activeTv = mActiveTaskView; + final VelocityTracker velocityTracker = mVelocityTracker; + velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); + + if (mIsSwiping) { + int initialVelocity = (int) velocityTracker.getXVelocity(mActivePointerId); + if ((Math.abs(initialVelocity) > mMinimumVelocity)) { + // Fling to dismiss + int newScrollX = (int) (Math.signum(initialVelocity) * + activeTv.getMeasuredWidth()); + int duration = Math.min(Constants.Values.TaskStackView.Animation.SwipeDismissDuration, + (int) (Math.abs(newScrollX - activeTv.getScrollX()) * + 1000f / Math.abs(initialVelocity))); + activeTv.animate() + .translationX(newScrollX) + .alpha(0f) + .setDuration(duration) + .setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + Task task = activeTv.getTask(); + Activity activity = (Activity) mSv.getContext(); + + // We have to disable the listener to ensure that we + // don't hit this again + activeTv.animate().setListener(null); + + // Remove the task from the view + mSv.mStack.removeTask(task); + + // Remove any stored data from the loader + RecentsTaskLoader loader = RecentsTaskLoader.getInstance(); + loader.deleteTaskData(task); + + // Remove the task from activity manager + final ActivityManager am = (ActivityManager) + activity.getSystemService(Context.ACTIVITY_SERVICE); + if (am != null) { + am.removeTask(activeTv.getTask().id, + ActivityManager.REMOVE_TASK_KILL_PROCESS); + } + + // If there are no remaining tasks, then just close the activity + if (mSv.mStack.getTaskCount() == 0) { + activity.finish(); + } + + // Disable HW layers + mSv.decHwLayersRefCount(); + } + }) + .start(); + // Enable HW layers + mSv.addHwLayersRefCount(); + } else { + // Animate it back into place + // XXX: Make this animation a function of the velocity OR distance + int duration = Constants.Values.TaskStackView.Animation.SwipeSnapBackDuration; + activeTv.animate() + .translationX(0) + .setDuration(duration) + .setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + // Disable HW layers + mSv.decHwLayersRefCount(); + } + }) + .start(); + // Enable HW layers + mSv.addHwLayersRefCount(); + } + } else { + int velocity = (int) velocityTracker.getYVelocity(mActivePointerId); + if ((Math.abs(velocity) > mMinimumVelocity)) { + Console.log(Constants.DebugFlags.UI.TouchEvents, + "[TaskStackViewTouchHandler|fling]", + "scroll: " + mSv.getStackScroll() + " velocity: " + velocity, + Console.AnsiGreen); + // Enable HW layers on the stack + mSv.addHwLayersRefCount(); + // Fling scroll + mSv.mScroller.fling(0, mSv.getStackScroll(), + 0, -velocity, + 0, 0, + mSv.mMinScroll, mSv.mMaxScroll, + 0, 0); + // Invalidate to kick off computeScroll + mSv.invalidate(); + } else if (mSv.isScrollOutOfBounds()) { + // Animate the scroll back into bounds + // XXX: Make this animation a function of the velocity OR distance + mSv.animateBoundScroll(Constants.Values.TaskStackView.Animation.SnapScrollBackDuration); + } + } + } + + mActivePointerId = INACTIVE_POINTER_ID; + mIsScrolling = false; + mIsSwiping = false; + mTotalScrollMotion = 0; + recycleVelocityTracker(); + // Disable HW layers + mSv.decHwLayersRefCount(); + break; + } + case MotionEvent.ACTION_CANCEL: { + if (mIsScrolling || mIsSwiping) { + if (mIsSwiping) { + // Animate it back into place + // XXX: Make this animation a function of the velocity OR distance + int duration = Constants.Values.TaskStackView.Animation.SwipeSnapBackDuration; + mActiveTaskView.animate() + .translationX(0) + .setDuration(duration) + .start(); + } else { + // Animate the scroll back into bounds + // XXX: Make this animation a function of the velocity OR distance + mSv.animateBoundScroll(Constants.Values.TaskStackView.Animation.SnapScrollBackDuration); + } + } + + mActivePointerId = INACTIVE_POINTER_ID; + mIsScrolling = false; + mIsSwiping = false; + mTotalScrollMotion = 0; + recycleVelocityTracker(); + // Disable HW layers + mSv.decHwLayersRefCount(); + break; + } + } + return true; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/TaskView.java b/packages/SystemUI/src/com/android/systemui/recents/views/TaskView.java new file mode 100644 index 0000000..b1d0d13 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskView.java @@ -0,0 +1,386 @@ +/* + * Copyright (C) 2014 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.views; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ObjectAnimator; +import android.animation.TimeInterpolator; +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.Point; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.Typeface; +import android.view.Gravity; +import android.view.View; +import android.view.animation.AccelerateDecelerateInterpolator; +import android.view.animation.AccelerateInterpolator; +import android.view.animation.DecelerateInterpolator; +import android.widget.FrameLayout; +import android.widget.ImageView; +import com.android.systemui.recents.Console; +import com.android.systemui.recents.Constants; +import com.android.systemui.recents.RecentsConfiguration; +import com.android.systemui.recents.model.Task; +import com.android.systemui.recents.model.TaskCallbacks; + +/** The TaskView callbacks */ +interface TaskViewCallbacks { + public void onTaskIconClicked(TaskView tv); + // public void onTaskViewReboundToTask(TaskView tv, Task t); +} + +/** The task thumbnail view */ +class TaskThumbnailView extends ImageView { + Task mTask; + int mBarColor; + + Path mRoundedRectClipPath = new Path(); + + public TaskThumbnailView(Context context) { + super(context); + setScaleType(ScaleType.FIT_XY); + } + + /** Binds the thumbnail view to the task */ + void rebindToTask(Task t, boolean animate) { + mTask = t; + if (t.thumbnail != null) { + // Update the bar color + if (Constants.Values.TaskView.DrawColoredTaskBars) { + int[] colors = {0xFFCC0C39, 0xFFE6781E, 0xFFC8CF02, 0xFF1693A7}; + mBarColor = colors[mTask.intent.getComponent().getPackageName().length() % colors.length]; + } + + setImageBitmap(t.thumbnail); + if (animate) { + setAlpha(0f); + animate().alpha(1f) + .setDuration(Constants.Values.TaskView.Animation.TaskDataUpdatedFadeDuration) + .start(); + } + } + } + + /** Unbinds the thumbnail view from the task */ + void unbindFromTask() { + mTask = null; + setImageDrawable(null); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + + // Update the rounded rect clip path + RecentsConfiguration config = RecentsConfiguration.getInstance(); + float radius = config.pxFromDp(Constants.Values.TaskView.RoundedCornerRadiusDps); + mRoundedRectClipPath.reset(); + mRoundedRectClipPath.addRoundRect(new RectF(0, 0, getMeasuredWidth(), getMeasuredHeight()), + radius, radius, Path.Direction.CW); + } + + @Override + protected void onDraw(Canvas canvas) { + if (Constants.Values.TaskView.UseRoundedCorners) { + canvas.clipPath(mRoundedRectClipPath); + } + + super.onDraw(canvas); + + if (Constants.Values.TaskView.DrawColoredTaskBars) { + RecentsConfiguration config = RecentsConfiguration.getInstance(); + int taskBarHeight = config.pxFromDp(Constants.Values.TaskView.TaskBarHeightDps); + // XXX: If we actually use this, this should be pulled out into a TextView that we + // inflate + + // Draw the task bar + Rect r = new Rect(); + Paint p = new Paint(); + p.setAntiAlias(true); + p.setSubpixelText(true); + p.setColor(mBarColor); + p.setTypeface(Typeface.create("sans-serif-light", Typeface.NORMAL)); + canvas.drawRect(0, 0, getMeasuredWidth(), taskBarHeight, p); + p.setColor(0xFFffffff); + p.setTextSize(68); + p.getTextBounds("X", 0, 1, r); + int offset = (int) (taskBarHeight - r.height()) / 2; + canvas.drawText(mTask.title, offset, offset + r.height(), p); + } + } +} + +/* The task icon view */ +class TaskIconView extends ImageView { + Task mTask; + + Path mClipPath = new Path(); + float mClipRadius; + Point mClipOrigin = new Point(); + ObjectAnimator mCircularClipAnimator; + + public TaskIconView(Context context) { + super(context); + mClipPath = new Path(); + mClipRadius = 1f; + } + + /** Binds the icon view to the task */ + void rebindToTask(Task t, boolean animate) { + mTask = t; + if (t.icon != null) { + setImageDrawable(t.icon); + if (animate) { + setAlpha(0f); + animate().alpha(1f) + .setDuration(Constants.Values.TaskView.Animation.TaskDataUpdatedFadeDuration) + .start(); + } + } + } + + /** Unbinds the icon view from the task */ + void unbindFromTask() { + mTask = null; + setImageDrawable(null); + } + + /** Sets the circular clip radius on the icon */ + public void setCircularClipRadius(float r) { + Console.log(Constants.DebugFlags.UI.Clipping, "[TaskView|setCircularClip]", "" + r); + mClipRadius = r; + invalidate(); + } + + /** Gets the circular clip radius on the icon */ + public float getCircularClipRadius() { + return mClipRadius; + } + + /** Animates the circular clip radius on the icon */ + void animateCircularClip(boolean brNotTl, float newRadius, int duration, int startDelay, + TimeInterpolator interpolator, + AnimatorListenerAdapter listener) { + if (mCircularClipAnimator != null) { + mCircularClipAnimator.cancel(); + mCircularClipAnimator.removeAllListeners(); + } + if (brNotTl) { + mClipOrigin.set(0, 0); + } else { + mClipOrigin.set(getMeasuredWidth(), getMeasuredHeight()); + } + mCircularClipAnimator = ObjectAnimator.ofFloat(this, "circularClipRadius", newRadius); + mCircularClipAnimator.setStartDelay(startDelay); + mCircularClipAnimator.setDuration(duration); + mCircularClipAnimator.setInterpolator(interpolator); + if (listener != null) { + mCircularClipAnimator.addListener(listener); + } + mCircularClipAnimator.start(); + } + + @Override + protected void onDraw(Canvas canvas) { + int saveCount = canvas.save(Canvas.CLIP_SAVE_FLAG); + int width = getMeasuredWidth(); + int height = getMeasuredHeight(); + int maxSize = (int) Math.ceil(Math.sqrt(width * width + height * height)); + mClipPath.reset(); + mClipPath.addCircle(mClipOrigin.x, mClipOrigin.y, mClipRadius * maxSize, Path.Direction.CW); + canvas.clipPath(mClipPath); + super.onDraw(canvas); + canvas.restoreToCount(saveCount); + } +} + +/* A task view */ +public class TaskView extends FrameLayout implements View.OnClickListener, TaskCallbacks { + Task mTask; + TaskThumbnailView mThumbnailView; + TaskIconView mIconView; + TaskViewCallbacks mCb; + + public TaskView(Context context) { + super(context); + mThumbnailView = new TaskThumbnailView(context); + mIconView = new TaskIconView(context); + mIconView.setOnClickListener(this); + addView(mThumbnailView); + addView(mIconView); + + RecentsConfiguration config = RecentsConfiguration.getInstance(); + int barHeight = config.pxFromDp(Constants.Values.TaskView.TaskBarHeightDps); + int iconSize = config.pxFromDp(Constants.Values.TaskView.TaskIconSizeDps); + int offset = barHeight - (iconSize / 2); + + // XXX: Lets keep the icon in the corner for the time being + offset = iconSize / 4; + + /* + ((LayoutParams) mThumbnailView.getLayoutParams()).leftMargin = barHeight / 2; + ((LayoutParams) mThumbnailView.getLayoutParams()).rightMargin = barHeight / 2; + ((LayoutParams) mThumbnailView.getLayoutParams()).bottomMargin = barHeight; + */ + ((LayoutParams) mIconView.getLayoutParams()).gravity = Gravity.END; + ((LayoutParams) mIconView.getLayoutParams()).width = iconSize; + ((LayoutParams) mIconView.getLayoutParams()).height = iconSize; + ((LayoutParams) mIconView.getLayoutParams()).topMargin = offset; + ((LayoutParams) mIconView.getLayoutParams()).rightMargin = offset; + } + + /** Set the task and callback */ + void bindToTask(Task t, TaskViewCallbacks cb) { + mTask = t; + mTask.setCallbacks(this); + mCb = cb; + } + + /** Actually synchronizes the model data into the views */ + void syncToTask() { + mThumbnailView.rebindToTask(mTask, false); + mIconView.rebindToTask(mTask, false); + } + + /** Unset the task and callback */ + void unbindFromTask() { + mTask.setCallbacks(null); + mThumbnailView.unbindFromTask(); + mIconView.unbindFromTask(); + } + + /** Gets the task */ + Task getTask() { + return mTask; + } + + /** Synchronizes this view's properties with the task's transform */ + void updateViewPropertiesFromTask(TaskViewTransform animateFromTransform, + TaskViewTransform transform, int duration) { + if (duration > 0) { + if (animateFromTransform != null) { + setTranslationY(animateFromTransform.translationY); + setScaleX(animateFromTransform.scale); + setScaleY(animateFromTransform.scale); + } + animate().translationY(transform.translationY) + .scaleX(transform.scale) + .scaleY(transform.scale) + .setDuration(duration) + .setInterpolator(new AccelerateDecelerateInterpolator()) + .start(); + } else { + setTranslationY(transform.translationY); + setScaleX(transform.scale); + setScaleY(transform.scale); + } + } + + /** Resets this view's properties */ + void resetViewProperties() { + setTranslationX(0f); + setTranslationY(0f); + setScaleX(1f); + setScaleY(1f); + setAlpha(1f); + } + + /** Animates this task view as it enters recents */ + public void animateOnEnterRecents() { + mIconView.setCircularClipRadius(0f); + mIconView.animateCircularClip(true, 1f, + Constants.Values.TaskView.Animation.TaskIconCircularClipInDuration, + 300, new AccelerateInterpolator(), null); + } + + /** Animates this task view as it exits recents */ + public void animateOnLeavingRecents(final Runnable r) { + if (Constants.Values.TaskView.AnimateFrontTaskIconOnLeavingUseClip) { + mIconView.animateCircularClip(false, 0f, + Constants.Values.TaskView.Animation.TaskIconCircularClipOutDuration, 0, + new DecelerateInterpolator(), + new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + r.run(); + } + }); + } else { + mIconView.animate() + .alpha(0f) + .setDuration(Constants.Values.TaskView.Animation.TaskIconCircularClipOutDuration) + .setInterpolator(new DecelerateInterpolator()) + .setListener( + new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + r.run(); + } + }) + .start(); + } + } + + /** Returns the rect we want to clip (it may not be the full rect) */ + Rect getClippingRect(Rect outRect, boolean accountForRoundedRects) { + getHitRect(outRect); + // XXX: We should get the hit rect of the thumbnail view and intersect, but this is faster + outRect.right = outRect.left + mThumbnailView.getRight(); + outRect.bottom = outRect.top + mThumbnailView.getBottom(); + // We need to shrink the next rect by the rounded corners since those are draw on + // top of the current view + if (accountForRoundedRects) { + RecentsConfiguration config = RecentsConfiguration.getInstance(); + float radius = config.pxFromDp(Constants.Values.TaskView.RoundedCornerRadiusDps); + outRect.inset((int) radius, (int) radius); + } + return outRect; + } + + /** Enable the hw layers on this task view */ + void enableHwLayers() { + Console.log(Constants.DebugFlags.UI.HwLayers, "[TaskView|enableHwLayers]"); + mThumbnailView.setLayerType(View.LAYER_TYPE_HARDWARE, null); + } + + /** Disable the hw layers on this task view */ + void disableHwLayers() { + Console.log(Constants.DebugFlags.UI.HwLayers, "[TaskView|disableHwLayers]"); + mThumbnailView.setLayerType(View.LAYER_TYPE_NONE, null); + } + + @Override + public void onTaskDataChanged(Task task) { + Console.log(Constants.DebugFlags.App.EnableBackgroundTaskLoading, + "[TaskView|onTaskDataChanged]", task); + + // Only update this task view if the changed task is the same as the task for this view + if (mTask == task) { + mThumbnailView.rebindToTask(mTask, true); + mIconView.rebindToTask(mTask, true); + } + } + + @Override + public void onClick(View v) { + mCb.onTaskIconClicked(this); + } +}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/TaskViewTransform.java b/packages/SystemUI/src/com/android/systemui/recents/views/TaskViewTransform.java new file mode 100644 index 0000000..66c52a0 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskViewTransform.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2014 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.views; + +import android.graphics.Rect; + + +/* The transform state for a task view */ +public class TaskViewTransform { + public int translationY = 0; + public float scale = 1f; + public boolean visible = true; + public Rect rect = new Rect(); + float t; + + @Override + public String toString() { + return "TaskViewTransform y: " + translationY + " scale: " + scale + + " visible: " + visible + " rect: " + rect; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/ViewPool.java b/packages/SystemUI/src/com/android/systemui/recents/views/ViewPool.java new file mode 100644 index 0000000..f7d7095 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/recents/views/ViewPool.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2014 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.views; + +import android.content.Context; + +import java.util.Iterator; +import java.util.LinkedList; + + +/* A view pool to manage more views than we can visibly handle */ +public class ViewPool<V, T> { + Context mContext; + ViewPoolConsumer<V, T> mViewCreator; + LinkedList<V> mPool = new LinkedList<V>(); + + /** Initializes the pool with a fixed predetermined pool size */ + public ViewPool(Context context, ViewPoolConsumer<V, T> viewCreator) { + mContext = context; + mViewCreator = viewCreator; + } + + /** Returns a view into the pool */ + void returnViewToPool(V v) { + mViewCreator.prepareViewToEnterPool(v); + mPool.push(v); + } + + /** Gets a view from the pool and prepares it */ + V pickUpViewFromPool(T preferredData, T prepareData) { + V v = null; + boolean isNewView = false; + if (mPool.isEmpty()) { + v = mViewCreator.createView(mContext); + isNewView = true; + } else { + // Try and find a preferred view + Iterator<V> iter = mPool.iterator(); + while (iter.hasNext()) { + V vpv = iter.next(); + if (mViewCreator.hasPreferredData(vpv, preferredData)) { + v = vpv; + iter.remove(); + break; + } + } + // Otherwise, just grab the first view + if (v == null) { + v = mPool.pop(); + } + } + mViewCreator.prepareViewToLeavePool(v, prepareData, isNewView); + return v; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/ViewPoolConsumer.java b/packages/SystemUI/src/com/android/systemui/recents/views/ViewPoolConsumer.java new file mode 100644 index 0000000..50f45bf --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/recents/views/ViewPoolConsumer.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2014 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.views; + +import android.content.Context; + + +/* An interface to the consumer of a view pool */ +public interface ViewPoolConsumer<V, T> { + public V createView(Context context); + public void prepareViewToEnterPool(V v); + public void prepareViewToLeavePool(V v, T prepareData, boolean isNewView); + public boolean hasPreferredData(V v, T preferredData); +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java index fb11743..a89921f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java @@ -28,9 +28,15 @@ import android.content.IntentFilter; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; +import android.content.res.ColorStateList; import android.content.res.Configuration; import android.database.ContentObserver; +import android.graphics.Color; +import android.graphics.PorterDuff; import android.graphics.Rect; +import android.graphics.drawable.AnimationDrawable; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Build; import android.os.Handler; @@ -44,9 +50,13 @@ import android.provider.Settings; import android.service.dreams.DreamService; import android.service.dreams.IDreamManager; import android.service.notification.StatusBarNotification; +import android.text.SpannableStringBuilder; +import android.text.Spanned; import android.text.TextUtils; +import android.text.style.TextAppearanceSpan; import android.util.Log; import android.util.SparseBooleanArray; +import android.view.ContextThemeWrapper; import android.view.Display; import android.view.IWindowManager; import android.view.LayoutInflater; @@ -57,6 +67,7 @@ import android.view.ViewGroup; import android.view.ViewGroup.LayoutParams; import android.view.WindowManager; import android.view.WindowManagerGlobal; +import android.widget.Button; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.PopupMenu; @@ -67,6 +78,7 @@ import com.android.internal.statusbar.IStatusBarService; import com.android.internal.statusbar.StatusBarIcon; import com.android.internal.statusbar.StatusBarIconList; import com.android.internal.widget.SizeAdaptiveLayout; +import com.android.systemui.ImageUtils; import com.android.systemui.R; import com.android.systemui.RecentsComponent; import com.android.systemui.SearchPanelView; @@ -76,6 +88,7 @@ import com.android.systemui.statusbar.policy.NotificationRowLayout; import java.util.ArrayList; import java.util.Locale; +import java.util.Stack; public abstract class BaseStatusBar extends SystemUI implements CommandQueue.Callbacks { @@ -140,6 +153,8 @@ public abstract class BaseStatusBar extends SystemUI implements // public mode, private notifications, etc private boolean mLockscreenPublicMode = false; private final SparseBooleanArray mUsersAllowingPrivateNotifications = new SparseBooleanArray(); + private Context mLightThemeContext; + private ImageUtils mImageUtils = new ImageUtils(); // UI-specific methods @@ -261,6 +276,8 @@ public abstract class BaseStatusBar extends SystemUI implements true, mLockscreenSettingsObserver, UserHandle.USER_ALL); + mLightThemeContext = new RemoteViewsThemeContextWrapper(mContext, + android.R.style.Theme_Holo_Light); mBarService = IStatusBarService.Stub.asInterface( ServiceManager.getService(Context.STATUS_BAR_SERVICE)); @@ -412,6 +429,158 @@ public abstract class BaseStatusBar extends SystemUI implements } } + private void processLegacyHoloNotification(StatusBarNotification sbn, View content) { + + // TODO: Also skip processing if it is a holo-style notification. + // If the notification is custom, we can't process it. + if (!sbn.getNotification().extras.getBoolean(Notification.EXTRA_BUILDER_REMOTE_VIEWS)) { + return; + } + + processLegacyHoloLargeIcon(content); + processLegacyHoloActions(content); + processLegacyNotificationIcon(content); + processLegacyTextViews(content); + } + + /** + * @return the context to be used for the inflation of the specified {@code sbn}; this is + * dependent whether the notification is quantum-style or holo-style + */ + private Context getInflationContext(StatusBarNotification sbn) { + + // TODO: Adjust this logic when we change the theme of the status bar windows. + if (sbn.getNotification().extras.getBoolean(Notification.EXTRA_BUILDER_REMOTE_VIEWS)) { + return mLightThemeContext; + } else { + return mContext; + } + } + + private void processLegacyNotificationIcon(View content) { + View v = content.findViewById(com.android.internal.R.id.right_icon); + if (v != null & v instanceof ImageView) { + ImageView iv = (ImageView) v; + Drawable d = iv.getDrawable(); + if (isMonochrome(d)) { + d.mutate(); + d.setColorFilter(mLightThemeContext.getResources().getColor( + R.color.notification_action_legacy_color_filter), PorterDuff.Mode.MULTIPLY); + } + } + } + + private void processLegacyHoloLargeIcon(View content) { + View v = content.findViewById(com.android.internal.R.id.icon); + if (v != null & v instanceof ImageView) { + ImageView iv = (ImageView) v; + if (isMonochrome(iv.getDrawable())) { + iv.setBackground(mLightThemeContext.getResources().getDrawable( + R.drawable.notification_icon_legacy_bg_inset)); + } + } + } + + private boolean isMonochrome(Drawable d) { + if (d == null) { + return false; + } else if (d instanceof BitmapDrawable) { + BitmapDrawable bd = (BitmapDrawable) d; + return bd.getBitmap() != null && mImageUtils.isGrayscale(bd.getBitmap()); + } else if (d instanceof AnimationDrawable) { + AnimationDrawable ad = (AnimationDrawable) d; + int count = ad.getNumberOfFrames(); + return count > 0 && isMonochrome(ad.getFrame(0)); + } else { + return false; + } + } + + private void processLegacyHoloActions(View content) { + View v = content.findViewById(com.android.internal.R.id.actions); + if (v != null & v instanceof ViewGroup) { + ViewGroup vg = (ViewGroup) v; + int childCount = vg.getChildCount(); + for (int i = 0; i < childCount; i++) { + View child = vg.getChildAt(i); + if (child instanceof Button) { + Button button = (Button) child; + Drawable[] compoundDrawables = button.getCompoundDrawablesRelative(); + if (isMonochrome(compoundDrawables[0])) { + Drawable d = compoundDrawables[0]; + d.mutate(); + d.setColorFilter(mLightThemeContext.getResources().getColor( + R.color.notification_action_legacy_color_filter), + PorterDuff.Mode.MULTIPLY); + } + } + } + } + } + + private void processLegacyTextViews(View content) { + Stack<View> viewStack = new Stack<View>(); + viewStack.push(content); + while(!viewStack.isEmpty()) { + View current = viewStack.pop(); + if(current instanceof ViewGroup){ + ViewGroup currentGroup = (ViewGroup) current; + int numChildren = currentGroup.getChildCount(); + for(int i=0;i<numChildren;i++){ + viewStack.push(currentGroup.getChildAt(i)); + } + } + if (current instanceof TextView) { + processLegacyTextView((TextView) current); + } + } + } + + private void processLegacyTextView(TextView textView) { + if (textView.getText() instanceof Spanned) { + Spanned ss = (Spanned) textView.getText(); + Object[] spans = ss.getSpans(0, ss.length(), Object.class); + SpannableStringBuilder builder = new SpannableStringBuilder(ss.toString()); + for (Object span : spans) { + Object resultSpan = span; + if (span instanceof TextAppearanceSpan) { + resultSpan = processTextAppearanceSpan((TextAppearanceSpan) span); + } + builder.setSpan(resultSpan, ss.getSpanStart(span), ss.getSpanEnd(span), + ss.getSpanFlags(span)); + } + textView.setText(builder); + } + } + + private TextAppearanceSpan processTextAppearanceSpan(TextAppearanceSpan span) { + ColorStateList colorStateList = span.getTextColor(); + if (colorStateList != null) { + int[] colors = colorStateList.getColors(); + boolean changed = false; + for (int i = 0; i < colors.length; i++) { + if (mImageUtils.isGrayscale(colors[i])) { + colors[i] = processColor(colors[i]); + changed = true; + } + } + if (changed) { + return new TextAppearanceSpan( + span.getFamily(), span.getTextStyle(), span.getTextSize(), + new ColorStateList(colorStateList.getStates(), colors), + span.getLinkTextColor()); + } + } + return span; + } + + private int processColor(int color) { + return Color.argb(Color.alpha(color), + 255 - Color.red(color), + 255 - Color.green(color), + 255 - Color.blue(color)); + } + private void startApplicationDetailsActivity(String packageName) { Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, Uri.fromParts("package", packageName, null)); @@ -748,9 +917,11 @@ public abstract class BaseStatusBar extends SystemUI implements View contentViewLocal = null; View bigContentViewLocal = null; try { - contentViewLocal = contentView.apply(mContext, expanded, mOnClickHandler); + contentViewLocal = contentView.apply(getInflationContext(sbn), expanded, + mOnClickHandler); if (bigContentView != null) { - bigContentViewLocal = bigContentView.apply(mContext, expanded, mOnClickHandler); + bigContentViewLocal = bigContentView.apply(getInflationContext(sbn), expanded, + mOnClickHandler); } } catch (RuntimeException e) { @@ -760,6 +931,7 @@ public abstract class BaseStatusBar extends SystemUI implements } if (contentViewLocal != null) { + contentViewLocal.setIsRootNamespace(true); SizeAdaptiveLayout.LayoutParams params = new SizeAdaptiveLayout.LayoutParams(contentViewLocal.getLayoutParams()); params.minHeight = minHeight; @@ -767,6 +939,7 @@ public abstract class BaseStatusBar extends SystemUI implements expanded.addView(contentViewLocal, params); } if (bigContentViewLocal != null) { + bigContentViewLocal.setIsRootNamespace(true); SizeAdaptiveLayout.LayoutParams params = new SizeAdaptiveLayout.LayoutParams(bigContentViewLocal.getLayoutParams()); params.minHeight = minHeight+1; @@ -780,10 +953,11 @@ public abstract class BaseStatusBar extends SystemUI implements View publicViewLocal = null; if (publicNotification != null) { try { - publicViewLocal = publicNotification.contentView.apply(mContext, + publicViewLocal = publicNotification.contentView.apply(getInflationContext(sbn), expandedPublic, mOnClickHandler); if (publicViewLocal != null) { + publicViewLocal.setIsRootNamespace(true); SizeAdaptiveLayout.LayoutParams params = new SizeAdaptiveLayout.LayoutParams(publicViewLocal.getLayoutParams()); params.minHeight = minHeight; @@ -831,6 +1005,13 @@ public abstract class BaseStatusBar extends SystemUI implements row.setDrawingCacheEnabled(true); applyLegacyRowBackground(sbn, content); + processLegacyHoloNotification(sbn, contentViewLocal); + if (bigContentViewLocal != null) { + processLegacyHoloNotification(sbn, bigContentViewLocal); + } + if (publicViewLocal != null) { + processLegacyHoloNotification(sbn, publicViewLocal); + } if (MULTIUSER_DEBUG) { TextView debug = (TextView) row.findViewById(R.id.debug_info); @@ -1245,12 +1426,17 @@ public abstract class BaseStatusBar extends SystemUI implements : null; // Reapply the RemoteViews - contentView.reapply(mContext, entry.expanded, mOnClickHandler); + contentView.reapply(getInflationContext(notification), entry.expanded, mOnClickHandler); + processLegacyHoloNotification(notification, entry.expanded); if (bigContentView != null && entry.getBigContentView() != null) { - bigContentView.reapply(mContext, entry.getBigContentView(), mOnClickHandler); + bigContentView.reapply(getInflationContext(notification), entry.getBigContentView(), + mOnClickHandler); + processLegacyHoloNotification(notification, entry.getBigContentView()); } if (publicContentView != null && entry.getPublicContentView() != null) { - publicContentView.reapply(mContext, entry.getPublicContentView(), mOnClickHandler); + publicContentView.reapply(getInflationContext(notification), + entry.getPublicContentView(), mOnClickHandler); + processLegacyHoloNotification(notification, entry.getPublicContentView()); } // update the contentIntent final PendingIntent contentIntent = notification.getNotification().contentIntent; @@ -1330,4 +1516,35 @@ public abstract class BaseStatusBar extends SystemUI implements } mContext.unregisterReceiver(mBroadcastReceiver); } + + /** + * A custom context theme wrapper that applies a platform theme to a created package context. + * This is useful if you want to inflate {@link RemoteViews} with a custom theme (normally, the + * theme used there is the default platform theme). + */ + private static class RemoteViewsThemeContextWrapper extends ContextThemeWrapper { + + private int mThemeRes; + + private RemoteViewsThemeContextWrapper(Context base, int themeres) { + super(base, themeres); + mThemeRes = themeres; + } + + @Override + public Context createPackageContextAsUser(String packageName, int flags, UserHandle user) + throws NameNotFoundException { + Context c = super.createPackageContextAsUser(packageName, flags, user); + c.setTheme(mThemeRes); + return c; + } + + @Override + public Context createPackageContext(String packageName, int flags) + throws NameNotFoundException { + Context c = super.createPackageContext(packageName, flags); + c.setTheme(mThemeRes); + return c; + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/QuickSettingsModel.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/QuickSettingsModel.java index 48ee1ce..174cad8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/QuickSettingsModel.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/QuickSettingsModel.java @@ -996,8 +996,8 @@ class QuickSettingsModel implements BluetoothStateChangeCallback, mInversionState.toggled = enabled; mInversionState.type = type; // TODO: Add real icon assets. - mInversionState.iconId = enabled ? R.drawable.ic_qs_bluetooth_on - : R.drawable.ic_qs_bluetooth_off; + mInversionState.iconId = enabled ? R.drawable.ic_qs_inversion_on + : R.drawable.ic_qs_inversion_off; mInversionState.label = res.getString(R.string.quick_settings_inversion_label); mInversionCallback.refreshView(mInversionTile, mInversionState); } @@ -1026,8 +1026,8 @@ class QuickSettingsModel implements BluetoothStateChangeCallback, mContrastState.contrast = contrast; mContrastState.brightness = brightness; // TODO: Add real icon assets. - mContrastState.iconId = enabled ? R.drawable.ic_qs_bluetooth_on - : R.drawable.ic_qs_bluetooth_off; + mContrastState.iconId = enabled ? R.drawable.ic_qs_contrast_on + : R.drawable.ic_qs_contrast_off; mContrastState.label = res.getString(R.string.quick_settings_contrast_label); mContrastCallback.refreshView(mContrastTile, mContrastState); } @@ -1053,8 +1053,8 @@ class QuickSettingsModel implements BluetoothStateChangeCallback, mColorSpaceState.toggled = enabled; mColorSpaceState.type = type; // TODO: Add real icon assets. - mColorSpaceState.iconId = enabled ? R.drawable.ic_qs_bluetooth_on - : R.drawable.ic_qs_bluetooth_off; + mColorSpaceState.iconId = enabled ? R.drawable.ic_qs_color_space_on + : R.drawable.ic_qs_color_space_off; mColorSpaceState.label = res.getString(R.string.quick_settings_color_space_label); mColorSpaceCallback.refreshView(mColorSpaceTile, mColorSpaceState); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ZenModeView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ZenModeView.java index 783e371..fa7f96a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ZenModeView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ZenModeView.java @@ -28,6 +28,8 @@ import android.graphics.Rect; import android.graphics.Typeface; import android.graphics.drawable.ShapeDrawable; import android.graphics.drawable.shapes.PathShape; +import android.os.AsyncTask; +import android.os.Vibrator; import android.text.Spannable; import android.text.SpannableStringBuilder; import android.text.TextPaint; @@ -45,7 +47,6 @@ import android.widget.ArrayAdapter; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.ListView; -import android.widget.PopupWindow; import android.widget.RelativeLayout; import android.widget.Spinner; import android.widget.TextView; @@ -79,6 +80,7 @@ public class ZenModeView extends RelativeLayout { private final Rect mLayoutRect = new Rect(); private final UntilPager mUntilPager; private final AlarmWarning mAlarmWarning; + private final int mPopDuration; private float mDownY; private int mDownBottom; @@ -87,6 +89,7 @@ public class ZenModeView extends RelativeLayout { private int mBottom; private int mWidthSpec; private Adapter mAdapter; + private boolean mPopped; public ZenModeView(Context context) { this(context, null); @@ -144,7 +147,6 @@ public class ZenModeView extends RelativeLayout { lp.addRule(RelativeLayout.CENTER_HORIZONTAL); addView(mModeSpinner, lp); - mUntilPager = new UntilPager(mContext, mPathPaint, iconSize); mUntilPager.setId(android.R.id.tabhost); mUntilPager.setAlpha(0); @@ -165,6 +167,8 @@ public class ZenModeView extends RelativeLayout { mHintText.setGravity(Gravity.CENTER); mHintText.setTextColor(GRAY); addView(mHintText, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); + + mPopDuration = mContext.getResources().getInteger(R.integer.blinds_pop_duration_ms); } private boolean isApplicable() { @@ -180,9 +184,11 @@ public class ZenModeView extends RelativeLayout { public void onAnimationUpdate(ValueAnimator animation) { final float f = animation.getAnimatedFraction(); final int hintBottom = mHintText.getBottom(); - setPeeked(hintBottom + (int)((1-f) * (startBottom - hintBottom)), max); - if (f == 1) { + final boolean isDone = f == 1; + setPeeked(hintBottom + (int)((1-f) * (startBottom - hintBottom)), max, isDone); + if (isDone) { mPeekable = true; + mPopped = false; mClosing = false; mModeSpinner.updateState(); if (mAdapter != null) { @@ -261,10 +267,9 @@ public class ZenModeView extends RelativeLayout { protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { if (DEBUG) log("onMeasure %s %s", MeasureSpec.toString(widthMeasureSpec), MeasureSpec.toString(heightMeasureSpec)); - if (MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY) { - throw new UnsupportedOperationException("Width must be exact"); - } - if (widthMeasureSpec != mWidthSpec) { + final boolean widthExact = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY; + + if (!widthExact || (widthMeasureSpec != mWidthSpec)) { if (DEBUG) log(" super.onMeasure"); final int hms = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); super.onMeasure(widthMeasureSpec, hms); @@ -336,11 +341,15 @@ public class ZenModeView extends RelativeLayout { return true; } else if (event.getAction() == MotionEvent.ACTION_MOVE) { final float dy = event.getY() - mDownY; - setPeeked(mDownBottom + (int)dy, getExpandedBottom()); + if (!mPopped) { + mPopped = true; + AsyncTask.execute(mPopVibration); + } + setPeeked(mDownBottom + (int)dy, getExpandedBottom(), false); } else if (event.getAction() == MotionEvent.ACTION_UP || event.getAction() == MotionEvent.ACTION_CANCEL) { final float dy = event.getY() - mDownY; - setPeeked(mDownBottom + (int)dy, getExpandedBottom()); + setPeeked(mDownBottom + (int)dy, getExpandedBottom(), true); if (mPeekable) { close(); } @@ -348,14 +357,14 @@ public class ZenModeView extends RelativeLayout { return rt; } - private void setPeeked(int peeked, int max) { + private void setPeeked(int peeked, int max, boolean isDone) { if (DEBUG) log("setPeeked=" + peeked); final int min = mHintText.getBottom(); peeked = Math.max(min, Math.min(peeked, max)); - if (mBottom == peeked) { + if (!isDone && mBottom == peeked) { return; } - if (peeked == max) { + if (peeked == max && isDone) { mPeekable = false; mModeSpinner.setEnabled(true); if (mAdapter != null) { @@ -420,6 +429,14 @@ public class ZenModeView extends RelativeLayout { }).start(); } + private final Runnable mPopVibration = new Runnable() { + @Override + public void run() { + Vibrator v = (Vibrator) mContext.getSystemService(Context.VIBRATOR_SERVICE); + v.vibrate(mPopDuration); + } + }; + private final class UntilPager extends RelativeLayout { private final ImageView mPrev; private final ImageView mNext; |
