diff options
57 files changed, 1329 insertions, 372 deletions
diff --git a/api/current.txt b/api/current.txt index b73214f79d32..622a8b7b203b 100644 --- a/api/current.txt +++ b/api/current.txt @@ -2114,22 +2114,6 @@ package android { field public static final int Theme_Light_Panel = 16973914; // 0x103005a field public static final int Theme_Light_WallpaperSettings = 16973922; // 0x1030062 field public static final int Theme_Material = 16974372; // 0x1030224 - field public static final int Theme_Material_DayNight = 16974552; // 0x10302d8 - field public static final int Theme_Material_DayNight_DarkActionBar = 16974553; // 0x10302d9 - field public static final int Theme_Material_DayNight_Dialog = 16974554; // 0x10302da - field public static final int Theme_Material_DayNight_DialogWhenLarge = 16974560; // 0x10302e0 - field public static final int Theme_Material_DayNight_DialogWhenLarge_DarkActionBar = 16974568; // 0x10302e8 - field public static final int Theme_Material_DayNight_DialogWhenLarge_NoActionBar = 16974561; // 0x10302e1 - field public static final int Theme_Material_DayNight_Dialog_Alert = 16974555; // 0x10302db - field public static final int Theme_Material_DayNight_Dialog_MinWidth = 16974556; // 0x10302dc - field public static final int Theme_Material_DayNight_Dialog_NoActionBar = 16974557; // 0x10302dd - field public static final int Theme_Material_DayNight_Dialog_NoActionBar_MinWidth = 16974558; // 0x10302de - field public static final int Theme_Material_DayNight_Dialog_Presentation = 16974559; // 0x10302df - field public static final int Theme_Material_DayNight_NoActionBar = 16974562; // 0x10302e2 - field public static final int Theme_Material_DayNight_NoActionBar_Fullscreen = 16974563; // 0x10302e3 - field public static final int Theme_Material_DayNight_NoActionBar_Overscan = 16974564; // 0x10302e4 - field public static final int Theme_Material_DayNight_NoActionBar_TranslucentDecor = 16974565; // 0x10302e5 - field public static final int Theme_Material_DayNight_Panel = 16974566; // 0x10302e6 field public static final int Theme_Material_Dialog = 16974373; // 0x1030225 field public static final int Theme_Material_DialogWhenLarge = 16974379; // 0x103022b field public static final int Theme_Material_DialogWhenLarge_NoActionBar = 16974380; // 0x103022c @@ -2143,7 +2127,7 @@ package android { field public static final int Theme_Material_Light_DarkActionBar = 16974392; // 0x1030238 field public static final int Theme_Material_Light_Dialog = 16974393; // 0x1030239 field public static final int Theme_Material_Light_DialogWhenLarge = 16974399; // 0x103023f - field public static final int Theme_Material_Light_DialogWhenLarge_DarkActionBar = 16974567; // 0x10302e7 + field public static final int Theme_Material_Light_DialogWhenLarge_DarkActionBar = 16974552; // 0x10302d8 field public static final int Theme_Material_Light_DialogWhenLarge_NoActionBar = 16974400; // 0x1030240 field public static final int Theme_Material_Light_Dialog_Alert = 16974394; // 0x103023a field public static final int Theme_Material_Light_Dialog_MinWidth = 16974395; // 0x103023b @@ -3374,6 +3358,7 @@ package android.app { method public boolean dispatchTouchEvent(android.view.MotionEvent); method public boolean dispatchTrackballEvent(android.view.MotionEvent); method public void dump(java.lang.String, java.io.FileDescriptor, java.io.PrintWriter, java.lang.String[]); + method public void enterPictureInPictureMode(); method public android.view.View findViewById(int); method public void finish(); method public void finishActivity(int); @@ -10605,6 +10590,7 @@ package android.database.sqlite { method public void setVersion(int); method public int update(java.lang.String, android.content.ContentValues, java.lang.String, java.lang.String[]); method public int updateWithOnConflict(java.lang.String, android.content.ContentValues, java.lang.String, java.lang.String[], int); + method public void validateSql(java.lang.String, android.os.CancellationSignal); method public deprecated boolean yieldIfContended(); method public boolean yieldIfContendedSafely(); method public boolean yieldIfContendedSafely(long); diff --git a/api/system-current.txt b/api/system-current.txt index 84dbe6254b91..e51b7d58ac7b 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -2215,22 +2215,6 @@ package android { field public static final int Theme_Light_Panel = 16973914; // 0x103005a field public static final int Theme_Light_WallpaperSettings = 16973922; // 0x1030062 field public static final int Theme_Material = 16974372; // 0x1030224 - field public static final int Theme_Material_DayNight = 16974552; // 0x10302d8 - field public static final int Theme_Material_DayNight_DarkActionBar = 16974553; // 0x10302d9 - field public static final int Theme_Material_DayNight_Dialog = 16974554; // 0x10302da - field public static final int Theme_Material_DayNight_DialogWhenLarge = 16974560; // 0x10302e0 - field public static final int Theme_Material_DayNight_DialogWhenLarge_DarkActionBar = 16974568; // 0x10302e8 - field public static final int Theme_Material_DayNight_DialogWhenLarge_NoActionBar = 16974561; // 0x10302e1 - field public static final int Theme_Material_DayNight_Dialog_Alert = 16974555; // 0x10302db - field public static final int Theme_Material_DayNight_Dialog_MinWidth = 16974556; // 0x10302dc - field public static final int Theme_Material_DayNight_Dialog_NoActionBar = 16974557; // 0x10302dd - field public static final int Theme_Material_DayNight_Dialog_NoActionBar_MinWidth = 16974558; // 0x10302de - field public static final int Theme_Material_DayNight_Dialog_Presentation = 16974559; // 0x10302df - field public static final int Theme_Material_DayNight_NoActionBar = 16974562; // 0x10302e2 - field public static final int Theme_Material_DayNight_NoActionBar_Fullscreen = 16974563; // 0x10302e3 - field public static final int Theme_Material_DayNight_NoActionBar_Overscan = 16974564; // 0x10302e4 - field public static final int Theme_Material_DayNight_NoActionBar_TranslucentDecor = 16974565; // 0x10302e5 - field public static final int Theme_Material_DayNight_Panel = 16974566; // 0x10302e6 field public static final int Theme_Material_Dialog = 16974373; // 0x1030225 field public static final int Theme_Material_DialogWhenLarge = 16974379; // 0x103022b field public static final int Theme_Material_DialogWhenLarge_NoActionBar = 16974380; // 0x103022c @@ -2244,7 +2228,7 @@ package android { field public static final int Theme_Material_Light_DarkActionBar = 16974392; // 0x1030238 field public static final int Theme_Material_Light_Dialog = 16974393; // 0x1030239 field public static final int Theme_Material_Light_DialogWhenLarge = 16974399; // 0x103023f - field public static final int Theme_Material_Light_DialogWhenLarge_DarkActionBar = 16974567; // 0x10302e7 + field public static final int Theme_Material_Light_DialogWhenLarge_DarkActionBar = 16974552; // 0x10302d8 field public static final int Theme_Material_Light_DialogWhenLarge_NoActionBar = 16974400; // 0x1030240 field public static final int Theme_Material_Light_Dialog_Alert = 16974394; // 0x103023a field public static final int Theme_Material_Light_Dialog_MinWidth = 16974395; // 0x103023b @@ -3477,6 +3461,7 @@ package android.app { method public boolean dispatchTouchEvent(android.view.MotionEvent); method public boolean dispatchTrackballEvent(android.view.MotionEvent); method public void dump(java.lang.String, java.io.FileDescriptor, java.io.PrintWriter, java.lang.String[]); + method public void enterPictureInPictureMode(); method public android.view.View findViewById(int); method public void finish(); method public void finishActivity(int); @@ -10966,6 +10951,7 @@ package android.database.sqlite { method public void setVersion(int); method public int update(java.lang.String, android.content.ContentValues, java.lang.String, java.lang.String[]); method public int updateWithOnConflict(java.lang.String, android.content.ContentValues, java.lang.String, java.lang.String[], int); + method public void validateSql(java.lang.String, android.os.CancellationSignal); method public deprecated boolean yieldIfContended(); method public boolean yieldIfContendedSafely(); method public boolean yieldIfContendedSafely(long); diff --git a/api/test-current.txt b/api/test-current.txt index 390a017bd401..2377a9b87939 100644 --- a/api/test-current.txt +++ b/api/test-current.txt @@ -2114,22 +2114,6 @@ package android { field public static final int Theme_Light_Panel = 16973914; // 0x103005a field public static final int Theme_Light_WallpaperSettings = 16973922; // 0x1030062 field public static final int Theme_Material = 16974372; // 0x1030224 - field public static final int Theme_Material_DayNight = 16974552; // 0x10302d8 - field public static final int Theme_Material_DayNight_DarkActionBar = 16974553; // 0x10302d9 - field public static final int Theme_Material_DayNight_Dialog = 16974554; // 0x10302da - field public static final int Theme_Material_DayNight_DialogWhenLarge = 16974560; // 0x10302e0 - field public static final int Theme_Material_DayNight_DialogWhenLarge_DarkActionBar = 16974568; // 0x10302e8 - field public static final int Theme_Material_DayNight_DialogWhenLarge_NoActionBar = 16974561; // 0x10302e1 - field public static final int Theme_Material_DayNight_Dialog_Alert = 16974555; // 0x10302db - field public static final int Theme_Material_DayNight_Dialog_MinWidth = 16974556; // 0x10302dc - field public static final int Theme_Material_DayNight_Dialog_NoActionBar = 16974557; // 0x10302dd - field public static final int Theme_Material_DayNight_Dialog_NoActionBar_MinWidth = 16974558; // 0x10302de - field public static final int Theme_Material_DayNight_Dialog_Presentation = 16974559; // 0x10302df - field public static final int Theme_Material_DayNight_NoActionBar = 16974562; // 0x10302e2 - field public static final int Theme_Material_DayNight_NoActionBar_Fullscreen = 16974563; // 0x10302e3 - field public static final int Theme_Material_DayNight_NoActionBar_Overscan = 16974564; // 0x10302e4 - field public static final int Theme_Material_DayNight_NoActionBar_TranslucentDecor = 16974565; // 0x10302e5 - field public static final int Theme_Material_DayNight_Panel = 16974566; // 0x10302e6 field public static final int Theme_Material_Dialog = 16974373; // 0x1030225 field public static final int Theme_Material_DialogWhenLarge = 16974379; // 0x103022b field public static final int Theme_Material_DialogWhenLarge_NoActionBar = 16974380; // 0x103022c @@ -2143,7 +2127,7 @@ package android { field public static final int Theme_Material_Light_DarkActionBar = 16974392; // 0x1030238 field public static final int Theme_Material_Light_Dialog = 16974393; // 0x1030239 field public static final int Theme_Material_Light_DialogWhenLarge = 16974399; // 0x103023f - field public static final int Theme_Material_Light_DialogWhenLarge_DarkActionBar = 16974567; // 0x10302e7 + field public static final int Theme_Material_Light_DialogWhenLarge_DarkActionBar = 16974552; // 0x10302d8 field public static final int Theme_Material_Light_DialogWhenLarge_NoActionBar = 16974400; // 0x1030240 field public static final int Theme_Material_Light_Dialog_Alert = 16974394; // 0x103023a field public static final int Theme_Material_Light_Dialog_MinWidth = 16974395; // 0x103023b @@ -3374,6 +3358,7 @@ package android.app { method public boolean dispatchTouchEvent(android.view.MotionEvent); method public boolean dispatchTrackballEvent(android.view.MotionEvent); method public void dump(java.lang.String, java.io.FileDescriptor, java.io.PrintWriter, java.lang.String[]); + method public void enterPictureInPictureMode(); method public android.view.View findViewById(int); method public void finish(); method public void finishActivity(int); @@ -10605,6 +10590,7 @@ package android.database.sqlite { method public void setVersion(int); method public int update(java.lang.String, android.content.ContentValues, java.lang.String, java.lang.String[]); method public int updateWithOnConflict(java.lang.String, android.content.ContentValues, java.lang.String, java.lang.String[], int); + method public void validateSql(java.lang.String, android.os.CancellationSignal); method public deprecated boolean yieldIfContended(); method public boolean yieldIfContendedSafely(); method public boolean yieldIfContendedSafely(long); diff --git a/cmds/pm/src/com/android/commands/pm/Pm.java b/cmds/pm/src/com/android/commands/pm/Pm.java index ad1b3b5013b7..24449d43b350 100644 --- a/cmds/pm/src/com/android/commands/pm/Pm.java +++ b/cmds/pm/src/com/android/commands/pm/Pm.java @@ -376,6 +376,7 @@ public final class Pm { != PackageInstaller.STATUS_SUCCESS) { return 1; } + System.out.println("Success"); return 0; } finally { try { @@ -591,7 +592,6 @@ public final class Pm { } else { System.err.println("Failure [" + result.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE) + "]"); - System.err.println("Failure details: " + result.getExtras()); } return status; } finally { diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java index ef84ab05c2b2..571bc6efeac5 100644 --- a/core/java/android/app/Activity.java +++ b/core/java/android/app/Activity.java @@ -1782,6 +1782,17 @@ public class Activity extends ContextThemeWrapper } /** + * Puts the activity in picture-in-picture mode. + * @see android.R.attr#supportsPictureInPicture + */ + public void enterPictureInPictureMode() { + try { + ActivityManagerNative.getDefault().enterPictureInPictureMode(mToken); + } catch (RemoteException e) { + } + } + + /** * Called by the system when the device configuration changes while your * activity is running. Note that this will <em>only</em> be called if * you have selected configurations you would like to handle with the diff --git a/core/java/android/app/ActivityManagerNative.java b/core/java/android/app/ActivityManagerNative.java index f11bf742c925..d724823843d7 100644 --- a/core/java/android/app/ActivityManagerNative.java +++ b/core/java/android/app/ActivityManagerNative.java @@ -2759,6 +2759,13 @@ public abstract class ActivityManagerNative extends Binder implements IActivityM reply.writeInt(pipMode ? 1 : 0); return true; } + case ENTER_PICTURE_IN_PICTURE_MODE_TRANSACTION: { + data.enforceInterface(IActivityManager.descriptor); + final IBinder token = data.readStrongBinder(); + enterPictureInPictureMode(token); + reply.writeNoException(); + return true; + } } return super.onTransact(code, data, reply, flags); @@ -6433,5 +6440,17 @@ class ActivityManagerProxy implements IActivityManager return pipMode; } + @Override + public void enterPictureInPictureMode(IBinder token) throws RemoteException { + Parcel data = Parcel.obtain(); + Parcel reply = Parcel.obtain(); + data.writeInterfaceToken(IActivityManager.descriptor); + data.writeStrongBinder(token); + mRemote.transact(ENTER_PICTURE_IN_PICTURE_MODE_TRANSACTION, data, reply, 0); + reply.readException(); + data.recycle(); + reply.recycle(); + } + private IBinder mRemote; } diff --git a/core/java/android/app/IActivityManager.java b/core/java/android/app/IActivityManager.java index 64c69af8691d..f91a0bef7833 100644 --- a/core/java/android/app/IActivityManager.java +++ b/core/java/android/app/IActivityManager.java @@ -550,6 +550,8 @@ public interface IActivityManager extends IInterface { public boolean inPictureInPictureMode(IBinder token) throws RemoteException; + public void enterPictureInPictureMode(IBinder token) throws RemoteException; + /* * Private non-Binder interfaces */ @@ -914,4 +916,5 @@ public interface IActivityManager extends IInterface { int IN_MULTI_WINDOW_MODE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 352; int IN_PICTURE_IN_PICTURE_MODE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 353; int KILL_PACKAGE_DEPENDENTS_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 354; + int ENTER_PICTURE_IN_PICTURE_MODE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 355; } diff --git a/core/java/android/database/sqlite/SQLiteDatabase.java b/core/java/android/database/sqlite/SQLiteDatabase.java index 433d5d1c713e..50e73564ef29 100644 --- a/core/java/android/database/sqlite/SQLiteDatabase.java +++ b/core/java/android/database/sqlite/SQLiteDatabase.java @@ -16,6 +16,8 @@ package android.database.sqlite; +import android.annotation.NonNull; +import android.annotation.Nullable; import android.content.ContentValues; import android.database.Cursor; import android.database.DatabaseErrorHandler; @@ -1683,6 +1685,21 @@ public final class SQLiteDatabase extends SQLiteClosable { } /** + * Verifies that a SQL SELECT statement is valid by compiling it. + * If the SQL statement is not valid, this method will throw a {@link SQLiteException}. + * + * @param sql SQL to be validated + * @param cancellationSignal A signal to cancel the operation in progress, or null if none. + * If the operation is canceled, then {@link OperationCanceledException} will be thrown + * when the query is executed. + * @throws SQLiteException if {@code sql} is invalid + */ + public void validateSql(@NonNull String sql, @Nullable CancellationSignal cancellationSignal) { + getThreadSession().prepare(sql, + getThreadDefaultConnectionFlags(/* readOnly =*/ true), cancellationSignal, null); + } + + /** * Returns true if the database is opened as read only. * * @return True if database is opened as read only. diff --git a/core/java/android/database/sqlite/SQLiteQueryBuilder.java b/core/java/android/database/sqlite/SQLiteQueryBuilder.java index 91884abaf992..56cba795355e 100644 --- a/core/java/android/database/sqlite/SQLiteQueryBuilder.java +++ b/core/java/android/database/sqlite/SQLiteQueryBuilder.java @@ -386,8 +386,7 @@ public class SQLiteQueryBuilder // in both the wrapped and original forms. String sqlForValidation = buildQuery(projectionIn, "(" + selection + ")", groupBy, having, sortOrder, limit); - validateQuerySql(db, sqlForValidation, - cancellationSignal); // will throw if query is invalid + db.validateSql(sqlForValidation, cancellationSignal); // will throw if query is invalid } String sql = buildQuery( @@ -404,16 +403,6 @@ public class SQLiteQueryBuilder } /** - * Verifies that a SQL SELECT statement is valid by compiling it. - * If the SQL statement is not valid, this method will throw a {@link SQLiteException}. - */ - private void validateQuerySql(SQLiteDatabase db, String sql, - CancellationSignal cancellationSignal) { - db.getThreadSession().prepare(sql, - db.getThreadDefaultConnectionFlags(true /*readOnly*/), cancellationSignal, null); - } - - /** * Construct a SELECT statement suitable for use in a group of * SELECT statements that will be joined through UNION operators * in buildUnionQuery. diff --git a/core/java/android/view/ThreadedRenderer.java b/core/java/android/view/ThreadedRenderer.java index f2a4d7bb78fd..0e4bc84a4db2 100644 --- a/core/java/android/view/ThreadedRenderer.java +++ b/core/java/android/view/ThreadedRenderer.java @@ -422,9 +422,10 @@ public final class ThreadedRenderer { * @return True if the initialization was successful, false otherwise. */ boolean initialize(Surface surface) throws OutOfResourcesException { + boolean status = !mInitialized; mInitialized = true; updateEnabledState(surface); - boolean status = nInitialize(mNativeProxy, surface); + nInitialize(mNativeProxy, surface); return status; } @@ -958,7 +959,7 @@ public final class ThreadedRenderer { private static native boolean nLoadSystemProperties(long nativeProxy); private static native void nSetName(long nativeProxy, String name); - private static native boolean nInitialize(long nativeProxy, Surface window); + private static native void nInitialize(long nativeProxy, Surface window); private static native void nUpdateSurface(long nativeProxy, Surface window); private static native boolean nPauseSurface(long nativeProxy, Surface window); private static native void nSetup(long nativeProxy, int width, int height, diff --git a/core/java/com/android/internal/policy/DecorView.java b/core/java/com/android/internal/policy/DecorView.java index 6a1e07b71037..40eaaf7bae80 100644 --- a/core/java/com/android/internal/policy/DecorView.java +++ b/core/java/com/android/internal/policy/DecorView.java @@ -702,11 +702,13 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind helper = mWindow.mContextMenu.showDialog(originalView, originalView.getWindowToken()); } - // If it's a dialog, the callback needs to handle showing sub-menus. - // Either way, the callback is required for propagating selection to - // Context.onContextMenuItemSelected(). - callback.setShowDialogForSubmenu(!isPopup); - helper.setPresenterCallback(callback); + if (helper != null) { + // If it's a dialog, the callback needs to handle showing + // sub-menus. Either way, the callback is required for propagating + // selection to Context.onContextMenuItemSelected(). + callback.setShowDialogForSubmenu(!isPopup); + helper.setPresenterCallback(callback); + } mWindow.mContextMenuHelper = helper; return helper != null; diff --git a/core/jni/android_view_ThreadedRenderer.cpp b/core/jni/android_view_ThreadedRenderer.cpp index 17eb876aebf3..5aa6a739751c 100644 --- a/core/jni/android_view_ThreadedRenderer.cpp +++ b/core/jni/android_view_ThreadedRenderer.cpp @@ -262,11 +262,11 @@ static void android_view_ThreadedRenderer_setName(JNIEnv* env, jobject clazz, env->ReleaseStringUTFChars(jname, name); } -static jboolean android_view_ThreadedRenderer_initialize(JNIEnv* env, jobject clazz, +static void android_view_ThreadedRenderer_initialize(JNIEnv* env, jobject clazz, jlong proxyPtr, jobject jsurface) { RenderProxy* proxy = reinterpret_cast<RenderProxy*>(proxyPtr); sp<ANativeWindow> window = android_view_Surface_getNativeWindow(env, jsurface); - return proxy->initialize(window); + proxy->initialize(window); } static void android_view_ThreadedRenderer_updateSurface(JNIEnv* env, jobject clazz, @@ -492,7 +492,7 @@ static const JNINativeMethod gMethods[] = { { "nDeleteProxy", "(J)V", (void*) android_view_ThreadedRenderer_deleteProxy }, { "nLoadSystemProperties", "(J)Z", (void*) android_view_ThreadedRenderer_loadSystemProperties }, { "nSetName", "(JLjava/lang/String;)V", (void*) android_view_ThreadedRenderer_setName }, - { "nInitialize", "(JLandroid/view/Surface;)Z", (void*) android_view_ThreadedRenderer_initialize }, + { "nInitialize", "(JLandroid/view/Surface;)V", (void*) android_view_ThreadedRenderer_initialize }, { "nUpdateSurface", "(JLandroid/view/Surface;)V", (void*) android_view_ThreadedRenderer_updateSurface }, { "nPauseSurface", "(JLandroid/view/Surface;)Z", (void*) android_view_ThreadedRenderer_pauseSurface }, { "nSetup", "(JIIFII)V", (void*) android_view_ThreadedRenderer_setup }, diff --git a/core/res/res/values/public.xml b/core/res/res/values/public.xml index ad36f3c107bc..09c17179b4b5 100644 --- a/core/res/res/values/public.xml +++ b/core/res/res/values/public.xml @@ -2686,23 +2686,7 @@ <public type="attr" name="languageTag" /> <public type="attr" name="pointerShape" /> - <public type="style" name="Theme.Material.DayNight" /> - <public type="style" name="Theme.Material.DayNight.DarkActionBar" /> - <public type="style" name="Theme.Material.DayNight.Dialog" /> - <public type="style" name="Theme.Material.DayNight.Dialog.Alert" /> - <public type="style" name="Theme.Material.DayNight.Dialog.MinWidth" /> - <public type="style" name="Theme.Material.DayNight.Dialog.NoActionBar" /> - <public type="style" name="Theme.Material.DayNight.Dialog.NoActionBar.MinWidth" /> - <public type="style" name="Theme.Material.DayNight.Dialog.Presentation" /> - <public type="style" name="Theme.Material.DayNight.DialogWhenLarge" /> - <public type="style" name="Theme.Material.DayNight.DialogWhenLarge.NoActionBar" /> - <public type="style" name="Theme.Material.DayNight.NoActionBar" /> - <public type="style" name="Theme.Material.DayNight.NoActionBar.Fullscreen" /> - <public type="style" name="Theme.Material.DayNight.NoActionBar.Overscan" /> - <public type="style" name="Theme.Material.DayNight.NoActionBar.TranslucentDecor" /> - <public type="style" name="Theme.Material.DayNight.Panel" /> <public type="style" name="Theme.Material.Light.DialogWhenLarge.DarkActionBar" /> - <public type="style" name="Theme.Material.DayNight.DialogWhenLarge.DarkActionBar" /> <public type="id" name="accessibilityActionSetProgress" /> <public type="id" name="icon_frame" /> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 832984ea1813..f6326f723298 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -2389,4 +2389,22 @@ <java-symbol type="layout" name="work_widget_mask_view" /> <java-symbol type="id" name="work_widget_app_icon" /> <java-symbol type="drawable" name="work_widget_mask_view_background" /> + + <!-- Framework-private Material.DayNight styles. --> + <java-symbol type="style" name="Theme.Material.DayNight" /> + <java-symbol type="style" name="Theme.Material.DayNight.DarkActionBar" /> + <java-symbol type="style" name="Theme.Material.DayNight.Dialog" /> + <java-symbol type="style" name="Theme.Material.DayNight.Dialog.Alert" /> + <java-symbol type="style" name="Theme.Material.DayNight.Dialog.MinWidth" /> + <java-symbol type="style" name="Theme.Material.DayNight.Dialog.NoActionBar" /> + <java-symbol type="style" name="Theme.Material.DayNight.Dialog.NoActionBar.MinWidth" /> + <java-symbol type="style" name="Theme.Material.DayNight.Dialog.Presentation" /> + <java-symbol type="style" name="Theme.Material.DayNight.DialogWhenLarge" /> + <java-symbol type="style" name="Theme.Material.DayNight.DialogWhenLarge.NoActionBar" /> + <java-symbol type="style" name="Theme.Material.DayNight.NoActionBar" /> + <java-symbol type="style" name="Theme.Material.DayNight.NoActionBar.Fullscreen" /> + <java-symbol type="style" name="Theme.Material.DayNight.NoActionBar.Overscan" /> + <java-symbol type="style" name="Theme.Material.DayNight.NoActionBar.TranslucentDecor" /> + <java-symbol type="style" name="Theme.Material.DayNight.Panel" /> + <java-symbol type="style" name="Theme.Material.DayNight.DialogWhenLarge.DarkActionBar" /> </resources> diff --git a/libs/hwui/BakedOpDispatcher.cpp b/libs/hwui/BakedOpDispatcher.cpp index 9e39797270a5..a892b1b29174 100644 --- a/libs/hwui/BakedOpDispatcher.cpp +++ b/libs/hwui/BakedOpDispatcher.cpp @@ -705,6 +705,36 @@ void BakedOpDispatcher::onTextOp(BakedOpRenderer& renderer, const TextOp& op, co renderTextOp(renderer, op, state, clip, TextRenderType::Flush); } +void BakedOpDispatcher::onTextOnPathOp(BakedOpRenderer& renderer, const TextOnPathOp& op, const BakedOpState& state) { + // Note: can't trust clipSideFlags since we record with unmappedBounds == clip. + // TODO: respect clipSideFlags, once we record with bounds + const Rect* renderTargetClip = &state.computedState.clipRect; + + FontRenderer& fontRenderer = renderer.caches().fontRenderer.getFontRenderer(); + fontRenderer.setFont(op.paint, SkMatrix::I()); + fontRenderer.setTextureFiltering(true); + + Rect layerBounds(FLT_MAX / 2.0f, FLT_MAX / 2.0f, FLT_MIN / 2.0f, FLT_MIN / 2.0f); + + int alpha = PaintUtils::getAlphaDirect(op.paint) * state.alpha; + SkXfermode::Mode mode = PaintUtils::getXfermodeDirect(op.paint); + TextDrawFunctor functor(&renderer, &state, renderTargetClip, + 0.0f, 0.0f, false, alpha, mode, op.paint); + + bool mustDirtyRenderTarget = renderer.offscreenRenderTarget(); + const Rect localSpaceClip = state.computedState.computeLocalSpaceClip(); + if (fontRenderer.renderTextOnPath(op.paint, &localSpaceClip, + reinterpret_cast<const char*>(op.glyphs), op.glyphCount, + op.path, op.hOffset, op.vOffset, + mustDirtyRenderTarget ? &layerBounds : nullptr, &functor)) { + if (mustDirtyRenderTarget) { + // manually dirty render target, since TextDrawFunctor won't + state.computedState.transform.mapRect(layerBounds); + renderer.dirtyRenderTarget(layerBounds); + } + } +} + void BakedOpDispatcher::onLayerOp(BakedOpRenderer& renderer, const LayerOp& op, const BakedOpState& state) { OffscreenBuffer* buffer = *op.layerHandle; diff --git a/libs/hwui/BakedOpState.h b/libs/hwui/BakedOpState.h index 9c836a00bfea..b12c0c970352 100644 --- a/libs/hwui/BakedOpState.h +++ b/libs/hwui/BakedOpState.h @@ -124,6 +124,15 @@ public: clipSideFlags = OpClipSideFlags::Full; } + Rect computeLocalSpaceClip() const { + Matrix4 inverse; + inverse.loadInverse(transform); + + Rect outClip(clipRect); + inverse.mapRect(outClip); + return outClip; + } + Matrix4 transform; Rect clipRect; int clipSideFlags = 0; diff --git a/libs/hwui/OpReorderer.cpp b/libs/hwui/OpReorderer.cpp index 6d90a42d49c7..e1472113d25f 100644 --- a/libs/hwui/OpReorderer.cpp +++ b/libs/hwui/OpReorderer.cpp @@ -90,7 +90,9 @@ public: } MergingOpBatch(batchid_t batchId, BakedOpState* op) - : BatchBase(batchId, op, true) { + : BatchBase(batchId, op, true) + , mClipSideFlags(op->computedState.clipSideFlags) + , mClipRect(op->computedState.clipRect) { } /* @@ -202,11 +204,11 @@ public: if (newClipSideFlags & OpClipSideFlags::Bottom) mClipRect.bottom = opClip.bottom; } - bool getClipSideFlags() const { return mClipSideFlags; } + int getClipSideFlags() const { return mClipSideFlags; } const Rect& getClipRect() const { return mClipRect; } private: - int mClipSideFlags = 0; + int mClipSideFlags; Rect mClipRect; }; @@ -308,12 +310,6 @@ void OpReorderer::LayerReorderer::replayBakedOpsImpl(void* arg, mergingBatch->getClipSideFlags(), mergingBatch->getClipRect() }; - if (data.clipSideFlags) { - // if right or bottom sides aren't used to clip, init them to viewport bounds - // in the clip rect, so it can be used to scissor - if (!(data.clipSideFlags & OpClipSideFlags::Right)) data.clip.right = width; - if (!(data.clipSideFlags & OpClipSideFlags::Bottom)) data.clip.bottom = height; - } mergedReceivers[opId](arg, data); } else { for (const BakedOpState* op : batch->getOps()) { @@ -850,14 +846,16 @@ void OpReorderer::deferSimpleRectsOp(const SimpleRectsOp& op) { currentLayer().deferUnmergeableOp(mAllocator, bakedState, OpBatchType::Vertices); } +static batchid_t textBatchId(const SkPaint& paint) { + // TODO: better handling of shader (since we won't care about color then) + return paint.getColor() == SK_ColorBLACK ? OpBatchType::Text : OpBatchType::ColorText; +} + void OpReorderer::deferTextOp(const TextOp& op) { BakedOpState* bakedState = tryBakeOpState(op); if (!bakedState) return; // quick rejected - // TODO: better handling of shader (since we won't care about color then) - batchid_t batchId = op.paint->getColor() == SK_ColorBLACK - ? OpBatchType::Text : OpBatchType::ColorText; - + batchid_t batchId = textBatchId(*(op.paint)); if (bakedState->computedState.transform.isPureTranslate() && PaintUtils::getXfermodeDirect(op.paint) == SkXfermode::kSrcOver_Mode) { mergeid_t mergeId = reinterpret_cast<mergeid_t>(op.paint->getColor()); @@ -867,6 +865,12 @@ void OpReorderer::deferTextOp(const TextOp& op) { } } +void OpReorderer::deferTextOnPathOp(const TextOnPathOp& op) { + BakedOpState* bakedState = tryBakeOpState(op); + if (!bakedState) return; // quick rejected + currentLayer().deferUnmergeableOp(mAllocator, bakedState, textBatchId(*(op.paint))); +} + void OpReorderer::saveForLayer(uint32_t layerWidth, uint32_t layerHeight, float contentTranslateX, float contentTranslateY, const Rect& repaintRect, diff --git a/libs/hwui/RecordedOp.h b/libs/hwui/RecordedOp.h index b58b7744c7c3..bb230054ad15 100644 --- a/libs/hwui/RecordedOp.h +++ b/libs/hwui/RecordedOp.h @@ -62,6 +62,7 @@ struct Vertex; U_OP_FN(ShadowOp) \ U_OP_FN(SimpleRectsOp) \ M_OP_FN(TextOp) \ + U_OP_FN(TextOnPathOp) \ U_OP_FN(BeginLayerOp) \ U_OP_FN(EndLayerOp) \ U_OP_FN(LayerOp) @@ -327,6 +328,23 @@ struct TextOp : RecordedOp { const float y; }; +struct TextOnPathOp : RecordedOp { + TextOnPathOp(BASE_PARAMS, const glyph_t* glyphs, int glyphCount, + const SkPath* path, float hOffset, float vOffset) + : SUPER(TextOnPathOp) + , glyphs(glyphs) + , glyphCount(glyphCount) + , path(path) + , hOffset(hOffset) + , vOffset(vOffset) {} + const glyph_t* glyphs; + const int glyphCount; + + const SkPath* path; + const float hOffset; + const float vOffset; +}; + //////////////////////////////////////////////////////////////////////////////////////////////////// // Layers //////////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/libs/hwui/RecordingCanvas.cpp b/libs/hwui/RecordingCanvas.cpp index 7f035e549404..46ae7904ae78 100644 --- a/libs/hwui/RecordingCanvas.cpp +++ b/libs/hwui/RecordingCanvas.cpp @@ -503,9 +503,15 @@ void RecordingCanvas::drawText(const uint16_t* glyphs, const float* positions, i drawTextDecorations(x, y, totalAdvance, paint); } -void RecordingCanvas::drawTextOnPath(const uint16_t* glyphs, int count, const SkPath& path, +void RecordingCanvas::drawTextOnPath(const uint16_t* glyphs, int glyphCount, const SkPath& path, float hOffset, float vOffset, const SkPaint& paint) { - LOG_ALWAYS_FATAL("TODO!"); + if (!glyphs || glyphCount <= 0 || PaintUtils::paintWillNotDrawText(paint)) return; + glyphs = refBuffer<glyph_t>(glyphs, glyphCount); + addOp(new (alloc()) TextOnPathOp( + mState.getRenderTargetClipBounds(), // TODO: explicitly define bounds + *(mState.currentSnapshot()->transform), + mState.getRenderTargetClipBounds(), + refPaint(&paint), glyphs, glyphCount, refPath(&path), hOffset, vOffset)); } void RecordingCanvas::drawBitmap(const SkBitmap* bitmap, const SkPaint* paint) { diff --git a/libs/hwui/RecordingCanvas.h b/libs/hwui/RecordingCanvas.h index 49bdba8bec9d..ce7d13507aea 100644 --- a/libs/hwui/RecordingCanvas.h +++ b/libs/hwui/RecordingCanvas.h @@ -189,10 +189,10 @@ public: const SkPaint* paint) override; // Text - virtual void drawText(const uint16_t* glyphs, const float* positions, int count, + virtual void drawText(const uint16_t* glyphs, const float* positions, int glyphCount, const SkPaint& paint, float x, float y, float boundsLeft, float boundsTop, float boundsRight, float boundsBottom, float totalAdvance) override; - virtual void drawTextOnPath(const uint16_t* glyphs, int count, const SkPath& path, + virtual void drawTextOnPath(const uint16_t* glyphs, int glyphCount, const SkPath& path, float hOffset, float vOffset, const SkPaint& paint) override; virtual bool drawTextAbsolutePos() const override { return false; } diff --git a/libs/hwui/renderthread/CanvasContext.cpp b/libs/hwui/renderthread/CanvasContext.cpp index 0620653371a6..dfcac1d1756a 100644 --- a/libs/hwui/renderthread/CanvasContext.cpp +++ b/libs/hwui/renderthread/CanvasContext.cpp @@ -128,14 +128,13 @@ void CanvasContext::setSwapBehavior(SwapBehavior swapBehavior) { mSwapBehavior = swapBehavior; } -bool CanvasContext::initialize(ANativeWindow* window) { +void CanvasContext::initialize(ANativeWindow* window) { setSurface(window); #if !HWUI_NEW_OPS - if (mCanvas) return false; + if (mCanvas) return; mCanvas = new OpenGLRenderer(mRenderThread.renderState()); mCanvas->initProperties(); #endif - return true; } void CanvasContext::updateSurface(ANativeWindow* window) { diff --git a/libs/hwui/renderthread/CanvasContext.h b/libs/hwui/renderthread/CanvasContext.h index d36ce993164e..09469008e6fa 100644 --- a/libs/hwui/renderthread/CanvasContext.h +++ b/libs/hwui/renderthread/CanvasContext.h @@ -73,7 +73,7 @@ public: // Won't take effect until next EGLSurface creation void setSwapBehavior(SwapBehavior swapBehavior); - bool initialize(ANativeWindow* window); + void initialize(ANativeWindow* window); void updateSurface(ANativeWindow* window); bool pauseSurface(ANativeWindow* window); bool hasSurface() { return mNativeWindow.get(); } diff --git a/libs/hwui/renderthread/RenderProxy.cpp b/libs/hwui/renderthread/RenderProxy.cpp index 15ccd6ac5b6b..43282c99803a 100644 --- a/libs/hwui/renderthread/RenderProxy.cpp +++ b/libs/hwui/renderthread/RenderProxy.cpp @@ -140,14 +140,15 @@ void RenderProxy::setName(const char* name) { } CREATE_BRIDGE2(initialize, CanvasContext* context, ANativeWindow* window) { - return (void*) args->context->initialize(args->window); + args->context->initialize(args->window); + return nullptr; } -bool RenderProxy::initialize(const sp<ANativeWindow>& window) { +void RenderProxy::initialize(const sp<ANativeWindow>& window) { SETUP_TASK(initialize); args->context = mContext; args->window = window.get(); - return (bool) postAndWait(task); + post(task); } CREATE_BRIDGE2(updateSurface, CanvasContext* context, ANativeWindow* window) { diff --git a/libs/hwui/renderthread/RenderProxy.h b/libs/hwui/renderthread/RenderProxy.h index 338fab650876..1d30eb8d8efe 100644 --- a/libs/hwui/renderthread/RenderProxy.h +++ b/libs/hwui/renderthread/RenderProxy.h @@ -66,7 +66,7 @@ public: ANDROID_API bool loadSystemProperties(); ANDROID_API void setName(const char* name); - ANDROID_API bool initialize(const sp<ANativeWindow>& window); + ANDROID_API void initialize(const sp<ANativeWindow>& window); ANDROID_API void updateSurface(const sp<ANativeWindow>& window); ANDROID_API bool pauseSurface(const sp<ANativeWindow>& window); ANDROID_API void setup(int width, int height, float lightRadius, diff --git a/libs/hwui/tests/common/TestUtils.cpp b/libs/hwui/tests/common/TestUtils.cpp index 6cef85203352..4c8d23dd82ea 100644 --- a/libs/hwui/tests/common/TestUtils.cpp +++ b/libs/hwui/tests/common/TestUtils.cpp @@ -41,11 +41,8 @@ void TestUtils::drawTextToCanvas(TestCanvas* canvas, const char* text, // drawing text requires GlyphID TextEncoding (which JNI layer would have done) LOG_ALWAYS_FATAL_IF(paint.getTextEncoding() != SkPaint::kGlyphID_TextEncoding, "must use glyph encoding"); - - SkMatrix identity; - identity.setIdentity(); SkSurfaceProps surfaceProps(0, kUnknown_SkPixelGeometry); - SkAutoGlyphCacheNoGamma autoCache(paint, &surfaceProps, &identity); + SkAutoGlyphCacheNoGamma autoCache(paint, &surfaceProps, &SkMatrix::I()); float totalAdvance = 0; std::vector<glyph_t> glyphs; @@ -89,5 +86,21 @@ void TestUtils::drawTextToCanvas(TestCanvas* canvas, const char* text, bounds.left, bounds.top, bounds.right, bounds.bottom, totalAdvance); } +void TestUtils::drawTextToCanvas(TestCanvas* canvas, const char* text, + const SkPaint& paint, const SkPath& path) { + // drawing text requires GlyphID TextEncoding (which JNI layer would have done) + LOG_ALWAYS_FATAL_IF(paint.getTextEncoding() != SkPaint::kGlyphID_TextEncoding, + "must use glyph encoding"); + SkSurfaceProps surfaceProps(0, kUnknown_SkPixelGeometry); + SkAutoGlyphCacheNoGamma autoCache(paint, &surfaceProps, &SkMatrix::I()); + + std::vector<glyph_t> glyphs; + while (*text != '\0') { + SkUnichar unichar = SkUTF8_NextUnichar(&text); + glyphs.push_back(autoCache.getCache()->unicharToGlyph(unichar)); + } + canvas->drawTextOnPath(glyphs.data(), glyphs.size(), path, 0, 0, paint); +} + } /* namespace uirenderer */ } /* namespace android */ diff --git a/libs/hwui/tests/common/TestUtils.h b/libs/hwui/tests/common/TestUtils.h index 0af99398b5f5..4f84474f9988 100644 --- a/libs/hwui/tests/common/TestUtils.h +++ b/libs/hwui/tests/common/TestUtils.h @@ -191,6 +191,9 @@ public: static void drawTextToCanvas(TestCanvas* canvas, const char* text, const SkPaint& paint, float x, float y); + static void drawTextToCanvas(TestCanvas* canvas, const char* text, + const SkPaint& paint, const SkPath& path); + private: static void syncHierarchyPropertiesAndDisplayListImpl(RenderNode* node) { node->syncProperties(); diff --git a/libs/hwui/tests/common/scenes/OvalAnimation.cpp b/libs/hwui/tests/common/scenes/OvalAnimation.cpp index 082c6287e86f..e56f2f97007e 100644 --- a/libs/hwui/tests/common/scenes/OvalAnimation.cpp +++ b/libs/hwui/tests/common/scenes/OvalAnimation.cpp @@ -15,6 +15,7 @@ */ #include "TestSceneBase.h" +#include "utils/Color.h" class OvalAnimation; @@ -28,12 +29,12 @@ class OvalAnimation : public TestScene { public: sp<RenderNode> card; void createContent(int width, int height, TestCanvas& canvas) override { - canvas.drawColor(0xFFFFFFFF, SkXfermode::kSrcOver_Mode); + canvas.drawColor(Color::White, SkXfermode::kSrcOver_Mode); card = TestUtils::createNode(0, 0, 200, 200, [](RenderProperties& props, TestCanvas& canvas) { SkPaint paint; paint.setAntiAlias(true); - paint.setColor(0xFF000000); + paint.setColor(Color::Black); canvas.drawOval(0, 0, 200, 200, paint); }); canvas.drawRenderNode(card.get()); diff --git a/libs/hwui/tests/common/scenes/TextAnimation.cpp b/libs/hwui/tests/common/scenes/TextAnimation.cpp new file mode 100644 index 000000000000..1823db2940aa --- /dev/null +++ b/libs/hwui/tests/common/scenes/TextAnimation.cpp @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2015 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. + */ + +#include "TestSceneBase.h" +#include "utils/Color.h" + +class TextAnimation; + +static TestScene::Registrar _Text(TestScene::Info{ + "text", + "Draws a bunch of text.", + TestScene::simpleCreateScene<TextAnimation> +}); + +class TextAnimation : public TestScene { +public: + sp<RenderNode> card; + void createContent(int width, int height, TestCanvas& canvas) override { + canvas.drawColor(Color::White, SkXfermode::kSrcOver_Mode); + card = TestUtils::createNode(0, 0, width, height, + [](RenderProperties& props, TestCanvas& canvas) { + SkPaint paint; + paint.setTextEncoding(SkPaint::kGlyphID_TextEncoding); + paint.setAntiAlias(true); + paint.setTextSize(50); + + paint.setColor(Color::Black); + for (int i = 0; i < 10; i++) { + TestUtils::drawTextToCanvas(&canvas, "Test string", paint, 400, i * 100); + } + + SkPath path; + path.addOval(SkRect::MakeLTRB(100, 100, 300, 300)); + + paint.setColor(Color::Blue_500); + TestUtils::drawTextToCanvas(&canvas, "This is a neat circle of text!", paint, path); + }); + canvas.drawRenderNode(card.get()); + } + + void doFrame(int frameNr) override { + int curFrame = frameNr % 150; + card->mutateStagingProperties().setTranslationX(curFrame); + card->mutateStagingProperties().setTranslationY(curFrame); + card->setPropertyFieldsDirty(RenderNode::X | RenderNode::Y); + } +}; diff --git a/libs/hwui/tests/unit/BakedOpStateTests.cpp b/libs/hwui/tests/unit/BakedOpStateTests.cpp index 33eff5ba2da0..f9f5316a58df 100644 --- a/libs/hwui/tests/unit/BakedOpStateTests.cpp +++ b/libs/hwui/tests/unit/BakedOpStateTests.cpp @@ -56,6 +56,28 @@ TEST(ResolvedRenderState, construct) { } } +TEST(ResolvedRenderState, computeLocalSpaceClip) { + Matrix4 translate10x20; + translate10x20.loadTranslate(10, 20, 0); + + SkPaint paint; + RectOp recordedOp(Rect(1000, 1000), translate10x20, Rect(100, 200), &paint); + { + // recorded with transform, no parent transform + auto parentSnapshot = TestUtils::makeSnapshot(Matrix4::identity(), Rect(100, 200)); + ResolvedRenderState state(*parentSnapshot, recordedOp, false); + EXPECT_EQ(Rect(-10, -20, 90, 180), state.computeLocalSpaceClip()) + << "Local clip rect should be 100x200, offset by -10,-20"; + } + { + // recorded with transform + parent transform + auto parentSnapshot = TestUtils::makeSnapshot(translate10x20, Rect(100, 200)); + ResolvedRenderState state(*parentSnapshot, recordedOp, false); + EXPECT_EQ(Rect(-10, -20, 80, 160), state.computeLocalSpaceClip()) + << "Local clip rect should be 90x190, offset by -10,-20"; + } +} + const float HAIRLINE = 0.0f; // Note: bounds will be conservative, but not precise for non-hairline diff --git a/libs/hwui/tests/unit/OpReordererTests.cpp b/libs/hwui/tests/unit/OpReordererTests.cpp index ac356a47c87c..ab0cc878178c 100644 --- a/libs/hwui/tests/unit/OpReordererTests.cpp +++ b/libs/hwui/tests/unit/OpReordererTests.cpp @@ -221,7 +221,35 @@ TEST(OpReorderer, simpleBatching) { << "Expect number of ops = 2 * loop count"; } -TEST(OpReorderer, textStrikethroughBatching) { +TEST(OpReorderer, textMerging) { + class TextMergingTestRenderer : public TestRendererBase { + public: + void onMergedTextOps(const MergedBakedOpList& opList) override { + EXPECT_EQ(0, mIndex); + mIndex += opList.count; + EXPECT_EQ(2u, opList.count); + EXPECT_EQ(OpClipSideFlags::Top, opList.clipSideFlags); + EXPECT_EQ(OpClipSideFlags::Top, opList.states[0]->computedState.clipSideFlags); + EXPECT_EQ(OpClipSideFlags::None, opList.states[1]->computedState.clipSideFlags); + } + }; + auto node = TestUtils::createNode(0, 0, 400, 400, + [](RenderProperties& props, TestCanvas& canvas) { + SkPaint paint; + paint.setTextEncoding(SkPaint::kGlyphID_TextEncoding); + paint.setAntiAlias(true); + paint.setTextSize(50); + TestUtils::drawTextToCanvas(&canvas, "Test string1", paint, 100, 0); // will be top clipped + TestUtils::drawTextToCanvas(&canvas, "Test string1", paint, 100, 100); // not clipped + }); + OpReorderer reorderer(sEmptyLayerUpdateQueue, SkRect::MakeWH(400, 400), 400, 400, + createSyncedNodeList(node), sLightCenter); + TextMergingTestRenderer renderer; + reorderer.replayBakedOps<TestDispatcher>(renderer); + EXPECT_EQ(2, renderer.getIndex()) << "Expect 2 ops"; +} + +TEST(OpReorderer, textStrikethrough) { const int LOOPS = 5; class TextStrikethroughTestRenderer : public TestRendererBase { public: @@ -250,7 +278,7 @@ TEST(OpReorderer, textStrikethroughBatching) { TextStrikethroughTestRenderer renderer; reorderer.replayBakedOps<TestDispatcher>(renderer); EXPECT_EQ(2 * LOOPS, renderer.getIndex()) - << "Expect number of ops = 2 * loop count"; // TODO: force no merging + << "Expect number of ops = 2 * loop count"; } TEST(OpReorderer, renderNode) { diff --git a/packages/Shell/res/layout/dialog_bugreport_info.xml b/packages/Shell/res/layout/dialog_bugreport_info.xml new file mode 100644 index 000000000000..5d1e9f994946 --- /dev/null +++ b/packages/Shell/res/layout/dialog_bugreport_info.xml @@ -0,0 +1,43 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2015 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. +--> + +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:orientation="vertical" + android:layout_width="wrap_content" + android:layout_height="wrap_content"> + <EditText + android:id="@+id/name" + android:maxLength="30" + android:singleLine="true" + android:inputType="textNoSuggestions" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:hint="@string/bugreport_info_name"/> + <EditText + android:id="@+id/title" + android:maxLength="80" + android:singleLine="true" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:hint="@string/bugreport_info_title"/> + <EditText + android:id="@+id/description" + android:singleLine="false" + android:inputType="textMultiLine" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:hint="@string/bugreport_info_description"/> +</LinearLayout> diff --git a/packages/Shell/res/values/strings.xml b/packages/Shell/res/values/strings.xml index 5c2557643121..a7f2df5a739a 100644 --- a/packages/Shell/res/values/strings.xml +++ b/packages/Shell/res/values/strings.xml @@ -42,4 +42,19 @@ <!-- Title for bug reports received from dumpstate without a name. [CHAR LIMIT=30]--> <string name="bugreport_unnamed">unnamed</string> + <!-- Title of the notification action that opens the dialog for the user-defined bug report details. --> + <string name="bugreport_info_action">Details</string> + + <!-- Title of the dialog asking for user-defined bug report details like name, title, and description. --> + <string name="bugreport_info_dialog_title">Bug report details</string> + + <!-- Text of the hint asking for the bug report name, which when set will define a suffix in the + bug report file names. [CHAR LIMIT=30] --> + <string name="bugreport_info_name">Short name</string> + <!-- Text of hint asking for the bug report title, which when set will define the + Subject of the email message. [CHAR LIMIT=60] --> + <string name="bugreport_info_title">1-line summary</string> + <!-- Text of hint asking for the bug report description, which when set will describe + what the bug report is about. [CHAR LIMIT=NONE] --> + <string name="bugreport_info_description">Detailed description</string> </resources> diff --git a/packages/Shell/src/com/android/shell/BugreportProgressService.java b/packages/Shell/src/com/android/shell/BugreportProgressService.java index e902589ddc51..82ee710f5bcc 100644 --- a/packages/Shell/src/com/android/shell/BugreportProgressService.java +++ b/packages/Shell/src/com/android/shell/BugreportProgressService.java @@ -29,16 +29,18 @@ import java.io.InputStream; import java.io.PrintWriter; import java.text.NumberFormat; import java.util.ArrayList; -import java.util.Date; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; import libcore.io.Streams; +import com.android.internal.annotations.VisibleForTesting; import com.google.android.collect.Lists; import android.accounts.Account; import android.accounts.AccountManager; +import android.annotation.SuppressLint; +import android.app.AlertDialog; import android.app.Notification; import android.app.Notification.Action; import android.app.NotificationManager; @@ -46,6 +48,7 @@ import android.app.PendingIntent; import android.app.Service; import android.content.ClipData; import android.content.Context; +import android.content.DialogInterface; import android.content.Intent; import android.content.res.Configuration; import android.net.Uri; @@ -59,10 +62,17 @@ import android.os.Parcelable; import android.os.Process; import android.os.SystemProperties; import android.support.v4.content.FileProvider; +import android.text.TextUtils; import android.text.format.DateUtils; import android.util.Log; import android.util.Patterns; import android.util.SparseArray; +import android.view.View; +import android.view.WindowManager; +import android.view.View.OnFocusChangeListener; +import android.view.inputmethod.EditorInfo; +import android.widget.Button; +import android.widget.EditText; import android.widget.Toast; /** @@ -103,19 +113,23 @@ public class BugreportProgressService extends Service { // Internal intents used on notification actions. static final String INTENT_BUGREPORT_CANCEL = "android.intent.action.BUGREPORT_CANCEL"; static final String INTENT_BUGREPORT_SHARE = "android.intent.action.BUGREPORT_SHARE"; + static final String INTENT_BUGREPORT_INFO_LAUNCH = + "android.intent.action.BUGREPORT_INFO_LAUNCH"; static final String EXTRA_BUGREPORT = "android.intent.extra.BUGREPORT"; static final String EXTRA_SCREENSHOT = "android.intent.extra.SCREENSHOT"; static final String EXTRA_PID = "android.intent.extra.PID"; static final String EXTRA_MAX = "android.intent.extra.MAX"; static final String EXTRA_NAME = "android.intent.extra.NAME"; + static final String EXTRA_TITLE = "android.intent.extra.TITLE"; + static final String EXTRA_DESCRIPTION = "android.intent.extra.DESCRIPTION"; static final String EXTRA_ORIGINAL_INTENT = "android.intent.extra.ORIGINAL_INTENT"; private static final int MSG_SERVICE_COMMAND = 1; private static final int MSG_POLL = 2; /** Polling frequency, in milliseconds. */ - static final long POLLING_FREQUENCY = 2000; + static final long POLLING_FREQUENCY = 2 * DateUtils.SECOND_IN_MILLIS; /** How long (in ms) a dumpstate process will be monitored if it didn't show progress. */ private static final long INACTIVITY_TIMEOUT = 3 * DateUtils.MINUTE_IN_MILLIS; @@ -124,8 +138,9 @@ public class BugreportProgressService extends Service { private static final String DUMPSTATE_PREFIX = "dumpstate."; private static final String PROGRESS_SUFFIX = ".progress"; private static final String MAX_SUFFIX = ".max"; + private static final String NAME_SUFFIX = ".name"; - /** System property (and value) used for stop dumpstate. */ + /** System property (and value) used to stop dumpstate. */ private static final String CTL_STOP = "ctl.stop"; private static final String BUGREPORT_SERVICE = "bugreportplus"; @@ -135,6 +150,8 @@ public class BugreportProgressService extends Service { private Looper mServiceLooper; private ServiceHandler mServiceHandler; + private final BugreportInfoDialog mInfoDialog = new BugreportInfoDialog(); + @Override public void onCreate() { HandlerThread thread = new HandlerThread("BugreportProgressServiceThread", @@ -242,6 +259,9 @@ public class BugreportProgressService extends Service { } onBugreportFinished(pid, intent); break; + case INTENT_BUGREPORT_INFO_LAUNCH: + launchBugreportInfoDialog(pid); + break; case INTENT_BUGREPORT_SHARE: shareBugreport(pid); break; @@ -312,6 +332,13 @@ public class BugreportProgressService extends Service { final String percentText = nf.format((double) info.progress / info.max); final Action cancelAction = new Action.Builder(null, context.getString( com.android.internal.R.string.cancel), newCancelIntent(context, info)).build(); + final Intent infoIntent = new Intent(context, BugreportProgressService.class); + infoIntent.setAction(INTENT_BUGREPORT_INFO_LAUNCH); + infoIntent.putExtra(EXTRA_PID, info.pid); + final Action infoAction = new Action.Builder(null, + context.getString(R.string.bugreport_info_action), + PendingIntent.getService(context, info.pid, infoIntent, + PendingIntent.FLAG_UPDATE_CURRENT)).build(); final String title = context.getString(R.string.bugreport_in_progress_title); final String name = @@ -328,6 +355,7 @@ public class BugreportProgressService extends Service { .setLocalOnly(true) .setColor(context.getColor( com.android.internal.R.color.system_notification_accent_color)) + .addAction(infoAction) .addAction(cancelAction) .build(); @@ -341,7 +369,8 @@ public class BugreportProgressService extends Service { final Intent intent = new Intent(INTENT_BUGREPORT_CANCEL); intent.setClass(context, BugreportProgressService.class); intent.putExtra(EXTRA_PID, info.pid); - return PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT); + return PendingIntent.getService(context, info.pid, intent, + PendingIntent.FLAG_UPDATE_CURRENT); } /** @@ -356,7 +385,7 @@ public class BugreportProgressService extends Service { } stopSelfWhenDone(); } - if (DEBUG) Log.v(TAG, "stopProgress(" + pid + "): cancel notification"); + Log.v(TAG, "stopProgress(" + pid + "): cancel notification"); NotificationManager.from(getApplicationContext()).cancel(TAG, pid); } @@ -364,8 +393,14 @@ public class BugreportProgressService extends Service { * Cancels a bugreport upon user's request. */ private void cancel(int pid) { - Log.i(TAG, "Cancelling PID " + pid + " on user's request"); - SystemProperties.set(CTL_STOP, BUGREPORT_SERVICE); + Log.v(TAG, "cancel: pid=" + pid); + synchronized (mProcesses) { + BugreportInfo info = mProcesses.get(pid); + if (info != null && !info.finished) { + Log.i(TAG, "Cancelling bugreport service (pid=" + pid + ") on user's request"); + setSystemProperty(CTL_STOP, BUGREPORT_SERVICE); + } + } stopProgress(pid); } @@ -393,7 +428,6 @@ public class BugreportProgressService extends Service { final int progress = SystemProperties.getInt(progressKey, 0); if (progress == 0) { Log.v(TAG, "System property " + progressKey + " is not set yet"); - continue; } final int max = SystemProperties.getInt(DUMPSTATE_PREFIX + pid + MAX_SUFFIX, 0); final boolean maxChanged = max > 0 && max != info.max; @@ -427,6 +461,30 @@ public class BugreportProgressService extends Service { } /** + * Fetches a {@link BugreportInfo} for a given process and launches a dialog where the user can + * change its values. + */ + private void launchBugreportInfoDialog(int pid) { + // Copy values so it doesn't lock mProcesses while UI is being updated + final String name, title, description; + synchronized (mProcesses) { + final BugreportInfo info = mProcesses.get(pid); + if (info == null) { + Log.w(TAG, "No bugreport info for PID " + pid); + return; + } + name = info.name; + title = info.title; + description = info.description; + } + + // Closes the notification bar first. + sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)); + + mInfoDialog.initialize(getApplicationContext(), pid, name, title, description); + } + + /** * Finishes the service when it's not monitoring any more processes. */ private void stopSelfWhenDone() { @@ -440,7 +498,11 @@ public class BugreportProgressService extends Service { } } + /** + * Handles the BUGREPORT_FINISHED intent sent by {@code dumpstate}. + */ private void onBugreportFinished(int pid, Intent intent) { + mInfoDialog.onBugreportFinished(pid); final Context context = getApplicationContext(); BugreportInfo info; synchronized (mProcesses) { @@ -453,6 +515,7 @@ public class BugreportProgressService extends Service { } info.bugreportFile = getFileExtra(intent, EXTRA_BUGREPORT); info.screenshotFile = getFileExtra(intent, EXTRA_SCREENSHOT); + info.finished = true; } final Configuration conf = context.getResources().getConfiguration(); @@ -494,21 +557,32 @@ public class BugreportProgressService extends Service { /** * Build {@link Intent} that can be used to share the given bugreport. */ - private static Intent buildSendIntent(Context context, Uri bugreportUri, Uri screenshotUri) { + private static Intent buildSendIntent(Context context, BugreportInfo info) { + // Files are kept on private storage, so turn into Uris that we can + // grant temporary permissions for. + final Uri bugreportUri = getUri(context, info.bugreportFile); + final Uri screenshotUri = getUri(context, info.screenshotFile); + final Intent intent = new Intent(Intent.ACTION_SEND_MULTIPLE); final String mimeType = "application/vnd.android.bugreport"; intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); intent.addCategory(Intent.CATEGORY_DEFAULT); intent.setType(mimeType); - intent.putExtra(Intent.EXTRA_SUBJECT, bugreportUri.getLastPathSegment()); + final String subject = info.title != null ? info.title : bugreportUri.getLastPathSegment(); + intent.putExtra(Intent.EXTRA_SUBJECT, subject); // EXTRA_TEXT should be an ArrayList, but some clients are expecting a single String. // So, to avoid an exception on Intent.migrateExtraStreamToClipData(), we need to manually // create the ClipData object with the attachments URIs. - String messageBody = String.format("Build info: %s\nSerial number:%s", - SystemProperties.get("ro.build.description"), SystemProperties.get("ro.serialno")); - intent.putExtra(Intent.EXTRA_TEXT, messageBody); + StringBuilder messageBody = new StringBuilder("Build info: ") + .append(SystemProperties.get("ro.build.description")) + .append("\nSerial number: ") + .append(SystemProperties.get("ro.serialno")); + if (!TextUtils.isEmpty(info.description)) { + messageBody.append("\nDescription: ").append(info.description); + } + intent.putExtra(Intent.EXTRA_TEXT, messageBody.toString()); final ClipData clipData = new ClipData(null, new String[] { mimeType }, new ClipData.Item(null, null, null, bugreportUri)); final ArrayList<Uri> attachments = Lists.newArrayList(bugreportUri); @@ -542,12 +616,7 @@ public class BugreportProgressService extends Service { return; } } - // Files are kept on private storage, so turn into Uris that we can - // grant temporary permissions for. - final Uri bugreportUri = getUri(context, info.bugreportFile); - final Uri screenshotUri = getUri(context, info.screenshotFile); - - final Intent sendIntent = buildSendIntent(context, bugreportUri, screenshotUri); + final Intent sendIntent = buildSendIntent(context, info); final Intent notifIntent; // Send through warning dialog by default @@ -580,13 +649,17 @@ public class BugreportProgressService extends Service { .setContentTitle(title) .setTicker(title) .setContentText(context.getString(R.string.bugreport_finished_text)) - .setContentIntent(PendingIntent.getService(context, 0, shareIntent, - PendingIntent.FLAG_CANCEL_CURRENT)) + .setContentIntent(PendingIntent.getService(context, info.pid, shareIntent, + PendingIntent.FLAG_UPDATE_CURRENT)) .setDeleteIntent(newCancelIntent(context, info)) .setLocalOnly(true) .setColor(context.getColor( com.android.internal.R.color.system_notification_accent_color)); + if (!TextUtils.isEmpty(info.name)) { + builder.setContentInfo(info.name); + } + NotificationManager.from(context).notify(TAG, info.pid, builder.build()); } @@ -684,6 +757,231 @@ public class BugreportProgressService extends Service { } } + private static boolean setSystemProperty(String key, String value) { + try { + if (DEBUG) Log.v(TAG, "Setting system property" + key + " to " + value); + SystemProperties.set(key, value); + } catch (IllegalArgumentException e) { + Log.e(TAG, "Could not set property " + key + " to " + value, e); + return false; + } + return true; + } + + /** + * Updates the system property used by {@code dumpstate} to rename the final bugreport files. + */ + private boolean setBugreportNameProperty(int pid, String name) { + Log.d(TAG, "Updating bugreport name to " + name); + final String key = DUMPSTATE_PREFIX + pid + NAME_SUFFIX; + return setSystemProperty(key, name); + } + + /** + * Updates the user-provided details of a bugreport. + */ + private void updateBugreportInfo(int pid, String name, String title, String description) { + synchronized (mProcesses) { + final BugreportInfo info = mProcesses.get(pid); + if (info == null) { + Log.w(TAG, "No bugreport info for PID " + pid); + return; + } + info.title = title; + info.description = description; + if (name != null && !info.name.equals(name)) { + info.name = name; + updateProgress(info); + } + } + } + + /** + * Checks whether a character is valid on bugreport names. + */ + @VisibleForTesting + static boolean isValid(char c) { + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') + || c == '_' || c == '-'; + } + + /** + * Helper class encapsulating the UI elements and logic used to display a dialog where user + * can change the details of a bugreport. + */ + private final class BugreportInfoDialog { + private EditText mInfoName; + private EditText mInfoTitle; + private EditText mInfoDescription; + private AlertDialog mDialog; + private Button mOkButton; + private int mPid; + + /** + * Last "committed" value of the bugreport name. + * <p> + * Once initially set, it's only updated when user clicks the OK button. + */ + private String mSavedName; + + /** + * Last value of the bugreport name as entered by the user. + * <p> + * Every time it's changed the equivalent system property is changed as well, but if the + * user clicks CANCEL, the old value (stored on {@code mSavedName} is restored. + * <p> + * This logic handles the corner-case scenario where {@code dumpstate} finishes after the + * user changed the name but didn't clicked OK yet (for example, because the user is typing + * the description). The only drawback is that if the user changes the name while + * {@code dumpstate} is running but clicks CANCEL after it finishes, then the final name + * will be the one that has been canceled. But when {@code dumpstate} finishes the {code + * name} UI is disabled and the old name restored anyways, so the user will be "alerted" of + * such drawback. + */ + private String mTempName; + + /** + * Sets its internal state and displays the dialog. + */ + private synchronized void initialize(Context context, int pid, String name, String title, + String description) { + // First initializes singleton. + if (mDialog == null) { + @SuppressLint("InflateParams") + // It's ok pass null ViewRoot on AlertDialogs. + final View view = View.inflate(context, R.layout.dialog_bugreport_info, null); + + mInfoName = (EditText) view.findViewById(R.id.name); + mInfoTitle = (EditText) view.findViewById(R.id.title); + mInfoDescription = (EditText) view.findViewById(R.id.description); + + mInfoName.setOnFocusChangeListener(new OnFocusChangeListener() { + + @Override + public void onFocusChange(View v, boolean hasFocus) { + if (hasFocus) { + return; + } + sanitizeName(); + } + }); + + mDialog = new AlertDialog.Builder(context) + .setView(view) + .setTitle(context.getString(R.string.bugreport_info_dialog_title)) + .setCancelable(false) + .setPositiveButton(context.getString(com.android.internal.R.string.ok), + null) + .setNegativeButton(context.getString(com.android.internal.R.string.cancel), + new DialogInterface.OnClickListener() + { + @Override + public void onClick(DialogInterface dialog, int id) + { + if (!mTempName.equals(mSavedName)) { + // Must restore dumpstate's name since it was changed + // before user clicked OK. + setBugreportNameProperty(mPid, mSavedName); + } + } + }) + .create(); + + mDialog.getWindow().setAttributes( + new WindowManager.LayoutParams( + WindowManager.LayoutParams.TYPE_SYSTEM_DIALOG)); + + } + + // Then set fields. + mSavedName = mTempName = name; + mPid = pid; + if (!TextUtils.isEmpty(name)) { + mInfoName.setText(name); + } + if (!TextUtils.isEmpty(title)) { + mInfoTitle.setText(title); + } + if (!TextUtils.isEmpty(description)) { + mInfoDescription.setText(description); + } + + // And finally display it. + mDialog.show(); + + // TODO: in a traditional AlertDialog, when the positive button is clicked the + // dialog is always closed, but we need to validate the name first, so we need to + // get a reference to it, which is only available after it's displayed. + // It would be cleaner to use a regular dialog instead, but let's keep this + // workaround for now and change it later, when we add another button to take + // extra screenshots. + if (mOkButton == null) { + mOkButton = mDialog.getButton(DialogInterface.BUTTON_POSITIVE); + mOkButton.setOnClickListener(new View.OnClickListener() { + + @Override + public void onClick(View view) { + sanitizeName(); + final String name = mInfoName.getText().toString(); + final String title = mInfoTitle.getText().toString(); + final String description = mInfoDescription.getText().toString(); + + updateBugreportInfo(mPid, name, title, description); + mDialog.dismiss(); + } + }); + } + } + + /** + * Sanitizes the user-provided value for the {@code name} field, automatically replacing + * invalid characters if necessary. + */ + private synchronized void sanitizeName() { + String name = mInfoName.getText().toString(); + if (name.equals(mTempName)) { + if (DEBUG) Log.v(TAG, "name didn't change, no need to sanitize: " + name); + return; + } + final StringBuilder safeName = new StringBuilder(name.length()); + boolean changed = false; + for (int i = 0; i < name.length(); i++) { + final char c = name.charAt(i); + if (isValid(c)) { + safeName.append(c); + } else { + changed = true; + safeName.append('_'); + } + } + if (changed) { + Log.v(TAG, "changed invalid name '" + name + "' to '" + safeName + "'"); + name = safeName.toString(); + mInfoName.setText(name); + } + mTempName = name; + + // Must update system property for the cases where dumpstate finishes + // while the user is still entering other fields (like title or + // description) + setBugreportNameProperty(mPid, name); + } + + /** + * Notifies the dialog that the bugreport has finished so it disables the {@code name} + * field. + * <p>Once the bugreport is finished dumpstate has already generated the final files, so + * changing the name would have no effect. + */ + private synchronized void onBugreportFinished(int pid) { + if (mInfoName != null) { + mInfoName.setEnabled(false); + mInfoName.setText(mSavedName); + } + } + + } + /** * Information about a bugreport process while its in progress. */ @@ -704,6 +1002,18 @@ public class BugreportProgressService extends Service { String name; /** + * User-provided, one-line summary of the bug; when set, will be used as the subject + * of the {@link Intent#ACTION_SEND_MULTIPLE} intent. + */ + String title; + + /** + * User-provided, detailed description of the bugreport; when set, will be added to the body + * of the {@link Intent#ACTION_SEND_MULTIPLE} intent. + */ + String description; + + /** * Maximum progress of the bugreport generation. */ int max; @@ -761,6 +1071,7 @@ public class BugreportProgressService extends Service { public String toString() { final float percent = ((float) progress * 100 / max); return "pid: " + pid + ", name: " + name + ", finished: " + finished + + "\n\ttitle: " + title + "\n\tdescription: " + description + "\n\tfile: " + bugreportFile + "\n\tscreenshot: " + screenshotFile + "\n\tprogress: " + progress + "/" + max + "(" + percent + ")" + "\n\tlast_update: " + getFormattedLastUpdate(); diff --git a/packages/Shell/tests/Android.mk b/packages/Shell/tests/Android.mk index 62a37bc70f4e..1e0eaace35da 100644 --- a/packages/Shell/tests/Android.mk +++ b/packages/Shell/tests/Android.mk @@ -8,9 +8,7 @@ LOCAL_SRC_FILES := $(call all-java-files-under, src) LOCAL_JAVA_LIBRARIES := android.test.runner -# TODO: update and/or remove LOCAL_STATIC_JAVA_LIBRARIES := ub-uiautomator -#LOCAL_STATIC_JAVA_LIBRARIES := android-support-v4 mockito-target ub-uiautomator LOCAL_PACKAGE_NAME := ShellTests LOCAL_INSTRUMENTATION_FOR := Shell diff --git a/packages/Shell/tests/src/com/android/shell/BugreportReceiverTest.java b/packages/Shell/tests/src/com/android/shell/BugreportReceiverTest.java index 1f4d749f3992..7f609faac83d 100644 --- a/packages/Shell/tests/src/com/android/shell/BugreportReceiverTest.java +++ b/packages/Shell/tests/src/com/android/shell/BugreportReceiverTest.java @@ -94,7 +94,11 @@ public class BugreportReceiverTest extends InstrumentationTestCase { private static final int PID = 42; private static final String PROGRESS_PROPERTY = "dumpstate.42.progress"; private static final String MAX_PROPERTY = "dumpstate.42.max"; + private static final String NAME_PROPERTY = "dumpstate.42.name"; private static final String NAME = "BUG, Y U NO REPORT?"; + private static final String NEW_NAME = "Bug_Forrest_Bug"; + private static final String TITLE = "Wimbugdom Champion 2015"; + private String mDescription; private String mPlainTextPath; private String mZipPath; @@ -120,10 +124,17 @@ public class BugreportReceiverTest extends InstrumentationTestCase { createTextFile(mScreenshotPath, SCREENSHOT_CONTENT); createZipFile(mZipPath, BUGREPORT_FILE, BUGREPORT_CONTENT); + // Creates a multi-line description. + StringBuilder sb = new StringBuilder(); + for (int i = 1; i <= 20; i++) { + sb.append("All work and no play makes Shell a dull app!\n"); + } + mDescription = sb.toString(); + BugreportPrefs.setWarningState(mContext, BugreportPrefs.STATE_HIDE); } - public void testFullWorkflow() throws Exception { + public void testProgress() throws Exception { resetProperties(); sendBugreportStarted(1000); @@ -145,6 +156,81 @@ public class BugreportReceiverTest extends InstrumentationTestCase { assertServiceNotRunning(); } + public void testProgress_changeDetails() throws Exception { + resetProperties(); + sendBugreportStarted(1000); + + DetailsUi detailsUi = new DetailsUi(mUiBot); + + // Check initial name. + String actualName = detailsUi.nameField.getText().toString(); + assertEquals("Wrong value on field 'name'", NAME, actualName); + + // Change name - it should have changed system property once focus is changed. + detailsUi.nameField.setText(NEW_NAME); + detailsUi.focusAwayFromName(); + assertPropertyValue(NAME_PROPERTY, NEW_NAME); + + // Cancel the dialog to make sure property was restored. + detailsUi.clickCancel(); + assertPropertyValue(NAME_PROPERTY, NAME); + + // Now try to set an invalid name. + detailsUi.reOpen(); + detailsUi.nameField.setText("/etc/passwd"); + detailsUi.clickOk(); + assertPropertyValue(NAME_PROPERTY, "_etc_passwd"); + + // Finally, make the real changes. + detailsUi.reOpen(); + detailsUi.nameField.setText(NEW_NAME); + detailsUi.titleField.setText(TITLE); + detailsUi.descField.setText(mDescription); + + detailsUi.clickOk(); + + assertPropertyValue(NAME_PROPERTY, NEW_NAME); + assertProgressNotification(NEW_NAME, "0.00%"); + + Bundle extras = sendBugreportFinishedAndGetSharedIntent(PID, mPlainTextPath, + mScreenshotPath); + assertActionSendMultiple(extras, TITLE, mDescription, BUGREPORT_CONTENT, SCREENSHOT_CONTENT); + + assertServiceNotRunning(); + } + + public void testProgress_bugreportFinishedWhileChangingDetails() throws Exception { + resetProperties(); + sendBugreportStarted(1000); + + DetailsUi detailsUi = new DetailsUi(mUiBot); + + // Finish the bugreport while user's still typing the name. + detailsUi.nameField.setText(NEW_NAME); + sendBugreportFinished(PID, mPlainTextPath, mScreenshotPath); + + // Wait until the share notifcation is received... + mUiBot.getNotification(mContext.getString(R.string.bugreport_finished_title)); + // ...then close notification bar. + mContext.sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)); + + // Make sure UI was updated properly. + assertFalse("didn't disable name on UI", detailsUi.nameField.isEnabled()); + assertEquals("didn't revert name on UI", NAME, detailsUi.nameField.getText().toString()); + + // Finish changing other fields. + detailsUi.titleField.setText(TITLE); + detailsUi.descField.setText(mDescription); + detailsUi.clickOk(); + + // Finally, share bugreport. + Bundle extras = acceptBugreportAndGetSharedIntent(); + assertActionSendMultiple(extras, TITLE, mDescription, BUGREPORT_CONTENT, + SCREENSHOT_CONTENT); + + assertServiceNotRunning(); + } + public void testBugreportFinished_withWarning() throws Exception { // Explicitly shows the warning. BugreportPrefs.setWarningState(mContext, BugreportPrefs.STATE_SHOW); @@ -204,14 +290,18 @@ public class BugreportReceiverTest extends InstrumentationTestCase { private void assertProgressNotification(String name, String percent) { // TODO: it current looks for 3 distinct objects, without taking advantage of their // relationship. - String title = mContext.getString(R.string.bugreport_in_progress_title); - Log.v(TAG, "Looking for progress notification title: '" + title+ "'"); - mUiBot.getNotification(title); + openProgressNotification(); Log.v(TAG, "Looking for progress notification details: '" + name + "-" + percent + "'"); mUiBot.getObject(name); mUiBot.getObject(percent); } + private void openProgressNotification() { + String title = mContext.getString(R.string.bugreport_in_progress_title); + Log.v(TAG, "Looking for progress notification title: '" + title + "'"); + mUiBot.getNotification(title); + } + void resetProperties() { // TODO: call method to remove property instead SystemProperties.set(PROGRESS_PROPERTY, "0"); @@ -270,7 +360,6 @@ public class BugreportReceiverTest extends InstrumentationTestCase { /** * Sends a "bugreport finished" intent. - * */ private void sendBugreportFinished(Integer pid, String bugreportPath, String screenshotPath) { Intent intent = new Intent(INTENT_BUGREPORT_FINISHED); @@ -292,13 +381,21 @@ public class BugreportReceiverTest extends InstrumentationTestCase { */ private void assertActionSendMultiple(Bundle extras, String bugreportContent, String screenshotContent) throws IOException { + assertActionSendMultiple(extras, ZIP_FILE, null, bugreportContent, screenshotContent); + } + + private void assertActionSendMultiple(Bundle extras, String subject, String description, + String bugreportContent, String screenshotContent) throws IOException { String body = extras.getString(Intent.EXTRA_TEXT); assertContainsRegex("missing build info", SystemProperties.get("ro.build.description"), body); assertContainsRegex("missing serial number", SystemProperties.get("ro.serialno"), body); + if (description != null) { + assertContainsRegex("missing description", description, body); + } - assertEquals("wrong subject", ZIP_FILE, extras.getString(Intent.EXTRA_SUBJECT)); + assertEquals("wrong subject", subject, extras.getString(Intent.EXTRA_SUBJECT)); List<Uri> attachments = extras.getParcelableArrayList(Intent.EXTRA_STREAM); int expectedSize = screenshotContent != null ? 2 : 1; @@ -355,6 +452,11 @@ public class BugreportReceiverTest extends InstrumentationTestCase { fail("Did not find entry '" + entryName + "' on file '" + uri + "'"); } + private void assertPropertyValue(String key, String expectedValue) { + String actualValue = SystemProperties.get(key); + assertEquals("Wrong value for property '" + key + "'", expectedValue, actualValue); + } + private void assertServiceNotRunning() { String service = BugreportProgressService.class.getName(); assertFalse("Service '" + service + "' is still running", isServiceRunning(service)); @@ -402,4 +504,55 @@ public class BugreportReceiverTest extends InstrumentationTestCase { Log.v(TAG, "Path for '" + file + "': " + path); return path; } + + /** + * Helper class containing the UiObjects present in the bugreport info dialog. + */ + private final class DetailsUi { + + final UiObject detailsButton; + final UiObject nameField; + final UiObject titleField; + final UiObject descField; + final UiObject okButton; + final UiObject cancelButton; + + /** + * Gets the UI objects by opening the progress notification and clicking DETAILS. + */ + DetailsUi(UiBot uiBot) { + openProgressNotification(); + detailsButton = mUiBot.getVisibleObject( + mContext.getString(R.string.bugreport_info_action).toUpperCase()); + mUiBot.click(detailsButton, "details_button"); + // TODO: unhardcode resource ids + nameField = mUiBot.getVisibleObjectById("com.android.shell:id/name"); + titleField = mUiBot.getVisibleObjectById("com.android.shell:id/title"); + descField = mUiBot.getVisibleObjectById("com.android.shell:id/description"); + okButton = mUiBot.getObjectById("android:id/button1"); + cancelButton = mUiBot.getObjectById("android:id/button2"); + } + + /** + * Takes focus away from the name field so it can be validated. + */ + void focusAwayFromName() { + mUiBot.click(titleField, "title_field"); // Change focus. + mUiBot.pressBack(); // Dismiss keyboard. + } + + void reOpen() { + openProgressNotification(); + mUiBot.click(detailsButton, "details_button"); + + } + + void clickOk() { + mUiBot.click(okButton, "details_ok_button"); + } + + void clickCancel() { + mUiBot.click(cancelButton, "details_cancel_button"); + } + } } diff --git a/packages/Shell/tests/src/com/android/shell/UiBot.java b/packages/Shell/tests/src/com/android/shell/UiBot.java index c87172720ad7..384c3daa51c9 100644 --- a/packages/Shell/tests/src/com/android/shell/UiBot.java +++ b/packages/Shell/tests/src/com/android/shell/UiBot.java @@ -79,6 +79,17 @@ final class UiBot { } /** + * Gets an object that might not yet be available in current UI. + * + * @param id Object's fully-qualified resource id (like {@code android:id/button1}) + */ + public UiObject getObjectById(String id) { + boolean gotIt = mDevice.wait(Until.hasObject(By.res(id)), mTimeout); + assertTrue("object with id '(" + id + "') not visible yet", gotIt); + return getVisibleObjectById(id); + } + + /** * Gets an object which is guaranteed to be present in the current UI. * * @param text Object's text as displayed by the UI. @@ -90,6 +101,18 @@ final class UiBot { } /** + * Gets an object which is guaranteed to be present in the current UI. + * + * @param text Object's text as displayed by the UI. + */ + public UiObject getVisibleObjectById(String id) { + UiObject uiObject = mDevice.findObject(new UiSelector().resourceId(id)); + assertTrue("could not find object with id '" + id+ "'", uiObject.exists()); + return uiObject; + } + + + /** * Clicks on a UI element. * * @param uiObject UI element to be clicked. @@ -151,4 +174,8 @@ final class UiBot { click(activity, name); } } + + public void pressBack() { + mDevice.pressBack(); + } } diff --git a/packages/Shell/tests/src/com/android/shell/UtilitiesTest.java b/packages/Shell/tests/src/com/android/shell/UtilitiesTest.java new file mode 100644 index 000000000000..51b7ba8529a8 --- /dev/null +++ b/packages/Shell/tests/src/com/android/shell/UtilitiesTest.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2015 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.shell; + +import android.test.suitebuilder.annotation.SmallTest; +import junit.framework.TestCase; +import static com.android.shell.BugreportProgressService.isValid; + +@SmallTest +public class UtilitiesTest extends TestCase { + + public void testIsValidChar_valid() { + String validChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; + for (int i = 0; i < validChars.length(); i++) { + char c = validChars.charAt(i); + assertTrue("char '" + c + "' should be valid", isValid(c)); + } + } + + public void testIsValidChar_invalid() { + String validChars = "/.<>;:'\'\"\\+=*&^%$#@!`~áéÃóúãñÂÊÎÔÛ"; + for (int i = 0; i < validChars.length(); i++) { + char c = validChars.charAt(i); + assertFalse("char '" + c + "' should not be valid", isValid(c)); + } + } +} diff --git a/packages/SystemUI/Android.mk b/packages/SystemUI/Android.mk index eb63e5d55f9a..61cad2fde6d8 100644 --- a/packages/SystemUI/Android.mk +++ b/packages/SystemUI/Android.mk @@ -8,7 +8,11 @@ LOCAL_SRC_FILES := $(call all-java-files-under, src) $(call all-proto-files-unde LOCAL_STATIC_JAVA_LIBRARIES := \ Keyguard \ - android-support-v7-recyclerview + android-support-v7-recyclerview \ + android-support-v7-preference \ + android-support-v7-appcompat \ + android-support-v14-preference + LOCAL_JAVA_LIBRARIES := telephony-common LOCAL_PACKAGE_NAME := SystemUI @@ -22,10 +26,13 @@ LOCAL_PROGUARD_FLAG_FILES := proguard.flags LOCAL_RESOURCE_DIR := \ frameworks/base/packages/Keyguard/res \ $(LOCAL_PATH)/res \ + frameworks/support/v7/preference/res \ + frameworks/support/v14/preference/res \ + frameworks/support/v7/appcompat/res \ frameworks/support/v7/recyclerview/res LOCAL_AAPT_FLAGS := --auto-add-overlay \ - --extra-packages com.android.keyguard:android.support.v7.recyclerview + --extra-packages com.android.keyguard:android.support.v7.recyclerview:android.support.v7.preference:android.support.v14.preference:android.support.v7.appcompat ifneq ($(SYSTEM_UI_INCREMENTAL_BUILDS),) LOCAL_PROGUARD_ENABLED := disabled diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml index 51b84f52498d..02ddae6845bc 100644 --- a/packages/SystemUI/AndroidManifest.xml +++ b/packages/SystemUI/AndroidManifest.xml @@ -186,7 +186,7 @@ <activity android:name=".tuner.TunerActivity" android:enabled="false" android:icon="@drawable/tuner" - android:theme="@android:style/Theme.Material.Settings" + android:theme="@style/TunerSettings" android:label="@string/system_ui_tuner" android:process=":tuner" android:exported="true"> diff --git a/packages/SystemUI/proguard.flags b/packages/SystemUI/proguard.flags index 75e795960737..6a10c2c62508 100644 --- a/packages/SystemUI/proguard.flags +++ b/packages/SystemUI/proguard.flags @@ -27,3 +27,9 @@ public float getTaskProgress(); public void setTaskProgress(float); } + +-keepclasseswithmembers class * { + public <init>(android.content.Context, android.util.AttributeSet); +} + +-keep class ** extends android.support.v14.preference.PreferenceFragment diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml index 47ad6dc2a8ad..aad428ae4032 100644 --- a/packages/SystemUI/res/values/styles.xml +++ b/packages/SystemUI/res/values/styles.xml @@ -315,4 +315,8 @@ <item name="android:layout_height">48dp</item> </style> + <style name="TunerSettings" parent="@android:style/Theme.Material.Settings"> + <item name="preferenceTheme">@android:style/Theme.Material.Settings</item> + </style> + </resources> diff --git a/packages/SystemUI/res/xml/tuner_prefs.xml b/packages/SystemUI/res/xml/tuner_prefs.xml index 43359b32fe8a..6103216d7cab 100644 --- a/packages/SystemUI/res/xml/tuner_prefs.xml +++ b/packages/SystemUI/res/xml/tuner_prefs.xml @@ -24,6 +24,7 @@ sysui:defValue="true" /> <PreferenceScreen + android:key="status_bar" android:title="@string/status_bar" > <com.android.systemui.tuner.StatusBarSwitch @@ -70,6 +71,7 @@ <PreferenceScreen + android:key="overview" android:title="@string/overview" > <com.android.systemui.tuner.TunerSwitch @@ -97,7 +99,8 @@ <Preference android:key="demo_mode" - android:title="@string/demo_mode" /> + android:title="@string/demo_mode" + android:fragment="com.android.systemui.tuner.DemoModeFragment" /> <!-- Warning, this goes last. --> <Preference diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/TaskView.java b/packages/SystemUI/src/com/android/systemui/recents/views/TaskView.java index 3b6130c76bf4..df7b9a63089b 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/views/TaskView.java +++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskView.java @@ -341,11 +341,10 @@ public class TaskView extends FrameLayout implements Task.TaskCallbacks, if (launchState.launchedFromAppWithThumbnail) { if (mTask.isLaunchTarget) { + ctx.postAnimationTrigger.increment(); // Immediately start the dim animation animateDimToProgress(taskViewEnterFromAppDuration, ctx.postAnimationTrigger.decrementOnAnimationEnd()); - ctx.postAnimationTrigger.increment(); - // Animate the action button in fadeInActionButton(taskViewEnterFromAppDuration); } else { diff --git a/packages/SystemUI/src/com/android/systemui/tuner/DemoModeFragment.java b/packages/SystemUI/src/com/android/systemui/tuner/DemoModeFragment.java index a2b062c8b710..f1de2342c9ad 100644 --- a/packages/SystemUI/src/com/android/systemui/tuner/DemoModeFragment.java +++ b/packages/SystemUI/src/com/android/systemui/tuner/DemoModeFragment.java @@ -22,12 +22,12 @@ import android.database.ContentObserver; import android.os.Bundle; import android.os.Handler; import android.os.Looper; -import android.preference.Preference; -import android.preference.Preference.OnPreferenceChangeListener; -import android.preference.PreferenceFragment; -import android.preference.PreferenceScreen; -import android.preference.SwitchPreference; import android.provider.Settings; +import android.support.v14.preference.PreferenceFragment; +import android.support.v14.preference.SwitchPreference; +import android.support.v7.preference.Preference; +import android.support.v7.preference.Preference.OnPreferenceChangeListener; +import android.support.v7.preference.PreferenceScreen; import android.view.MenuItem; import com.android.internal.logging.MetricsLogger; @@ -56,9 +56,7 @@ public class DemoModeFragment extends PreferenceFragment implements OnPreference private SwitchPreference mOnSwitch; @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - + public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { Context context = getContext(); mEnabledSwitch = new SwitchPreference(context); mEnabledSwitch.setTitle(R.string.enable_demo_mode); diff --git a/packages/SystemUI/src/com/android/systemui/tuner/StatusBarSwitch.java b/packages/SystemUI/src/com/android/systemui/tuner/StatusBarSwitch.java index dcb0d8d0c498..920ec7572b8a 100644 --- a/packages/SystemUI/src/com/android/systemui/tuner/StatusBarSwitch.java +++ b/packages/SystemUI/src/com/android/systemui/tuner/StatusBarSwitch.java @@ -18,8 +18,8 @@ package com.android.systemui.tuner; import android.app.ActivityManager; import android.content.ContentResolver; import android.content.Context; -import android.preference.SwitchPreference; import android.provider.Settings; +import android.support.v14.preference.SwitchPreference; import android.text.TextUtils; import android.util.AttributeSet; @@ -38,15 +38,15 @@ public class StatusBarSwitch extends SwitchPreference implements Tunable { } @Override - protected void onAttachedToActivity() { - super.onAttachedToActivity(); + public void onAttached() { + super.onAttached(); TunerService.get(getContext()).addTunable(this, StatusBarIconController.ICON_BLACKLIST); } @Override - protected void onDetachedFromActivity() { + public void onDetached() { TunerService.get(getContext()).removeTunable(this); - super.onDetachedFromActivity(); + super.onDetached(); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/tuner/TunerActivity.java b/packages/SystemUI/src/com/android/systemui/tuner/TunerActivity.java index c84f61865522..4173ecc2d7fc 100644 --- a/packages/SystemUI/src/com/android/systemui/tuner/TunerActivity.java +++ b/packages/SystemUI/src/com/android/systemui/tuner/TunerActivity.java @@ -15,16 +15,64 @@ */ package com.android.systemui.tuner; -import android.app.Activity; +import android.app.Fragment; +import android.app.FragmentTransaction; import android.os.Bundle; +import android.support.v14.preference.PreferenceFragment; +import android.support.v7.preference.Preference; +import android.support.v7.preference.PreferenceScreen; +import android.util.Log; -public class TunerActivity extends Activity { +import com.android.settingslib.drawer.SettingsDrawerActivity; +import com.android.systemui.R; + +public class TunerActivity extends SettingsDrawerActivity implements + PreferenceFragment.OnPreferenceStartFragmentCallback, + PreferenceFragment.OnPreferenceStartScreenCallback { protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - getFragmentManager().beginTransaction().replace(android.R.id.content, new TunerFragment()) + getFragmentManager().beginTransaction().replace(R.id.content_frame, new TunerFragment()) .commit(); } + @Override + public boolean onPreferenceStartFragment(PreferenceFragment caller, Preference pref) { + try { + Class<?> cls = Class.forName(pref.getFragment()); + Fragment fragment = (Fragment) cls.newInstance(); + FragmentTransaction transaction = getFragmentManager().beginTransaction(); + transaction.replace(R.id.content_frame, fragment); + transaction.addToBackStack("PreferenceFragment"); + transaction.commit(); + return true; + } catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) { + Log.d("TunerActivity", "Problem launching fragment", e); + return false; + } + } + + @Override + public boolean onPreferenceStartScreen(PreferenceFragment caller, PreferenceScreen pref) { + FragmentTransaction transaction = getFragmentManager().beginTransaction(); + SubSettingsFragment fragment = new SubSettingsFragment(); + final Bundle b = new Bundle(1); + b.putString(PreferenceFragment.ARG_PREFERENCE_ROOT, pref.getKey()); + fragment.setArguments(b); + fragment.setTargetFragment(caller, 0); + transaction.replace(R.id.content_frame, fragment); + transaction.addToBackStack("PreferenceFragment"); + transaction.commit(); + return true; + } + + public static class SubSettingsFragment extends PreferenceFragment { + @Override + public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + setPreferenceScreen((PreferenceScreen) ((PreferenceFragment) getTargetFragment()) + .getPreferenceScreen().findPreference(rootKey)); + } + } + } diff --git a/packages/SystemUI/src/com/android/systemui/tuner/TunerFragment.java b/packages/SystemUI/src/com/android/systemui/tuner/TunerFragment.java index b620b50bfcff..a3fe6bb61a18 100644 --- a/packages/SystemUI/src/com/android/systemui/tuner/TunerFragment.java +++ b/packages/SystemUI/src/com/android/systemui/tuner/TunerFragment.java @@ -24,20 +24,19 @@ import android.database.ContentObserver; import android.net.Uri; import android.os.Bundle; import android.os.Handler; -import android.preference.Preference; -import android.preference.Preference.OnPreferenceChangeListener; -import android.preference.Preference.OnPreferenceClickListener; -import android.preference.PreferenceFragment; -import android.preference.SwitchPreference; import android.provider.Settings; import android.provider.Settings.System; +import android.support.v14.preference.PreferenceFragment; +import android.support.v14.preference.SwitchPreference; +import android.support.v7.preference.Preference; +import android.support.v7.preference.Preference.OnPreferenceChangeListener; +import android.support.v7.preference.Preference.OnPreferenceClickListener; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; + import com.android.internal.logging.MetricsLogger; import com.android.systemui.R; -import com.android.systemui.qs.QSPanel; -import com.android.systemui.tuner.TunerService.Tunable; import static com.android.systemui.BatteryMeterDrawable.SHOW_PERCENT_SETTING; @@ -45,8 +44,6 @@ public class TunerFragment extends PreferenceFragment { private static final String TAG = "TunerFragment"; - private static final String KEY_QS_TUNER = "qs_tuner"; - private static final String KEY_DEMO_MODE = "demo_mode"; private static final String KEY_BATTERY_PCT = "battery_pct"; public static final String SETTING_SEEN_TUNER_WARNING = "seen_tuner_warning"; @@ -59,23 +56,18 @@ public class TunerFragment extends PreferenceFragment { private SwitchPreference mBatteryPct; + @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - addPreferencesFromResource(R.xml.tuner_prefs); getActivity().getActionBar().setDisplayHomeAsUpEnabled(true); setHasOptionsMenu(true); + } + + @Override + public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + addPreferencesFromResource(R.xml.tuner_prefs); - findPreference(KEY_DEMO_MODE).setOnPreferenceClickListener(new OnPreferenceClickListener() { - @Override - public boolean onPreferenceClick(Preference preference) { - FragmentTransaction ft = getFragmentManager().beginTransaction(); - ft.replace(android.R.id.content, new DemoModeFragment(), "DemoMode"); - ft.addToBackStack(null); - ft.commit(); - return true; - } - }); mBatteryPct = (SwitchPreference) findPreference(KEY_BATTERY_PCT); if (Settings.Secure.getInt(getContext().getContentResolver(), SETTING_SEEN_TUNER_WARNING, 0) == 0) { diff --git a/packages/SystemUI/src/com/android/systemui/tuner/TunerSwitch.java b/packages/SystemUI/src/com/android/systemui/tuner/TunerSwitch.java index 54078b0cfd91..7ad752ec74ce 100644 --- a/packages/SystemUI/src/com/android/systemui/tuner/TunerSwitch.java +++ b/packages/SystemUI/src/com/android/systemui/tuner/TunerSwitch.java @@ -2,8 +2,8 @@ package com.android.systemui.tuner; import android.content.Context; import android.content.res.TypedArray; -import android.preference.SwitchPreference; import android.provider.Settings; +import android.support.v14.preference.SwitchPreference; import android.util.AttributeSet; import com.android.systemui.R; @@ -21,15 +21,15 @@ public class TunerSwitch extends SwitchPreference implements Tunable { } @Override - protected void onAttachedToActivity() { - super.onAttachedToActivity(); + public void onAttached() { + super.onAttached(); TunerService.get(getContext()).addTunable(this, getKey()); } @Override - protected void onDetachedFromActivity() { + public void onDetached() { TunerService.get(getContext()).removeTunable(this); - super.onDetachedFromActivity(); + super.onDetached(); } @Override diff --git a/packages/SystemUI/tests/Android.mk b/packages/SystemUI/tests/Android.mk index 9cf64d347e4a..b7a41e21e7d4 100644 --- a/packages/SystemUI/tests/Android.mk +++ b/packages/SystemUI/tests/Android.mk @@ -21,7 +21,8 @@ LOCAL_PROTOC_OPTIMIZE_TYPE := nano LOCAL_PROTOC_FLAGS := -I$(LOCAL_PATH)/.. LOCAL_PROTO_JAVA_OUTPUT_PARAMS := optional_field_style=accessors -LOCAL_AAPT_FLAGS := --auto-add-overlay --extra-packages com.android.systemui:com.android.keyguard +LOCAL_AAPT_FLAGS := --auto-add-overlay \ + --extra-packages com.android.systemui:com.android.keyguard:android.support.v14.preference:android.support.v7.preference:android.support.v7.appcompat:android.support.v7.recyclerview LOCAL_SRC_FILES := $(call all-java-files-under, src) \ $(call all-Iaidl-files-under, src) \ @@ -30,6 +31,10 @@ LOCAL_SRC_FILES := $(call all-java-files-under, src) \ src/com/android/systemui/EventLogTags.logtags LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res \ + frameworks/support/v7/preference/res \ + frameworks/support/v14/preference/res \ + frameworks/support/v7/appcompat/res \ + frameworks/support/v7/recyclerview/res \ frameworks/base/packages/SystemUI/res \ frameworks/base/packages/Keyguard/res @@ -40,7 +45,10 @@ LOCAL_PACKAGE_NAME := SystemUITests LOCAL_STATIC_JAVA_LIBRARIES := \ mockito-target \ Keyguard \ - android-support-v7-recyclerview + android-support-v7-recyclerview \ + android-support-v7-preference \ + android-support-v7-appcompat \ + android-support-v14-preference # sign this with platform cert, so this test is allowed to inject key events into # UI it doesn't own. This is necessary to allow screenshots to be taken diff --git a/packages/WallpaperCropper/res/values/styles.xml b/packages/WallpaperCropper/res/values/styles.xml index e438c84027db..0f9e247f1c2b 100644 --- a/packages/WallpaperCropper/res/values/styles.xml +++ b/packages/WallpaperCropper/res/values/styles.xml @@ -15,7 +15,7 @@ --> <resources> - <style name="Theme.WallpaperCropper" parent="@android:style/Theme.Material.DayNight"> + <style name="Theme.WallpaperCropper" parent="@*android:style/Theme.Material.DayNight"> <item name="android:actionBarStyle">@style/WallpaperCropperActionBar</item> <item name="android:windowFullscreen">true</item> <item name="android:windowActionBarOverlay">true</item> diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index d8724b27e0e1..bcd8efdd61cf 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -7171,24 +7171,64 @@ public final class ActivityManagerService extends ActivityManagerNative @Override public boolean inMultiWindowMode(IBinder token) { - synchronized(this) { - final ActivityRecord r = ActivityRecord.isInStackLocked(token); - if (r == null) { - return false; + final long origId = Binder.clearCallingIdentity(); + try { + synchronized(this) { + final ActivityRecord r = ActivityRecord.isInStackLocked(token); + if (r == null) { + return false; + } + // An activity is consider to be in multi-window mode if its task isn't fullscreen. + return !r.task.mFullscreen; } - // An activity is consider to be in multi-window mode if its task isn't fullscreen. - return !r.task.mFullscreen; + } finally { + Binder.restoreCallingIdentity(origId); } } @Override public boolean inPictureInPictureMode(IBinder token) { - synchronized(this) { - final ActivityStack stack = ActivityRecord.getStackLocked(token); - if (stack == null) { - return false; + final long origId = Binder.clearCallingIdentity(); + try { + synchronized(this) { + final ActivityStack stack = ActivityRecord.getStackLocked(token); + if (stack == null) { + return false; + } + return stack.mStackId == PINNED_STACK_ID; } - return stack.mStackId == PINNED_STACK_ID; + } finally { + Binder.restoreCallingIdentity(origId); + } + } + + @Override + public void enterPictureInPictureMode(IBinder token) { + final long origId = Binder.clearCallingIdentity(); + try { + synchronized(this) { + if (!mSupportsPictureInPicture) { + throw new IllegalStateException("enterPictureInPictureMode: " + + "Device doesn't support picture-in-picture mode."); + } + + final ActivityRecord r = ActivityRecord.forTokenLocked(token); + + if (r == null) { + throw new IllegalStateException("enterPictureInPictureMode: " + + "Can't find activity for token=" + token); + } + + if (!r.info.supportsPip) { + throw new IllegalArgumentException("enterPictureInPictureMode: " + + "Picture-In-Picture not supported for r=" + r); + } + + mStackSupervisor.moveActivityToStackLocked( + r, PINNED_STACK_ID, "enterPictureInPictureMode", null); + } + } finally { + Binder.restoreCallingIdentity(origId); } } diff --git a/services/core/java/com/android/server/am/ActivityStackSupervisor.java b/services/core/java/com/android/server/am/ActivityStackSupervisor.java index 9117806257f0..f7d66afd148f 100644 --- a/services/core/java/com/android/server/am/ActivityStackSupervisor.java +++ b/services/core/java/com/android/server/am/ActivityStackSupervisor.java @@ -3623,24 +3623,30 @@ public final class ActivityStackSupervisor implements DisplayListener { return false; } + moveActivityToStackLocked(r, PINNED_STACK_ID, "moveTopActivityToPinnedStack", bounds); + return true; + } + + void moveActivityToStackLocked(ActivityRecord r, int stackId, String reason, Rect bounds) { final TaskRecord task = r.task; if (task.mActivities.size() == 1) { // There is only one activity in the task. So, we can just move the task over to the - // pinned stack without re-parenting the activity in a different task. - moveTaskToStackLocked(task.taskId, PINNED_STACK_ID, ON_TOP, FORCE_FOCUS, - "moveTopActivityToPinnedStack", true /* animate */); + // stack without re-parenting the activity in a different task. + moveTaskToStackLocked( + task.taskId, stackId, ON_TOP, FORCE_FOCUS, reason, true /* animate */); } else { final ActivityStack pinnedStack = getStack(PINNED_STACK_ID, CREATE_IF_NEEDED, ON_TOP); pinnedStack.moveActivityToStack(r); } - resizeStackLocked(PINNED_STACK_ID, bounds, !PRESERVE_WINDOWS, true); + if (bounds != null) { + resizeStackLocked(PINNED_STACK_ID, bounds, !PRESERVE_WINDOWS, true); + } // The task might have already been running and its visibility needs to be synchronized with // the visibility of the stack / windows. ensureActivitiesVisibleLocked(null, 0, !PRESERVE_WINDOWS); resumeTopActivitiesLocked(); - return true; } void positionTaskInStackLocked(int taskId, int stackId, int position) { diff --git a/services/core/java/com/android/server/notification/ZenModeConditions.java b/services/core/java/com/android/server/notification/ZenModeConditions.java index cee9ec8a026d..9cdece525bb3 100644 --- a/services/core/java/com/android/server/notification/ZenModeConditions.java +++ b/services/core/java/com/android/server/notification/ZenModeConditions.java @@ -109,7 +109,6 @@ public class ZenModeConditions implements ConditionProviders.Callback { if (DEBUG) Log.d(TAG, "onConditionChanged " + id + " " + condition); ZenModeConfig config = mHelper.getConfig(); if (config == null) return; - config = config.copy(); boolean updated = updateCondition(id, condition, config.manualRule); for (ZenRule automaticRule : config.automaticRules.values()) { updated |= updateCondition(id, condition, automaticRule); diff --git a/services/core/java/com/android/server/notification/ZenModeHelper.java b/services/core/java/com/android/server/notification/ZenModeHelper.java index 6030bab6db64..85c3cf88af19 100644 --- a/services/core/java/com/android/server/notification/ZenModeHelper.java +++ b/services/core/java/com/android/server/notification/ZenModeHelper.java @@ -77,6 +77,9 @@ public class ZenModeHelper { static final String TAG = "ZenModeHelper"; static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + // The amount of time rules instances can exist without their owning app being installed. + private static final int RULE_INSTANCE_GRACE_PERIOD = 1000 * 60 * 60 * 72; + private final Context mContext; private final H mHandler; private final SettingsObserver mSettingsObserver; @@ -93,6 +96,7 @@ public class ZenModeHelper { private int mUser = UserHandle.USER_SYSTEM; private ZenModeConfig mConfig; private AudioManagerInternal mAudioManager; + private PackageManager mPm; private boolean mEffectsSuppressed; public ZenModeHelper(Context context, Looper looper, ConditionProviders conditionProviders) { @@ -170,7 +174,9 @@ public class ZenModeHelper { if (mAudioManager != null) { mAudioManager.setRingerModeDelegate(mRingerModeDelegate); } + mPm = mContext.getPackageManager(); mHandler.postMetricsTimer(); + cleanUpZenRules(); evaluateZenMode("onSystemReady", true); } @@ -184,7 +190,10 @@ public class ZenModeHelper { config = mDefaultConfig.copy(); config.user = user; } - setConfig(config, "onUserSwitched"); + synchronized (mConfig) { + setConfig(config, "onUserSwitched"); + } + cleanUpZenRules(); } public void onUserRemoved(int user) { @@ -253,14 +262,15 @@ public class ZenModeHelper { throw new IllegalArgumentException("Rule already exists"); } newConfig = mConfig.copy(); - } - ZenRule rule = new ZenRule(); - populateZenRule(automaticZenRule, rule, true); - newConfig.automaticRules.put(rule.id, rule); - if (setConfig(newConfig, reason, true)) { - return createAutomaticZenRule(rule); - } else { - return null; + + ZenRule rule = new ZenRule(); + populateZenRule(automaticZenRule, rule, true); + newConfig.automaticRules.put(rule.id, rule); + if (setConfig(newConfig, reason, true)) { + return createAutomaticZenRule(rule); + } else { + return null; + } } } @@ -273,21 +283,21 @@ public class ZenModeHelper { + " reason=" + reason); } newConfig = mConfig.copy(); - } - final String ruleId = automaticZenRule.getId(); - ZenModeConfig.ZenRule rule; - if (ruleId == null) { - throw new IllegalArgumentException("Rule doesn't exist"); - } else { - rule = newConfig.automaticRules.get(ruleId); - if (rule == null || !canManageAutomaticZenRule(rule)) { - throw new SecurityException( - "Cannot update rules not owned by your condition provider"); + final String ruleId = automaticZenRule.getId(); + ZenModeConfig.ZenRule rule; + if (ruleId == null) { + throw new IllegalArgumentException("Rule doesn't exist"); + } else { + rule = newConfig.automaticRules.get(ruleId); + if (rule == null || !canManageAutomaticZenRule(rule)) { + throw new SecurityException( + "Cannot update rules not owned by your condition provider"); + } } + populateZenRule(automaticZenRule, rule, false); + newConfig.automaticRules.put(ruleId, rule); + return setConfig(newConfig, reason, true); } - populateZenRule(automaticZenRule, rule, false); - newConfig.automaticRules.put(ruleId, rule); - return setConfig(newConfig, reason, true); } public boolean removeAutomaticZenRule(String id, String reason) { @@ -295,17 +305,17 @@ public class ZenModeHelper { synchronized (mConfig) { if (mConfig == null) return false; newConfig = mConfig.copy(); + ZenRule rule = newConfig.automaticRules.get(id); + if (rule == null) return false; + if (canManageAutomaticZenRule(rule)) { + newConfig.automaticRules.remove(id); + if (DEBUG) Log.d(TAG, "removeZenRule zenRule=" + id + " reason=" + reason); + } else { + throw new SecurityException( + "Cannot delete rules not owned by your condition provider"); + } + return setConfig(newConfig, reason, true); } - ZenRule rule = newConfig.automaticRules.get(id); - if (rule == null) return false; - if (canManageAutomaticZenRule(rule)) { - newConfig.automaticRules.remove(id); - if (DEBUG) Log.d(TAG, "removeZenRule zenRule=" + id + " reason=" + reason); - } else { - throw new SecurityException( - "Cannot delete rules not owned by your condition provider"); - } - return setConfig(newConfig, reason, true); } public boolean removeAutomaticZenRules(String packageName, String reason) { @@ -313,15 +323,15 @@ public class ZenModeHelper { synchronized (mConfig) { if (mConfig == null) return false; newConfig = mConfig.copy(); - } - for (int i = newConfig.automaticRules.size() - 1; i >= 0; i--) { - ZenRule rule = newConfig.automaticRules.get(newConfig.automaticRules.keyAt(i)); - if (rule.component.getPackageName().equals(packageName) - && canManageAutomaticZenRule(rule)) { - newConfig.automaticRules.removeAt(i); + for (int i = newConfig.automaticRules.size() - 1; i >= 0; i--) { + ZenRule rule = newConfig.automaticRules.get(newConfig.automaticRules.keyAt(i)); + if (rule.component.getPackageName().equals(packageName) + && canManageAutomaticZenRule(rule)) { + newConfig.automaticRules.removeAt(i); + } } + return setConfig(newConfig, reason, true); } - return setConfig(newConfig, reason, true); } public boolean canManageAutomaticZenRule(ZenRule rule) { @@ -332,8 +342,7 @@ public class ZenModeHelper { == PackageManager.PERMISSION_GRANTED) { return true; } else { - String[] packages = - mContext.getPackageManager().getPackagesForUid(Binder.getCallingUid()); + String[] packages = mPm.getPackagesForUid(Binder.getCallingUid()); if (packages != null) { final int packageCount = packages.length; for (int i = 0; i < packageCount; i++) { @@ -384,22 +393,22 @@ public class ZenModeHelper { + " conditionId=" + conditionId + " reason=" + reason + " setRingerMode=" + setRingerMode); newConfig = mConfig.copy(); - } - if (zenMode == Global.ZEN_MODE_OFF) { - newConfig.manualRule = null; - for (ZenRule automaticRule : newConfig.automaticRules.values()) { - if (automaticRule.isAutomaticActive()) { - automaticRule.snoozing = true; + if (zenMode == Global.ZEN_MODE_OFF) { + newConfig.manualRule = null; + for (ZenRule automaticRule : newConfig.automaticRules.values()) { + if (automaticRule.isAutomaticActive()) { + automaticRule.snoozing = true; + } } + } else { + final ZenRule newRule = new ZenRule(); + newRule.enabled = true; + newRule.zenMode = zenMode; + newRule.conditionId = conditionId; + newConfig.manualRule = newRule; } - } else { - final ZenRule newRule = new ZenRule(); - newRule.enabled = true; - newRule.zenMode = zenMode; - newRule.conditionId = conditionId; - newConfig.manualRule = newRule; + setConfig(newConfig, reason, setRingerMode); } - setConfig(newConfig, reason, setRingerMode); } public void dump(PrintWriter pw, String prefix) { @@ -450,16 +459,20 @@ public class ZenModeHelper { return; } config.manualRule = null; // don't restore the manual rule + long time = System.currentTimeMillis(); if (config.automaticRules != null) { for (ZenRule automaticRule : config.automaticRules.values()) { // don't restore transient state from restored automatic rules automaticRule.snoozing = false; automaticRule.condition = null; + automaticRule.creationTime = time; } } } if (DEBUG) Log.d(TAG, "readXml"); - setConfig(config, "readXml"); + synchronized (mConfig) { + setConfig(config, "readXml"); + } } } @@ -484,11 +497,39 @@ public class ZenModeHelper { public void setNotificationPolicy(Policy policy) { if (policy == null || mConfig == null) return; - final ZenModeConfig newConfig = mConfig.copy(); - newConfig.applyNotificationPolicy(policy); - setConfig(newConfig, "setNotificationPolicy"); + synchronized (mConfig) { + final ZenModeConfig newConfig = mConfig.copy(); + newConfig.applyNotificationPolicy(policy); + setConfig(newConfig, "setNotificationPolicy"); + } + } + + /** + * Removes old rule instances whose owner is not installed. + */ + private void cleanUpZenRules() { + long currentTime = System.currentTimeMillis(); + synchronized (mConfig) { + final ZenModeConfig newConfig = mConfig.copy(); + if (newConfig.automaticRules != null) { + for (int i = newConfig.automaticRules.size() - 1; i >= 0; i--) { + ZenRule rule = newConfig.automaticRules.get(newConfig.automaticRules.keyAt(i)); + if (RULE_INSTANCE_GRACE_PERIOD < (currentTime - rule.creationTime)) { + try { + mPm.getPackageInfo(rule.component.getPackageName(), 0); + } catch (PackageManager.NameNotFoundException e) { + newConfig.automaticRules.removeAt(i); + } + } + } + } + setConfig(newConfig, "cleanUpZenRules"); + } } + /** + * @return a copy of the zen mode configuration + */ public ZenModeConfig getConfig() { synchronized (mConfig) { return mConfig.copy(); @@ -517,19 +558,17 @@ public class ZenModeHelper { return true; } mConditions.evaluateConfig(config, false /*processSubscriptions*/); // may modify config - synchronized (mConfig) { - mConfigs.put(config.user, config); - if (DEBUG) Log.d(TAG, "setConfig reason=" + reason, new Throwable()); - ZenLog.traceConfig(reason, mConfig, config); - final boolean policyChanged = !Objects.equals(getNotificationPolicy(mConfig), - getNotificationPolicy(config)); - mConfig = config; - if (config.equals(mConfig)) { - dispatchOnConfigChanged(); - } - if (policyChanged) { - dispatchOnPolicyChanged(); - } + mConfigs.put(config.user, config); + if (DEBUG) Log.d(TAG, "setConfig reason=" + reason, new Throwable()); + ZenLog.traceConfig(reason, mConfig, config); + final boolean policyChanged = !Objects.equals(getNotificationPolicy(mConfig), + getNotificationPolicy(config)); + mConfig = config; + if (config.equals(mConfig)) { + dispatchOnConfigChanged(); + } + if (policyChanged) { + dispatchOnPolicyChanged(); } final String val = Integer.toString(config.hashCode()); Global.putString(mContext.getContentResolver(), Global.ZEN_MODE_CONFIG_ETAG, val); @@ -994,7 +1033,9 @@ public class ZenModeHelper { break; case MSG_SET_CONFIG: ConfigMessageData configData = (ConfigMessageData)msg.obj; - setConfig(configData.config, configData.reason); + synchronized (mConfig) { + setConfig(configData.config, configData.reason); + } break; } } diff --git a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java index eef7d9b9f8f0..7c42ae103e8a 100644 --- a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java +++ b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java @@ -46,7 +46,6 @@ import android.os.RemoteException; import android.os.ShellCommand; import android.os.UserHandle; import android.text.TextUtils; - import android.util.PrintWriterPrinter; import com.android.internal.util.SizedInputStream; @@ -127,19 +126,30 @@ class PackageManagerShellCommand extends ShellCommand { final InstallParams params = makeInstallParams(); final int sessionId = doCreateSession(params.sessionParams, params.installerPackageName, params.userId); - - final String inPath = getNextArg(); - if (inPath == null && params.sessionParams.sizeBytes == 0) { - pw.println("Error: must either specify a package size or an APK file"); - return 1; - } - if (doWriteSession(sessionId, inPath, params.sessionParams.sizeBytes, "base.apk") != 0) { - return 1; - } - if (doCommitSession(sessionId) != 0) { - return 1; + boolean abandonSession = true; + try { + final String inPath = getNextArg(); + if (inPath == null && params.sessionParams.sizeBytes == 0) { + pw.println("Error: must either specify a package size or an APK file"); + return 1; + } + if (doWriteSession(sessionId, inPath, params.sessionParams.sizeBytes, "base.apk", + false /*logSuccess*/) != PackageInstaller.STATUS_SUCCESS) { + return 1; + } + if (doCommitSession(sessionId, false /*logSuccess*/) != PackageInstaller.STATUS_SUCCESS) { + return 1; + } + abandonSession = false; + return 0; + } finally { + if (abandonSession) { + try { + doAbandonSession(sessionId, false /*logSuccess*/); + } catch (Exception ignore) { + } + } } - return 0; } private int runSuspend(boolean suspendedState) { @@ -179,12 +189,12 @@ class PackageManagerShellCommand extends ShellCommand { private int runInstallAbandon() throws RemoteException { final int sessionId = Integer.parseInt(getNextArg()); - return doAbandonSession(sessionId); + return doAbandonSession(sessionId, true /*logSuccess*/); } private int runInstallCommit() throws RemoteException { final int sessionId = Integer.parseInt(getNextArg()); - return doCommitSession(sessionId); + return doCommitSession(sessionId, true /*logSuccess*/); } private int runInstallCreate() throws RemoteException { @@ -213,7 +223,7 @@ class PackageManagerShellCommand extends ShellCommand { final int sessionId = Integer.parseInt(getNextArg()); final String splitName = getNextArg(); final String path = getNextArg(); - return doWriteSession(sessionId, path, sizeBytes, splitName); + return doWriteSession(sessionId, path, sizeBytes, splitName, true /*logSuccess*/); } private int runList() throws RemoteException { @@ -559,7 +569,7 @@ class PackageManagerShellCommand extends ShellCommand { } else { final PackageInfo info = mInterface.getPackageInfo(packageName, 0, userId); if (info == null) { - pw.println("Failure - not installed for " + userId); + pw.println("Failure [not installed for " + userId + "]"); return 1; } final boolean isSystem = @@ -828,8 +838,8 @@ class PackageManagerShellCommand extends ShellCommand { return sessionId; } - private int doWriteSession(int sessionId, String inPath, long sizeBytes, String splitName) - throws RemoteException { + private int doWriteSession(int sessionId, String inPath, long sizeBytes, String splitName, + boolean logSuccess) throws RemoteException { final PrintWriter pw = getOutPrintWriter(); if ("-".equals(inPath)) { inPath = null; @@ -870,7 +880,9 @@ class PackageManagerShellCommand extends ShellCommand { } session.fsync(out); - pw.println("Success: streamed " + total + " bytes"); + if (logSuccess) { + pw.println("Success: streamed " + total + " bytes"); + } return 0; } catch (IOException e) { pw.println("Error: failed to write; " + e.getMessage()); @@ -882,7 +894,7 @@ class PackageManagerShellCommand extends ShellCommand { } } - private int doCommitSession(int sessionId) throws RemoteException { + private int doCommitSession(int sessionId, boolean logSuccess) throws RemoteException { final PrintWriter pw = getOutPrintWriter(); PackageInstaller.Session session = null; try { @@ -896,11 +908,12 @@ class PackageManagerShellCommand extends ShellCommand { final int status = result.getIntExtra(PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE); if (status == PackageInstaller.STATUS_SUCCESS) { - pw.println("Success"); + if (logSuccess) { + System.out.println("Success"); + } } else { pw.println("Failure [" + result.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE) + "]"); - pw.println("Failure details: " + result.getExtras()); } return status; } finally { @@ -908,14 +921,16 @@ class PackageManagerShellCommand extends ShellCommand { } } - private int doAbandonSession(int sessionId) throws RemoteException { + private int doAbandonSession(int sessionId, boolean logSuccess) throws RemoteException { final PrintWriter pw = getOutPrintWriter(); PackageInstaller.Session session = null; try { session = new PackageInstaller.Session( mInterface.getPackageInstaller().openSession(sessionId)); session.abandon(); - pw.println("Success"); + if (logSuccess) { + pw.println("Success"); + } return 0; } finally { IoUtils.closeQuietly(session); diff --git a/services/core/java/com/android/server/policy/GlobalActions.java b/services/core/java/com/android/server/policy/GlobalActions.java index c8523c908fed..5948d3cb665d 100644 --- a/services/core/java/com/android/server/policy/GlobalActions.java +++ b/services/core/java/com/android/server/policy/GlobalActions.java @@ -278,7 +278,7 @@ class GlobalActions implements DialogInterface.OnDismissListener, DialogInterfac } else if (GLOBAL_ACTION_KEY_BUGREPORT.equals(actionKey)) { if (Settings.Global.getInt(mContext.getContentResolver(), Settings.Global.BUGREPORT_IN_POWER_MENU, 0) != 0 && isCurrentUserOwner()) { - mItems.add(getBugReportAction()); + mItems.add(new BugReportAction()); } } else if (GLOBAL_ACTION_KEY_SILENT.equals(actionKey)) { if (mShowSilentToggle) { @@ -367,60 +367,67 @@ class GlobalActions implements DialogInterface.OnDismissListener, DialogInterfac } } - private Action getBugReportAction() { - return new SinglePressAction(com.android.internal.R.drawable.ic_lock_bugreport, - R.string.bugreport_title) { + private class BugReportAction extends SinglePressAction implements LongPressAction { - public void onPress() { - AlertDialog.Builder builder = new AlertDialog.Builder(mContext); - builder.setTitle(com.android.internal.R.string.bugreport_title); - builder.setMessage(com.android.internal.R.string.bugreport_message); - builder.setNegativeButton(com.android.internal.R.string.cancel, null); - builder.setPositiveButton(com.android.internal.R.string.report, - new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - // don't actually trigger the bugreport if we are running stability - // tests via monkey - if (ActivityManager.isUserAMonkey()) { - return; - } - // Add a little delay before executing, to give the - // dialog a chance to go away before it takes a - // screenshot. - mHandler.postDelayed(new Runnable() { - @Override public void run() { - // TODO: select 'progress' flag according to menu choice - try { - ActivityManagerNative.getDefault() - .requestBugReport(true); - } catch (RemoteException e) { - } - } - }, 500); - } - }); - AlertDialog dialog = builder.create(); - dialog.getWindow().setType(WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG); - dialog.show(); - } + public BugReportAction() { + super(com.android.internal.R.drawable.ic_lock_bugreport, R.string.bugreport_title); + } - public boolean showDuringKeyguard() { - return true; + @Override + public void onPress() { + // don't actually trigger the bugreport if we are running stability + // tests via monkey + if (ActivityManager.isUserAMonkey()) { + return; } + // Add a little delay before executing, to give the + // dialog a chance to go away before it takes a + // screenshot. + // TODO: remove once screenshots are handled by Shell (instead of dumpstate) + mHandler.postDelayed(new Runnable() { + @Override + public void run() { + try { + // Take a "heavy" bugreport: it's more user friendly, but causes more + // interference. + ActivityManagerNative.getDefault().requestBugReport(true); + } catch (RemoteException e) { + } + } + }, 500); + } - public boolean showBeforeProvisioning() { + @Override + public boolean onLongPress() { + // don't actually trigger the bugreport if we are running stability + // tests via monkey + if (ActivityManager.isUserAMonkey()) { return false; } - - @Override - public String getStatus() { - return mContext.getString( - com.android.internal.R.string.bugreport_status, - Build.VERSION.RELEASE, - Build.ID); + try { + // Take a "light" bugreport, with less interference. + ActivityManagerNative.getDefault().requestBugReport(false); + } catch (RemoteException e) { } - }; + return true; + } + + public boolean showDuringKeyguard() { + return true; + } + + @Override + public boolean showBeforeProvisioning() { + return false; + } + + @Override + public String getStatus() { + return mContext.getString( + com.android.internal.R.string.bugreport_status, + Build.VERSION.RELEASE, + Build.ID); + } } private Action getSettingsAction() { @@ -742,13 +749,6 @@ class GlobalActions implements DialogInterface.OnDismissListener, DialogInterfac mIcon = icon; } - protected SinglePressAction(int iconResId, CharSequence message) { - mIconResId = iconResId; - mMessageResId = 0; - mMessage = message; - mIcon = null; - } - public boolean isEnabled() { return true; } diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java index 189ed332abef..fb5f21ab9ca6 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -141,6 +141,14 @@ public final class SystemServer { "com.android.server.MountService$Lifecycle"; private static final String PERSISTENT_DATA_BLOCK_PROP = "ro.frp.pst"; + /** + * Default theme used by the system context. This is used to style + * system-provided dialogs, such as the Power Off dialog, and other + * visual content. + */ + private static final int DEFAULT_SYSTEM_THEME = + com.android.internal.R.style.Theme_Material_DayNight_DarkActionBar; + private final int mFactoryTestMode; private Timer mProfilerSnapshotTimer; @@ -320,7 +328,7 @@ public final class SystemServer { private void createSystemContext() { ActivityThread activityThread = ActivityThread.systemMain(); mSystemContext = activityThread.getSystemContext(); - mSystemContext.setTheme(android.R.style.Theme_Material_DayNight_DarkActionBar); + mSystemContext.setTheme(DEFAULT_SYSTEM_THEME); } /** |